Skip to content

Commit e5c3056

Browse files
authored
Merge pull request #389 from miurla/feat/enabled-load-chat
Save and load annotation data
2 parents 49475ac + e01f811 commit e5c3056

File tree

9 files changed

+232
-48
lines changed

9 files changed

+232
-48
lines changed

app/api/chat/route.ts

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import {
22
streamText,
33
createDataStreamResponse,
4-
Message,
54
convertToCoreMessages,
6-
generateId,
75
JSONValue,
86
ToolInvocation
97
} from 'ai'
108
import { researcher } from '@/lib/agents/researcher'
119
import { generateRelatedQuestions } from '@/lib/agents/generate-related-questions'
1210
import { cookies } from 'next/headers'
1311
import { getChat, saveChat } from '@/lib/actions/chat'
12+
import { ExtendedCoreMessage } from '@/lib/types'
13+
import { convertToExtendedCoreMessages } from '@/lib/utils'
1414

1515
export const maxDuration = 30
1616

@@ -19,7 +19,11 @@ const DEFAULT_MODEL = 'openai:gpt-4o-mini'
1919
export async function POST(req: Request) {
2020
const { messages, id: chatId } = await req.json()
2121

22+
// streamText requires core messages
2223
const coreMessages = convertToCoreMessages(messages)
24+
// convertToExtendedCoreMessages for saving annotations
25+
const extendedCoreMessages = convertToExtendedCoreMessages(messages)
26+
2327
const cookieStore = await cookies()
2428
const modelFromCookie = cookieStore.get('selected-model')?.value
2529
const model = modelFromCookie || DEFAULT_MODEL
@@ -31,24 +35,16 @@ export async function POST(req: Request) {
3135
model
3236
})
3337

