feat(chat): update chat page layout and enhance message handling

- Refactored the chat page to utilize a new `ChatSidebar` component for better organization and user experience.
- Improved message handling by simplifying session creation logic and ensuring proper state management.
- Updated UI elements for consistency, including button labels and input handling.
- Enhanced message rendering to support tool call outputs, improving the chat interaction flow.

These changes aim to streamline the chat interface and improve overall usability.
This commit is contained in:
abhi1992002
2026-01-30 15:02:33 +05:30
parent c3a126e705
commit c6e5f83de8
3 changed files with 175 additions and 76 deletions

View File

@@ -191,6 +191,9 @@ importers:
moment:
specifier: 2.30.1
version: 2.30.1
motion:
specifier: 12.29.2
version: 12.29.2(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next:
specifier: 15.4.10
version: 15.4.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -5164,6 +5167,20 @@ packages:
react-dom:
optional: true
framer-motion@12.29.2:
resolution: {integrity: sha512-lSNRzBJk4wuIy0emYQ/nfZ7eWhqud2umPKw2QAQki6uKhZPKm2hRQHeQoHTG9MIvfobb+A/LbEWPJU794ZUKrg==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
fs-extra@10.1.0:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
engines: {node: '>=12'}
@@ -6175,9 +6192,29 @@ packages:
motion-dom@12.24.8:
resolution: {integrity: sha512-wX64WITk6gKOhaTqhsFqmIkayLAAx45SVFiMnJIxIrH5uqyrwrxjrfo8WX9Kh8CaUAixjeMn82iH0W0QT9wD5w==}
motion-dom@12.29.2:
resolution: {integrity: sha512-/k+NuycVV8pykxyiTCoFzIVLA95Nb1BFIVvfSu9L50/6K6qNeAYtkxXILy/LRutt7AzaYDc2myj0wkCVVYAPPA==}
motion-utils@12.23.28:
resolution: {integrity: sha512-0W6cWd5Okoyf8jmessVK3spOmbyE0yTdNKujHctHH9XdAE4QDuZ1/LjSXC68rrhsJU+TkzXURC5OdSWh9ibOwQ==}
motion-utils@12.29.2:
resolution: {integrity: sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==}
motion@12.29.2:
resolution: {integrity: sha512-jMpHdAzEDF1QQ055cB+1lOBLdJ6ialVWl6QQzpJI2OvmHequ7zFVHM2mx0HNAy+Tu4omUlApfC+4vnkX0geEOg==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -13602,6 +13639,16 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
framer-motion@12.29.2(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
motion-dom: 12.29.2
motion-utils: 12.29.2
tslib: 2.8.1
optionalDependencies:
'@emotion/is-prop-valid': 1.2.2
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
fs-extra@10.1.0:
dependencies:
graceful-fs: 4.2.11
@@ -14896,8 +14943,23 @@ snapshots:
dependencies:
motion-utils: 12.23.28
motion-dom@12.29.2:
dependencies:
motion-utils: 12.29.2
motion-utils@12.23.28: {}
motion-utils@12.29.2: {}
motion@12.29.2(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
framer-motion: 12.29.2(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
tslib: 2.8.1
optionalDependencies:
'@emotion/is-prop-valid': 1.2.2
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
ms@2.1.3: {}
msw-storybook-addon@2.0.6(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3)):

View File

@@ -1,26 +1,28 @@
'use client';
"use client";
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState, useMemo } from 'react';
import { parseAsString, useQueryState } from 'nuqs';
import { postV2CreateSession } from '@/app/api/__generated__/endpoints/chat/chat';
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useState, useMemo } from "react";
import { parseAsString, useQueryState } from "nuqs";
import { ChatSidebar } from "./tools/components/ChatSidebar/ChatSidebar";
export default function Page() {
const [sessionId, setSessionId] = useQueryState('sessionId', parseAsString);
const [sessionId] = useQueryState("sessionId", parseAsString);
const [isCreating, setIsCreating] = useState(false);
const [input, setInput] = useState('');
const [input, setInput] = useState("");
const transport = useMemo(() => {
if (!sessionId) return null;
return new DefaultChatTransport({
api: `/api/chat/sessions/${sessionId}/stream`,
prepareSendMessagesRequest: ({ id, messages, trigger, messageId }) => {
prepareSendMessagesRequest: ({ messages }) => {
const last = messages[messages.length - 1];
return {
body: {
message: last.parts?.map((p) => (p.type === 'text' ? p.text : '')).join(''),
is_user_message: last.role === 'user',
message: last.parts
?.map((p) => (p.type === "text" ? p.text : ""))
.join(""),
is_user_message: last.role === "user",
context: null,
},
};
@@ -32,59 +34,22 @@ export default function Page() {
transport: transport ?? undefined,
});
async function createSession(): Promise<string | null> {
setIsCreating(true);
try {
const response = await postV2CreateSession({
body: JSON.stringify({}),
});
if (response.status === 200 && response.data?.id) {
return response.data.id;
}
console.error('[Copilot2] Failed to create session:', response);
return null;
} catch (error) {
console.error('[Copilot2] Error creating session:', error);
return null;
} finally {
setIsCreating(false);
}
}
async function handleNewSession() {
const newSessionId = await createSession();
if (newSessionId) {
setSessionId(newSessionId);
}
}
async function handleStartChat(prompt: string) {
if (!prompt.trim() || isCreating) return;
const newSessionId = await createSession();
if (newSessionId) {
await setSessionId(newSessionId);
sendMessage({ text: prompt });
setInput('');
}
if (!prompt.trim()) return;
sendMessage({ text: prompt });
setInput("");
}
// Show landing page when no session exists
if (!sessionId) {
return (
<div className="flex h-full">
<div className="flex w-64 flex-col border-r border-zinc-200 bg-zinc-50 p-4">
<button
onClick={handleNewSession}
disabled={isCreating}
className="rounded-md bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
>
{isCreating ? 'Creating...' : 'New Session'}
</button>
</div>
<ChatSidebar isCreating={isCreating} setIsCreating={setIsCreating} />
<div className="flex h-full flex-1 flex-col items-center justify-center bg-zinc-100 p-4">
<h2 className="mb-4 text-xl font-semibold text-zinc-700">Start a new conversation</h2>
<h2 className="mb-4 text-xl font-semibold text-zinc-700">
Start a new conversation
</h2>
<form
onSubmit={(e) => {
e.preventDefault();
@@ -104,7 +69,7 @@ export default function Page() {
disabled={isCreating || !input.trim()}
className="mt-2 w-full rounded-md bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
>
{isCreating ? 'Starting...' : 'Start Chat'}
{isCreating ? "Starting..." : "Start Chat"}
</button>
</form>
</div>
@@ -115,28 +80,55 @@ export default function Page() {
return (
<div className="flex h-full">
{/* Sidebar */}
<div className="flex w-64 flex-col border-r border-zinc-200 bg-zinc-50 p-4">
<button
onClick={handleNewSession}
disabled={isCreating}
className="rounded-md bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
>
{isCreating ? 'Creating...' : 'New Session'}
</button>
</div>
<ChatSidebar isCreating={isCreating} setIsCreating={setIsCreating} />
{/* Chat area */}
<div className="flex h-full flex-1 flex-col bg-zinc-100 p-4">
<div className="text-sm text-zinc-500 mb-2">Session ID: {sessionId}</div>
<div className="flex h-full flex-1 flex-col p-4">
<div className="mb-2 text-sm text-zinc-500">
Session ID: {sessionId}
</div>
<div className="flex-1 overflow-y-auto">
{messages.map((message) => (
<div key={message.id}>
{message.role === 'user' ? 'User: ' : 'AI: '}
{message.parts.map((part, index) =>
part.type === 'text' ? <p key={index}>{part.text}</p> : null,
)}
<div key={message.id} className="flex flex-col gap-4">
{message.parts.map((part, index) => {
if (part.type === "tool-find_block") {
return (
<div
key={index}
className="w-fit rounded-xl border border-zinc-200 bg-zinc-100 p-2 text-xs text-zinc-700"
>
{part.state === "input-streaming" && (
<p>Finding blocks for you</p>
)}
{part.state === "input-available" && (
<p>
Searching blocks for{" "}
{(part.input as { query: string }).query}
</p>
)}
{part.state === "output-available" && (
<p>
Found
{
(
JSON.parse(part.output as string) as {
count: number;
}
).count
}
blocks
</p>
)}
</div>
);
} else if (part.type === "text") {
return <p key={index}>{part.text}</p>;
}
})}
</div>
))}
{status === "submitted" && <p>Thinking....</p>}
{error && <div className="text-red-500">Error: {error.message}</div>}
</div>
@@ -145,21 +137,21 @@ export default function Page() {
e.preventDefault();
if (input.trim()) {
sendMessage({ text: input });
setInput('');
setInput("");
}
}}
>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={status !== 'ready'}
disabled={status !== "ready"}
placeholder="Say something..."
/>
<button type="submit" disabled={status !== 'ready'}>
<button type="submit" disabled={status !== "ready"}>
Submit
</button>
</form>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,45 @@
import { useState } from "react";
import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat";
import { parseAsString, useQueryState } from "nuqs";
export const ChatSidebar = ({ isCreating, setIsCreating }: { isCreating: boolean, setIsCreating: (isCreating: boolean) => void }) => {
const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString);
async function createSession(): Promise<string | null> {
setIsCreating(true);
try {
const response = await postV2CreateSession({
body: JSON.stringify({}),
});
if (response.status === 200 && response.data?.id) {
return response.data.id;
}
return null;
} catch (error) {
return null;
} finally {
setIsCreating(false);
}
}
async function handleNewSession() {
const newSessionId = await createSession();
if (newSessionId) {
setSessionId(newSessionId);
}
}
return (
<div className="flex w-64 flex-col border-r border-zinc-200 bg-zinc-50 p-4">
<button
onClick={handleNewSession}
disabled={isCreating}
className="rounded-md bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
>
{isCreating ? "Creating..." : "New Session"}
</button>
</div>
);
};