-
-
Notifications
You must be signed in to change notification settings - Fork 7
Migrate from ai/rsc to ai/ui #444
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
5695479
361625a
bab0fcf
a3261de
12da5a5
3d53060
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,70 +1,123 @@ | ||
| 'use client' | ||
|
|
||
| import { StreamableValue, useUIState } from 'ai/rsc' | ||
| import type { AI, UIState } from '@/app/actions' | ||
| import { Message } from 'ai' | ||
| import { CollapsibleMessage } from './collapsible-message' | ||
| import { UserMessage } from './user-message' | ||
| import { BotMessage } from './message' | ||
| import { Section } from './section' | ||
| import SearchRelated from './search-related' | ||
| import { FollowupPanel } from './followup-panel' | ||
| import { SearchSection } from './search-section' | ||
| import RetrieveSection from './retrieve-section' | ||
| import { VideoSearchSection } from './video-search-section' | ||
| import { MapQueryHandler } from './map/map-query-handler' | ||
| import { CopilotDisplay } from './copilot-display' | ||
| import { ResolutionSearchSection } from './resolution-search-section' | ||
|
|
||
| interface ChatMessagesProps { | ||
| messages: UIState | ||
| messages: Message[] | ||
| } | ||
|
|
||
| export function ChatMessages({ messages }: ChatMessagesProps) { | ||
| if (!messages.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] = { | ||
| id: message.id, | ||
| components: [], | ||
| isCollapsed: message.isCollapsed | ||
| return ( | ||
| <> | ||
| {messages.map((message, index) => { | ||
| const { role, content, id, toolInvocations, data } = message | ||
|
|
||
| if (role === 'user') { | ||
| return ( | ||
| <CollapsibleMessage | ||
| key={id} | ||
| message={{ | ||
| id, | ||
| component: ( | ||
| <UserMessage | ||
| content={content} | ||
| showShare={index === 0} | ||
| /> | ||
| ) | ||
| }} | ||
| isLastMessage={index === messages.length - 1} | ||
| /> | ||
| ) | ||
| } | ||
| } | ||
| 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> | ||
| }[] | ||
| if (role === 'assistant') { | ||
| const extraData = Array.isArray(data) ? data : [] | ||
|
|
||
| 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 | ||
| } | ||
| /> | ||
| ) | ||
| )} | ||
| return ( | ||
| <CollapsibleMessage | ||
| key={id} | ||
| message={{ | ||
| id, | ||
| component: ( | ||
| <div className="flex flex-col gap-4"> | ||
| {content && ( | ||
| <Section title="response"> | ||
| <BotMessage content={content} /> | ||
| </Section> | ||
| )} | ||
|
|
||
| {toolInvocations?.map((toolInvocation) => { | ||
| const { toolName, toolCallId, state } = toolInvocation | ||
|
|
||
| if (state === 'result') { | ||
| const { result } = toolInvocation | ||
|
|
||
| switch (toolName) { | ||
| case 'search': | ||
| return <SearchSection key={toolCallId} result={JSON.stringify(result)} /> | ||
| case 'retrieve': | ||
| return <RetrieveSection key={toolCallId} data={result} /> | ||
| case 'videoSearch': | ||
| return <VideoSearchSection key={toolCallId} result={JSON.stringify(result)} /> | ||
| case 'geospatialQueryTool': | ||
| if (result.type === 'MAP_QUERY_TRIGGER') { | ||
| return <MapQueryHandler key={toolCallId} toolOutput={result} /> | ||
| } | ||
| return null | ||
| default: | ||
| return null | ||
| } | ||
| } | ||
| return null | ||
| })} | ||
|
|
||
| {extraData.map((d: any, i) => { | ||
| if (d.type === 'related') { | ||
| return ( | ||
| <Section key={i} title="Related" separator={true}> | ||
| <SearchRelated relatedQueries={d.object} /> | ||
| </Section> | ||
| ) | ||
| } | ||
| if (d.type === 'inquiry') { | ||
| return <CopilotDisplay key={i} content={d.object.question} /> | ||
| } | ||
| if (d.type === 'resolution_search_result') { | ||
| return <ResolutionSearchSection key={i} result={d.object} /> | ||
| } | ||
| return null | ||
| })} | ||
|
|
||
| {index === messages.length - 1 && role === 'assistant' && ( | ||
| <Section title="Follow-up" className="pb-8"> | ||
| <FollowupPanel /> | ||
| </Section> | ||
| )} | ||
|
Comment on lines
49
to
111
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.
Also, the SuggestionIntroduce a small discriminated-union type for Example: type ChatDataItem =
| { type: 'related'; object: PartialRelated }
| { type: 'inquiry'; object: { question?: string } }
| { type: 'resolution_search_result'; object: unknown }
const extraData: ChatDataItem[] = Array.isArray(message.data)
? (message.data as ChatDataItem[])
: []Then use stable keys (e.g., hash of content or Reply with "@CharlieHelps yes please" if you’d like me to add a commit that adds these types and updates the renderer accordingly. |
||
| </div> | ||
| ) | ||
| }} | ||
| isLastMessage={index === messages.length - 1} | ||
| /> | ||
| ) | ||
| } | ||
| return null | ||
| })} | ||
| </> | ||
| ) | ||
| } | ||
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.
Tool implementations now return
{ error: string }objects on failure, butChatMessagesrenders tool results by passingresultintoSearchSection/RetrieveSection/VideoSearchSectionwithout checking for anerrorshape. This will likely cause runtime errors or confusing empty sections.At minimum, error payloads need consistent rendering.
Suggestion
Handle tool error results explicitly in
ChatMessages:result?.error, render aSectionwith an errorCard/message.Reply with "@CharlieHelps yes please" if you’d like me to add a commit implementing consistent tool error rendering.