34-
let toolResults: ToolInvocation[] = []
3538
const result = streamText({
3639
...researcherConfig,
37-
onStepFinish(event) {
38-
// onFinish's event.toolResults is empty. Use onStepFinish to get the tool results.
39-
if (event.stepType === 'initial') {
40-
toolResults = event.toolResults
41-
}
42-
},
4340
onFinish: async event => {
4441
const responseMessages = event.response.messages
4542

4643
let annotation: JSONValue = {
4744
type: 'related-questions',
4845
data: {
4946
items: []
50-
},
51-
status: 'loading'
47+
}
5248
}
5349

5450
// Notify related questions loading
@@ -63,21 +59,22 @@ export async function POST(req: Request) {
6359
// Update the annotation with the related questions
6460
annotation = {
6561
...annotation,
66-
data: relatedQuestions.object,
67-
status: 'done'
62+
data: relatedQuestions.object
6863
}
6964

7065
// Send related questions to client
7166
dataStream.writeMessageAnnotation(annotation)
7267

7368
// Create the message to save
74-
const generatedMessage: Message = {
75-
role: 'assistant',
76-
content: event.text,
77-
toolInvocations: toolResults,
78-
annotations: [annotation],
79-
id: generateId()
80-
}
69+
const generatedMessages = [
70+
...extendedCoreMessages,
71+
...responseMessages.slice(0, -1),
72+
{
73+
role: 'data',
74+
content: annotation
75+
},
76+
responseMessages[responseMessages.length - 1]
77+
] as ExtendedCoreMessage[]
8178

8279
// Get the chat from the database if it exists, otherwise create a new one
8380
const savedChat = (await getChat(chatId)) ?? {
@@ -89,10 +86,12 @@ export async function POST(req: Request) {
8986
id: chatId
9087
}
9188

89+
console.log('generatedMessages', generatedMessages)
90+
9291
// Save chat with complete response and related questions
9392
await saveChat({
9493
...savedChat,
95-
messages: [...savedChat.messages, generatedMessage]
94+
messages: generatedMessages
9695
})
9796
}
9897
})

app/search/[id]/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { notFound, redirect } from 'next/navigation'
22
import { Chat } from '@/components/chat'
33
import { getChat } from '@/lib/actions/chat'
4+
import { convertToCoreMessages } from 'ai'
5+
import { convertToUIMessages } from '@/lib/utils'
46

57
export const maxDuration = 60
68

@@ -20,6 +22,8 @@ export default async function SearchPage(props: {
2022
const userId = 'anonymous'
2123
const { id } = await props.params
2224
const chat = await getChat(id, userId)
25+
// convertToUIMessages for useChat hook
26+
const messages = convertToUIMessages(chat?.messages || [])
2327

2428
if (!chat) {
2529
redirect('/')
@@ -29,5 +33,7 @@ export default async function SearchPage(props: {
2933
notFound()
3034
}
3135

32-
return <Chat id={id} />
36+
console.log(chat)
37+
38+
return <Chat id={id} savedMessages={messages} />
3339
}

components/answer-section.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function AnswerSection({
2929
header={header}
3030
isOpen={isOpen}
3131
onOpenChange={onOpenChange}
32+
showBorder={false}
3233
>
3334
{content ? <BotMessage message={content} /> : <DefaultSkeleton />}
3435
</CollapsibleMessage>

components/chat.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
'use client'
22

3-
import { useChat } from 'ai/react'
3+
import { Message, useChat } from 'ai/react'
44
import { ChatMessages } from './chat-messages'
55
import { ChatPanel } from './chat-panel'
66

7-
export function Chat({ id }: { id: string }) {
7+
export function Chat({
8+
id,
9+
savedMessages = []
10+
}: {
11+
id: string
12+
savedMessages?: Message[]
13+
}) {
814
const {
915
messages,
1016
input,
@@ -15,11 +21,13 @@ export function Chat({ id }: { id: string }) {
1521
stop,
1622
append
1723
} = useChat({
24+
initialMessages: savedMessages,
25+
id: 'chat',
1826
body: {
1927
id
2028
},
2129
onFinish: () => {
22-
// window.history.replaceState({}, '', `/search/${id}`)
30+
window.history.replaceState({}, '', `/search/${id}`)
2331
}
2432
})
2533

components/collapsible-message.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from './ui/collapsible'
77
import { Separator } from './ui/separator'
88
import { cn } from '@/lib/utils'
9+
import { IconLogo } from './ui/icons'
910

1011
interface CollapsibleMessageProps {
1112
children: React.ReactNode
@@ -14,6 +15,7 @@ interface CollapsibleMessageProps {
1415
isOpen?: boolean
1516
header?: React.ReactNode
1617
onOpenChange?: (open: boolean) => void
18+
showBorder?: boolean
1719
}
1820

1921
export function CollapsibleMessage({
@@ -22,22 +24,30 @@ export function CollapsibleMessage({
2224
isCollapsible = false,
2325
isOpen = true,
2426
header,
25-
onOpenChange
27+
onOpenChange,
28+
showBorder = true
2629
}: CollapsibleMessageProps) {
2730
const content = <div className="py-2 flex-1">{children}</div>
2831

2932
return (
3033
<div className="flex gap-3">
3134
<div className="relative flex flex-col items-center">
32-
<div className={cn('mt-[10px]', role === 'assistant' && 'pl-4')}>
33-
{role === 'user' && (
35+
<div className={cn('mt-[10px]', role === 'assistant' && 'mt-4')}>
36+
{role === 'user' ? (
3437
<UserCircle2 size={20} className="text-muted-foreground" />
38+
) : (
39+
<IconLogo className="size-5" />
3540
)}
3641
</div>
3742
</div>
3843

3944
{isCollapsible ? (
40-
<div className="flex-1 border border-border/50 rounded-2xl p-4">
45+
<div
46+
className={cn(
47+
'flex-1 rounded-2xl p-4',
48+
showBorder && 'border border-border/50'
49+
)}
50+
>
4151
<Collapsible
4252
open={isOpen}
4353
onOpenChange={onOpenChange}
@@ -50,7 +60,7 @@ export function CollapsibleMessage({
5060
</div>
5161
</CollapsibleTrigger>
5262
<CollapsibleContent className="data-[state=closed]:animate-collapse-up data-[state=open]:animate-collapse-down">
53-
<Separator className="my-4" />
63+
<Separator className="my-4 border-border/50" />
5464
{content}
5565
</CollapsibleContent>
5666
</Collapsible>

components/related-questions.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ArrowRight, Repeat2 } from 'lucide-react'
66
import { Skeleton } from './ui/skeleton'
77
import { JSONValue } from 'ai'
88
import { CollapsibleMessage } from './collapsible-message'
9+
import { useChat } from 'ai/react'
910

1011
export interface RelatedQuestionsProps {
1112
annotations: JSONValue[]
@@ -19,7 +20,6 @@ interface RelatedQuestionsAnnotation extends Record<string, JSONValue> {
1920
data: {
2021
items: Array<{ query: string }>
2122
}
22-
status: 'loading' | 'done'
2323
}
2424

2525
export const RelatedQuestions: React.FC<RelatedQuestionsProps> = ({
@@ -28,18 +28,17 @@ export const RelatedQuestions: React.FC<RelatedQuestionsProps> = ({
2828
isOpen,
2929
onOpenChange
3030
}) => {
31+
const { isLoading } = useChat({
32+
id: 'chat'
33+
})
34+
3135
if (!annotations) {
3236
return null
3337
}
3438

35-
const lastRelatedQuestionsAnnotation = annotations.find(
36-
(a): a is RelatedQuestionsAnnotation =>
37-
a !== null &&
38-
typeof a === 'object' &&
39-
'type' in a &&
40-
a.type === 'related-questions' &&
41-
a.status === 'done'
42-
)
39+
const lastRelatedQuestionsAnnotation = annotations[
40+
annotations.length - 1
41+
] as RelatedQuestionsAnnotation
4342

4443
const header = (
4544
<div className="flex items-center gap-1">
@@ -49,7 +48,11 @@ export const RelatedQuestions: React.FC<RelatedQuestionsProps> = ({
4948
)
5049

5150
const relatedQuestions = lastRelatedQuestionsAnnotation?.data
52-
if (!relatedQuestions) {
51+
if (!relatedQuestions && !isLoading) {
52+
return null
53+
}
54+
55+
if (!relatedQuestions || isLoading) {
5356
return (
5457
<CollapsibleMessage
5558
role="assistant"

components/search-section.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Section, ToolArgsSection } from './section'
77
import type { SearchResults as TypeSearchResults } from '@/lib/types'
88
import { ToolInvocation } from 'ai'
99
import { CollapsibleMessage } from './collapsible-message'
10+
import { useChat } from 'ai/react'
1011

1112
interface SearchSectionProps {
1213
tool: ToolInvocation
@@ -19,7 +20,10 @@ export function SearchSection({
1920
isOpen,
2021
onOpenChange
2122
}: SearchSectionProps) {
22-
const isLoading = tool.state === 'call'
23+
const { isLoading } = useChat({
24+
id: 'chat'
25+
})
26+
const isToolLoading = tool.state === 'call'
2327
const searchResults: TypeSearchResults =
2428
tool.state === 'result' ? tool.result : undefined
2529
const query = tool.args.query as string | undefined
@@ -50,13 +54,13 @@ export function SearchSection({
5054
/>
5155
</Section>
5256
)}
53-
{!isLoading && searchResults.results ? (
57+
{isLoading && isToolLoading ? (
58+
<DefaultSkeleton />
59+
) : searchResults?.results ? (
5460
<Section title="Sources">
5561
<SearchResults results={searchResults.results} />
5662
</Section>
57-
) : (
58-
<DefaultSkeleton />
59-
)}
63+
) : null}
6064
</CollapsibleMessage>
6165
)
6266
}

lib/types/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Message } from 'ai'
1+
import { CoreMessage, JSONValue, Message } from 'ai'
22

33
export type SearchResults = {
44
images: SearchResultImage[]
@@ -63,10 +63,16 @@ export interface Chat extends Record<string, any> {
6363
createdAt: Date
6464
userId: string
6565
path: string
66-
messages: Message[] // Note: Changed from AIMessage to Message
66+
messages: ExtendedCoreMessage[] // Note: Changed from AIMessage to ExtendedCoreMessage
6767
sharePath?: string
6868
}
6969

70+
// ExtendedCoreMessage for saveing annotations
71+
export type ExtendedCoreMessage = Omit<CoreMessage, 'role' | 'content'> & {
72+
role: CoreMessage['role'] | 'data'
73+
content: CoreMessage['content'] | JSONValue
74+
}
75+
7076
export type AIMessage = {
7177
role: 'user' | 'assistant' | 'system' | 'function' | 'data' | 'tool'
7278
content: string

0 commit comments

Comments
 (0)