-
-
Notifications
You must be signed in to change notification settings - Fork 6
feat: migrate from ai/rsc to ai/ui (useChat) #579
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
40f56ad
e3ebcbf
55f6f26
7dd2b66
cf39484
92113aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,17 @@ | ||
| import { Chat } from '@/components/chat' | ||
| import { nanoid } from '@/lib/utils' | ||
| import { AI } from './actions' | ||
| import { ChatProvider } from '@/components/chat-provider' | ||
| import { MapDataProvider } from '@/components/map/map-data-context' | ||
|
|
||
| export const maxDuration = 60 | ||
|
|
||
| import { MapDataProvider } from '@/components/map/map-data-context' | ||
|
|
||
| export default function Page() { | ||
| const id = nanoid() | ||
| return ( | ||
| <AI initialAIState={{ chatId: id, messages: [] }}> | ||
| <ChatProvider chatId={id}> | ||
| <MapDataProvider> | ||
| <Chat id={id} /> | ||
| </MapDataProvider> | ||
| </AI> | ||
| </ChatProvider> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,76 +1,58 @@ | ||
| import { notFound, redirect } from 'next/navigation'; | ||
| import { Chat } from '@/components/chat'; | ||
| import { getChat, getChatMessages } from '@/lib/actions/chat'; // Added getChatMessages | ||
| import { AI } from '@/app/actions'; | ||
| import { ChatProvider } from '@/components/chat-provider'; | ||
| import { getChat, getChatMessages } from '@/lib/actions/chat'; | ||
| import { MapDataProvider } from '@/components/map/map-data-context'; | ||
| import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user'; // For server-side auth | ||
| import type { AIMessage } from '@/lib/types'; // For AIMessage type | ||
| import type { Message as DrizzleMessage } from '@/lib/actions/chat-db'; // For DrizzleMessage type | ||
| import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user'; | ||
| import type { Message } from 'ai/react'; | ||
| import type { Message as DrizzleMessage } from '@/lib/actions/chat-db'; | ||
|
|
||
| export const maxDuration = 60; | ||
|
|
||
| export interface SearchPageProps { | ||
| params: Promise<{ id: string }>; // Keep as is for now | ||
| params: Promise<{ id: string }>; | ||
| } | ||
|
|
||
| export async function generateMetadata({ params }: SearchPageProps) { | ||
| const { id } = await params; // Keep as is for now | ||
| // TODO: Metadata generation might need authenticated user if chats are private | ||
| // For now, assuming getChat can be called or it handles anon access for metadata appropriately | ||
| const userId = await getCurrentUserIdOnServer(); // Attempt to get user for metadata | ||
| const chat = await getChat(id, userId || 'anonymous'); // Pass userId or 'anonymous' if none | ||
| const { id } = await params; | ||
| const userId = await getCurrentUserIdOnServer(); | ||
| const chat = await getChat(id, userId || 'anonymous'); | ||
| return { | ||
| title: chat?.title?.toString().slice(0, 50) || 'Search', | ||
| }; | ||
| } | ||
|
|
||
| export default async function SearchPage({ params }: SearchPageProps) { | ||
| const { id } = await params; // Keep as is for now | ||
| const { id } = await params; | ||
| const userId = await getCurrentUserIdOnServer(); | ||
|
|
||
| if (!userId) { | ||
| // If no user, redirect to login or show appropriate page | ||
| // For now, redirecting to home, but a login page would be better. | ||
| redirect('/'); | ||
| } | ||
|
|
||
| const chat = await getChat(id, userId); | ||
|
|
||
| if (!chat) { | ||
| // If chat doesn't exist or user doesn't have access (handled by getChat) | ||
| notFound(); | ||
| } | ||
|
|
||
| // Fetch messages for the chat | ||
| const dbMessages: DrizzleMessage[] = await getChatMessages(chat.id); | ||
|
|
||
| // Transform DrizzleMessages to AIMessages | ||
| const initialMessages: AIMessage[] = dbMessages.map((dbMsg): AIMessage => { | ||
| return { | ||
| const validRoles = new Set(['user', 'assistant', 'system']) | ||
| const initialMessages: Message[] = dbMessages | ||
| .filter((dbMsg) => validRoles.has(dbMsg.role)) | ||
| .map((dbMsg): Message => ({ | ||
| id: dbMsg.id, | ||
| role: dbMsg.role as AIMessage['role'], // Cast role, ensure AIMessage['role'] includes all dbMsg.role possibilities | ||
| role: dbMsg.role as Message['role'], | ||
| content: dbMsg.content, | ||
| createdAt: dbMsg.createdAt ? new Date(dbMsg.createdAt) : undefined, | ||
| // 'type' and 'name' are not in the basic Drizzle 'messages' schema. | ||
| // These would be undefined unless specific logic is added to derive them. | ||
| // For instance, if a message with role 'tool' should have a 'name', | ||
| // or if some messages have a specific 'type' based on content or other flags. | ||
| // This mapping assumes standard user/assistant messages primarily. | ||
| }; | ||
| }); | ||
| })); | ||
|
|
||
| return ( | ||
| <AI | ||
| initialAIState={{ | ||
| chatId: chat.id, | ||
| messages: initialMessages, // Use the transformed messages from the database | ||
| // isSharePage: true, // This was in PR#533, but share functionality is removed. | ||
| // If needed for styling or other logic, it can be set. | ||
| }} | ||
| > | ||
| <ChatProvider chatId={chat.id} initialMessages={initialMessages}> | ||
| <MapDataProvider> | ||
| <Chat id={id} /> | ||
| </MapDataProvider> | ||
| </AI> | ||
| </ChatProvider> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,70 +1,128 @@ | ||
| 'use client' | ||
|
|
||
| import { StreamableValue, useUIState } from 'ai/rsc' | ||
| import type { AI, UIState } from '@/app/actions' | ||
| import type { Message } from 'ai/react' | ||
| import { CollapsibleMessage } from './collapsible-message' | ||
| import { Section } from './section' | ||
| import { BotMessage } from './message' | ||
| import { UserMessage } from './user-message' | ||
| import { ToolResultRenderer } from './tool-result-renderer' | ||
| import { useChatContext, type Annotation } from './chat-provider' | ||
| import { Copilot } from './copilot' | ||
| import SearchRelated from './search-related' | ||
|
|
||
| interface ChatMessagesProps { | ||
| messages: UIState | ||
| messages: Message[] | ||
| } | ||
|
|
||
| export function ChatMessages({ messages }: ChatMessagesProps) { | ||
| if (!messages.length) { | ||
| const { annotations, isLoading } = useChatContext() | ||
|
|
||
| if (!messages.length && !annotations.length) { | ||
| return null | ||
| } | ||
|
|
||
| // Group messages based on ID, and if there are multiple messages with the same ID, combine them into one message | ||
| const groupedMessages = messages.reduce( | ||
| (acc: { [key: string]: any }, message) => { | ||
| if (!acc[message.id]) { | ||
| acc[message.id] = { | ||
| const renderedMessages: { | ||
| id: string | ||
| component: React.ReactNode | ||
| isCollapsed?: boolean | ||
| isAssistant?: boolean | ||
| }[] = [] | ||
|
|
||
| // Render tool result annotations first (they come before the text) | ||
| const toolAnnotations = annotations.filter((a: Annotation) => a.type === 'tool_result') | ||
| for (let i = 0; i < toolAnnotations.length; i++) { | ||
| const ann = toolAnnotations[i] | ||
| renderedMessages.push({ | ||
| id: `tool-${ann.toolName}-${i}`, | ||
| component: <ToolResultRenderer toolName={ann.toolName} result={ann.result} />, | ||
| isCollapsed: true | ||
| }) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
Comment on lines
+32
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Duplicates
🤖 Prompt for AI Agents
Comment on lines
+31
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify how tool_result annotations are emitted (per-turn marker?) and whether the route
# tags them with any message/turn identifier.
rg -nP -C3 "tool_result|allToolOutputs|toolResponses" app/api/chat/stream/route.ts
# Look for any per-message annotation correlation in chat-provider/chat-messages.
rg -nP -C2 "messageId|turnIndex|annotations" components/chat-provider.tsx components/chat-messages.tsxRepository: QueueLab/QCX Length of output: 5381 🏁 Script executed: # View the full rendering logic in chat-messages.tsx around the tool annotations
sed -n '30,85p' components/chat-messages.tsxRepository: QueueLab/QCX Length of output: 1771 Tool result annotations render at the top of the message list and accumulate across conversation turns.
In
To fix: emit a 🤖 Prompt for AI Agents |
||
|
|
||
| // Render chat messages | ||
| for (const message of messages) { | ||
| if (message.role === 'user') { | ||
| renderedMessages.push({ | ||
| id: message.id, | ||
| component: <UserMessage content={message.content} /> | ||
| }) | ||
| } else if (message.role === 'assistant') { | ||
| if (message.content) { | ||
| renderedMessages.push({ | ||
| id: message.id, | ||
| components: [], | ||
| isCollapsed: message.isCollapsed | ||
| component: ( | ||
| <Section title="response"> | ||
| <BotMessage content={message.content} /> | ||
| </Section> | ||
| ), | ||
| isAssistant: true | ||
| }) | ||
| } | ||
|
|
||
| // Render tool invocations | ||
| if (message.toolInvocations) { | ||
| for (const invocation of message.toolInvocations) { | ||
| if (invocation.state === 'result') { | ||
| renderedMessages.push({ | ||
| id: `${message.id}-tool-${invocation.toolCallId}`, | ||
| component: ( | ||
| <ToolResultRenderer | ||
| toolName={invocation.toolName} | ||
| result={invocation.result} | ||
| /> | ||
| ), | ||
| isCollapsed: true | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| acc[message.id].components.push(message.component) | ||
| return acc | ||
| }, | ||
| {} | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // Convert grouped messages into an array with explicit type | ||
| const groupedMessagesArray = Object.values(groupedMessages).map(group => ({ | ||
| ...group, | ||
| components: group.components as React.ReactNode[] | ||
| })) as { | ||
| id: string | ||
| components: React.ReactNode[] | ||
| isCollapsed?: StreamableValue<boolean> | ||
| }[] | ||
| // Render inquiry annotation if present | ||
| const inquiry = annotations.find((a: Annotation) => a.type === 'inquiry') | ||
| if (inquiry) { | ||
| renderedMessages.push({ | ||
| id: 'inquiry', | ||
| component: <Copilot inquiry={{ value: inquiry.data }} /> | ||
| }) | ||
| } | ||
|
|
||
| // Render related queries annotation | ||
| const related = annotations.findLast?.((a: Annotation) => a.type === 'related') | ||
| if (related && related.relatedQueries?.items?.length > 0) { | ||
| renderedMessages.push({ | ||
| id: 'related', | ||
| component: ( | ||
| <Section title="Related" separator={true}> | ||
| <SearchRelated relatedQueries={related.relatedQueries} /> | ||
| </Section> | ||
| ) | ||
| }) | ||
| } | ||
|
Comment on lines
+82
to
+102
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Resolution results not rendered /api/chat/stream emits a resolution_search_result data annotation, but the client never renders that annotation type, so resolution-search UI (carousel/GeoJSON/map preview) will not appear. Agent Prompt
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // Find last assistant message index for isLastMessage prop | ||
| let lastAssistantIndex = -1 | ||
| for (let i = renderedMessages.length - 1; i >= 0; i--) { | ||
| if (renderedMessages[i].isAssistant) { | ||
| lastAssistantIndex = i | ||
| break | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <> | ||
| {groupedMessagesArray.map( | ||
| ( | ||
| groupedMessage: { | ||
| id: string | ||
| components: React.ReactNode[] | ||
| isCollapsed?: StreamableValue<boolean> | ||
| }, | ||
| index | ||
| ) => ( | ||
| <CollapsibleMessage | ||
| key={`${groupedMessage.id}`} | ||
| message={{ | ||
| id: groupedMessage.id, | ||
| component: groupedMessage.components.map((component, i) => ( | ||
| <div key={`${groupedMessage.id}-${i}`}>{component}</div> | ||
| )), | ||
| isCollapsed: groupedMessage.isCollapsed | ||
| }} | ||
| isLastMessage={ | ||
| groupedMessage.id === messages[messages.length - 1].id | ||
| } | ||
| /> | ||
| ) | ||
| )} | ||
| {renderedMessages.map((msg, index) => ( | ||
| <CollapsibleMessage | ||
| key={msg.id} | ||
| message={{ | ||
| id: msg.id, | ||
| component: <div>{msg.component}</div>, | ||
| isCollapsed: msg.isCollapsed | ||
| }} | ||
| isLastMessage={index === lastAssistantIndex} | ||
| /> | ||
| ))} | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| </> | ||
| ) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isLoadingis destructured but never used.Either thread it into
CollapsibleMessage/the assistant skeleton (the prior RSC version used a streaming indicator) or drop it.🤖 Prompt for AI Agents