wuyiqunLu commited on
Commit
bc1cf4e
1 Parent(s): 6f242f9

feat: add responseBody field and serving data to hooks (#93)

Browse files

New chat loaded properly and refresh still loaded:
<img width="1477" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/132986242/5fa60f22-e609-49e4-9a80-5ffde16bce8c">

Previous chat can still be loaded properly:
<img width="1500" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/132986242/e526e549-711c-433d-a70f-459c8af7bb9b">

app/api/vision-agent/route.ts CHANGED
@@ -1,16 +1,17 @@
1
- import { JSONValue, StreamingTextResponse, experimental_StreamData } from 'ai';
2
 
3
  // import { auth } from '@/auth';
4
- import { MessageUI, SignedPayload } from '@/lib/types';
5
 
6
  import { logger, withLogging } from '@/lib/logger';
7
  import { getPresignedUrl } from '@/lib/aws';
 
8
 
9
  // export const runtime = 'edge';
10
  export const dynamic = 'force-dynamic';
11
  export const maxDuration = 300; // This function can run for a maximum of 5 minutes
12
  const TIMEOUT_MILI_SECONDS = 2 * 60 * 1000;
13
- const FINAL_TIMEOUT_ERROR = {
14
  type: 'final_error',
15
  status: 'failed',
16
  payload: {
@@ -53,20 +54,25 @@ const uploadBase64 = async (
53
  };
54
 
55
  const modifyCodePayload = async (
56
- msg: Record<string, any>,
57
  messageId: string,
58
  chatId: string,
59
  user: string,
60
- ) => {
61
  if (
62
- msg.type !== 'final_code' &&
63
- (msg.type !== 'code' ||
64
- msg.status === 'started' ||
65
- msg.status === 'running')
 
66
  ) {
67
  return msg;
68
  }
69
- const result = JSON.parse(msg.payload.result) as PrismaJson.StructuredResult;
 
 
 
 
70
  if (msg.type === 'code') {
71
  if (result && result.results) {
72
  msg.payload.result = {
@@ -105,17 +111,18 @@ export const POST = withLogging(
105
  session,
106
  json: {
107
  apiMessages: string;
108
- id: string;
 
109
  mediaUrl: string;
110
  },
111
  request,
112
  ) => {
113
- const { apiMessages, mediaUrl } = json;
114
  const messages: MessageUI[] = JSON.parse(apiMessages);
115
  const user = session?.user?.email ?? 'anonymous';
116
 
117
  const formData = new FormData();
118
- formData.append('input', JSON.stringify(messages));
119
  formData.append('image', mediaUrl);
120
 
121
  const agentHost = process.env.LND_TIER
@@ -171,102 +178,147 @@ export const POST = withLogging(
171
  let maxChunkSize = 0;
172
  let buffer = '';
173
  let time = Date.now();
174
- let done = false;
175
  const stream = new ReadableStream({
176
  async start(controller) {
177
- // const parser = createParser(streamParser);
178
- for await (const chunk of fetchResponse.body as any) {
179
- const data = decoder.decode(chunk);
180
- buffer += data;
181
- maxChunkSize = Math.max(data.length, maxChunkSize);
182
- const lines = buffer
183
- .split('\n')
184
- .filter(line => line.trim().length > 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  if (lines.length === 0) {
186
  if (Date.now() - time > TIMEOUT_MILI_SECONDS) {
187
- // logger.info(
188
- // session,
189
- // {
190
- // message: 'Agent timed out',
191
- // },
192
- // request,
193
- // '__Agent_timeout__',
194
- // );
195
  controller.enqueue(
196
- encoder.encode(JSON.stringify(FINAL_TIMEOUT_ERROR) + '\n'),
 
197
  );
198
- controller.close();
199
- return;
200
  }
201
  } else {
202
  time = Date.now();
203
  }
204
  buffer = lines.pop() ?? ''; // Save the last incomplete line back to the buffer
205
- const parseLine = async (
206
- line: string,
207
- ignoreParsingError = false,
208
- ) => {
209
- const handleError = (e: Error) => {
210
- console.error(e);
211
- logger.error(
212
- session,
213
- {
214
- line,
215
- message: e.message,
216
  },
217
- request,
 
 
 
 
 
218
  );
219
- controller.error(e);
220
- };
221
- let msg = null;
222
- try {
223
- msg = JSON.parse(line);
224
- if (msg.type === 'final_code' || msg.type === 'final_error') {
225
- done = true;
 
226
  }
227
- } catch (e) {
228
- if (ignoreParsingError) return;
229
- handleError(e as Error);
230
- }
231
- if (!msg) return;
232
- try {
233
- const modifiedMsg = await modifyCodePayload(
234
- {
235
- ...msg,
236
- timestamp: new Date(),
237
- },
238
- messages[messages.length - 1].id,
239
- json.id,
240
- user,
241
- );
242
- return modifiedMsg;
243
- } catch (e) {
244
- handleError(e as Error);
245
  }
246
- };
247
- for (let line of lines) {
248
- const parsedMsg = await parseLine(line);
249
- controller.enqueue(
250
- parsedMsg ? encoder.encode(JSON.stringify(parsedMsg) + '\n') : '',
251
- );
252
  }
253
  if (buffer) {
254
- const parsedBuffer = await parseLine(buffer, true);
255
- if (parsedBuffer) {
 
 
 
 
 
 
 
 
 
 
 
256
  buffer = '';
 
257
  controller.enqueue(
258
- encoder.encode(JSON.stringify(parsedBuffer) + '\n'),
259
  );
 
 
 
 
 
 
 
 
 
260
  } else {
261
- controller.enqueue('');
262
  }
263
  }
 
 
 
 
 
 
 
 
 
 
 
 
264
  if (done) {
 
 
 
 
 
 
 
 
 
 
265
  logger.info(
266
  session,
267
  {
268
- message: 'Streaming finished',
269
  maxChunkSize,
 
 
270
  },
271
  request,
272
  '__AGENT_DONE',
 
1
+ import { StreamingTextResponse } from 'ai';
2
 
3
  // import { auth } from '@/auth';
4
+ import { MessageUI } from '@/lib/types';
5
 
6
  import { logger, withLogging } from '@/lib/logger';
7
  import { getPresignedUrl } from '@/lib/aws';
8
+ import { dbPostUpdateMessageResponse } from '@/lib/db/functions';
9
 
10
  // export const runtime = 'edge';
11
  export const dynamic = 'force-dynamic';
12
  export const maxDuration = 300; // This function can run for a maximum of 5 minutes
13
  const TIMEOUT_MILI_SECONDS = 2 * 60 * 1000;
14
+ const FINAL_TIMEOUT_ERROR: PrismaJson.FinalErrorBody = {
15
  type: 'final_error',
16
  status: 'failed',
17
  payload: {
 
54
  };
55
 
56
  const modifyCodePayload = async (
57
+ msg: PrismaJson.MessageBody,
58
  messageId: string,
59
  chatId: string,
60
  user: string,
61
+ ): Promise<PrismaJson.MessageBody> => {
62
  if (
63
+ (msg.type !== 'final_code' &&
64
+ (msg.type !== 'code' ||
65
+ msg.status === 'started' ||
66
+ msg.status === 'running')) ||
67
+ !msg.payload?.result
68
  ) {
69
  return msg;
70
  }
71
+ const result = (
72
+ typeof msg.payload.result === 'string'
73
+ ? JSON.parse(msg.payload.result)
74
+ : msg.payload.result
75
+ ) as PrismaJson.StructuredResult;
76
  if (msg.type === 'code') {
77
  if (result && result.results) {
78
  msg.payload.result = {
 
111
  session,
112
  json: {
113
  apiMessages: string;
114
+ chatId: string;
115
+ messageId: string;
116
  mediaUrl: string;
117
  },
118
  request,
119
  ) => {
120
+ const { apiMessages, mediaUrl, chatId, messageId } = json;
121
  const messages: MessageUI[] = JSON.parse(apiMessages);
122
  const user = session?.user?.email ?? 'anonymous';
123
 
124
  const formData = new FormData();
125
+ formData.append('input', apiMessages);
126
  formData.append('image', mediaUrl);
127
 
128
  const agentHost = process.env.LND_TIER
 
178
  let maxChunkSize = 0;
179
  let buffer = '';
180
  let time = Date.now();
181
+ const results: PrismaJson.MessageBody[] = [];
182
  const stream = new ReadableStream({
183
  async start(controller) {
184
+ const parseLine = async (
185
+ line: string,
186
+ ignoreParsingError = false,
187
+ ): Promise<{ data?: PrismaJson.MessageBody; error?: Error }> => {
188
+ let msg = null;
189
+ try {
190
+ msg = JSON.parse(line);
191
+ } catch (e) {
192
+ if (ignoreParsingError) return {};
193
+ else {
194
+ return { error: e as Error };
195
+ }
196
+ }
197
+ if (!msg) return {};
198
+ try {
199
+ const modifiedMsg = await modifyCodePayload(
200
+ {
201
+ ...msg,
202
+ timestamp: new Date(),
203
+ },
204
+ messageId,
205
+ chatId,
206
+ user,
207
+ );
208
+ return { data: modifiedMsg };
209
+ } catch (e) {
210
+ return { error: e as Error };
211
+ }
212
+ };
213
+
214
+ const processChunk = async (lines: string[]) => {
215
  if (lines.length === 0) {
216
  if (Date.now() - time > TIMEOUT_MILI_SECONDS) {
217
+ results.push(FINAL_TIMEOUT_ERROR);
218
+ // https://github.com/vercel/ai/blob/f7002ad2c5aa58ce6ed83e8d31fe22f71ebdb7d7/packages/ui-utils/src/stream-parts.ts#L62
 
 
 
 
 
 
219
  controller.enqueue(
220
+ '2:' +
221
+ encoder.encode(JSON.stringify(FINAL_TIMEOUT_ERROR) + '\n'),
222
  );
223
+ return { done: true, reason: 'timeout' };
 
224
  }
225
  } else {
226
  time = Date.now();
227
  }
228
  buffer = lines.pop() ?? ''; // Save the last incomplete line back to the buffer
229
+ for (let line of lines) {
230
+ const { data: parsedMsg, error } = await parseLine(line);
231
+ if (error) {
232
+ results.push({
233
+ type: 'final_error',
234
+ status: 'failed',
235
+ payload: {
236
+ name: 'ParsingError',
237
+ value: line,
238
+ traceback_raw: [],
 
239
  },
240
+ });
241
+ return { done: true, reason: 'api_error', error };
242
+ } else if (parsedMsg) {
243
+ results.push(parsedMsg);
244
+ controller.enqueue(
245
+ encoder.encode('2:' + JSON.stringify([parsedMsg]) + '\n'),
246
  );
247
+ if (parsedMsg.type === 'final_code') {
248
+ return { done: true, reason: 'agent_concluded' };
249
+ } else if (parsedMsg.type === 'final_error') {
250
+ return {
251
+ done: true,
252
+ reason: 'agent_error',
253
+ error: parsedMsg.payload,
254
+ };
255
  }
256
+ } else {
257
+ controller.enqueue(encoder.encode(''));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  }
 
 
 
 
 
 
259
  }
260
  if (buffer) {
261
+ const { data: parsedBuffer, error } = await parseLine(buffer, true);
262
+ if (error) {
263
+ results.push({
264
+ type: 'final_error',
265
+ status: 'failed',
266
+ payload: {
267
+ name: 'ParsingError',
268
+ value: buffer,
269
+ traceback_raw: [],
270
+ },
271
+ });
272
+ return { done: true, reason: 'api_error', error };
273
+ } else if (parsedBuffer) {
274
  buffer = '';
275
+ results.push(parsedBuffer);
276
  controller.enqueue(
277
+ encoder.encode('2:' + JSON.stringify([parsedBuffer]) + '\n'),
278
  );
279
+ if (parsedBuffer.type === 'final_code') {
280
+ return { done: true, reason: 'agent_concluded' };
281
+ } else if (parsedBuffer.type === 'final_error') {
282
+ return {
283
+ done: true,
284
+ reason: 'agent_error',
285
+ error: parsedBuffer.payload,
286
+ };
287
+ }
288
  } else {
289
+ controller.enqueue(encoder.encode(''));
290
  }
291
  }
292
+ return { done: false };
293
+ };
294
+
295
+ // const parser = createParser(streamParser);
296
+ for await (const chunk of fetchResponse.body as any) {
297
+ const data = decoder.decode(chunk);
298
+ buffer += data;
299
+ maxChunkSize = Math.max(data.length, maxChunkSize);
300
+ const lines = buffer
301
+ .split('\n')
302
+ .filter(line => line.trim().length > 0);
303
+ const { done, reason, error } = await processChunk(lines);
304
  if (done) {
305
+ const processMsgs = results.filter(
306
+ res => res.type !== 'final_code',
307
+ ) as PrismaJson.AgentResponseBodies;
308
+ await dbPostUpdateMessageResponse(messageId, {
309
+ response: processMsgs.map(res => JSON.stringify(res)).join('\n'),
310
+ result: results.find(
311
+ res => res.type === 'final_code',
312
+ ) as PrismaJson.FinalCodeBody,
313
+ responseBody: processMsgs,
314
+ });
315
  logger.info(
316
  session,
317
  {
318
+ message: 'Streaming ended',
319
  maxChunkSize,
320
+ reason,
321
+ error,
322
  },
323
  request,
324
  '__AGENT_DONE',
components/CodeResultDisplay.tsx CHANGED
@@ -23,7 +23,7 @@ import {
23
  export interface CodeResultDisplayProps {}
24
 
25
  const CodeResultDisplay: React.FC<{
26
- codeResult: PrismaJson.FinalChatResult['payload'];
27
  }> = ({ codeResult }) => {
28
  const { code, test, result } = codeResult;
29
  const getDetail = () => {
 
23
  export interface CodeResultDisplayProps {}
24
 
25
  const CodeResultDisplay: React.FC<{
26
+ codeResult: PrismaJson.FinalCodeBody['payload'];
27
  }> = ({ codeResult }) => {
28
  const { code, test, result } = codeResult;
29
  const getDetail = () => {
components/chat/ChatList.tsx CHANGED
@@ -4,9 +4,8 @@
4
  import Composer from '@/components/chat/Composer';
5
  import useVisionAgent from '@/lib/hooks/useVisionAgent';
6
  import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
7
- import { Session } from 'next-auth';
8
  import { useEffect } from 'react';
9
- import { ChatWithMessages, MessageUserInput } from '@/lib/types';
10
  import { ChatMessage } from './ChatMessage';
11
  import { Button } from '../ui/Button';
12
  import { cn } from '@/lib/utils';
@@ -25,16 +24,11 @@ export const SCROLL_BOTTOM = 120;
25
 
26
  const ChatList: React.FC<ChatListProps> = ({ chat, userId }) => {
27
  const { id, messages: dbMessages, userId: chatUserId } = chat;
28
- const { messages, append, isLoading } = useVisionAgent(chat);
29
 
30
  // Only login and chat owner can compose
31
  const canCompose = !chatUserId || userId === chatUserId;
32
 
33
- const lastAssistantMessage = messages.length
34
- ? messages[messages.length - 1]?.role === 'assistant'
35
- ? messages[messages.length - 1]?.content
36
- : undefined
37
- : undefined;
38
  const lastDbMessage = dbMessages[dbMessages.length - 1];
39
  const setMessageId = useSetAtom(selectedMessageId);
40
 
@@ -63,11 +57,7 @@ const ChatList: React.FC<ChatListProps> = ({ chat, userId }) => {
63
  key={message.id}
64
  message={message}
65
  loading={isLastMessage && isLoading}
66
- wipAssistantMessage={
67
- lastAssistantMessage && isLastMessage
68
- ? lastAssistantMessage
69
- : undefined
70
- }
71
  />
72
  );
73
  })}
 
4
  import Composer from '@/components/chat/Composer';
5
  import useVisionAgent from '@/lib/hooks/useVisionAgent';
6
  import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
 
7
  import { useEffect } from 'react';
8
+ import { ChatWithMessages } from '@/lib/types';
9
  import { ChatMessage } from './ChatMessage';
10
  import { Button } from '../ui/Button';
11
  import { cn } from '@/lib/utils';
 
24
 
25
  const ChatList: React.FC<ChatListProps> = ({ chat, userId }) => {
26
  const { id, messages: dbMessages, userId: chatUserId } = chat;
27
+ const { append, isLoading, data } = useVisionAgent(chat);
28
 
29
  // Only login and chat owner can compose
30
  const canCompose = !chatUserId || userId === chatUserId;
31
 
 
 
 
 
 
32
  const lastDbMessage = dbMessages[dbMessages.length - 1];
33
  const setMessageId = useSetAtom(selectedMessageId);
34
 
 
57
  key={message.id}
58
  message={message}
59
  loading={isLastMessage && isLoading}
60
+ wipAssistantMessage={data}
 
 
 
 
61
  />
62
  );
63
  })}
components/chat/ChatMessage.tsx CHANGED
@@ -33,7 +33,7 @@ import { cn } from '@/lib/utils';
33
  export interface ChatMessageProps {
34
  message: Message;
35
  loading?: boolean;
36
- wipAssistantMessage?: string;
37
  }
38
 
39
  export const ChatMessage: React.FC<ChatMessageProps> = ({
@@ -42,10 +42,15 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
42
  loading,
43
  }) => {
44
  const [messageId, setMessageId] = useAtom(selectedMessageId);
45
- const { id, mediaUrl, prompt, response, result } = message;
46
  const [formattedSections, finalResult, finalError] = useMemo(
47
- () => formatStreamLogs(response ?? wipAssistantMessage),
48
- [response, wipAssistantMessage],
 
 
 
 
 
49
  );
50
  // prioritize the result from the message over the WIP message
51
  const codeResult = result?.payload ?? finalResult;
@@ -266,7 +271,7 @@ const ChunkPayloadAction: React.FC<{
266
  </DialogContent>
267
  </Dialog>
268
  );
269
- } else if ((payload as PrismaJson.FinalChatResult['payload']).code) {
270
  return (
271
  <Dialog>
272
  <DialogTrigger asChild>
@@ -276,7 +281,7 @@ const ChunkPayloadAction: React.FC<{
276
  </DialogTrigger>
277
  <DialogContent className="max-w-5xl">
278
  <CodeResultDisplay
279
- codeResult={payload as PrismaJson.FinalChatResult['payload']}
280
  />
281
  </DialogContent>
282
  </Dialog>
 
33
  export interface ChatMessageProps {
34
  message: Message;
35
  loading?: boolean;
36
+ wipAssistantMessage?: PrismaJson.MessageBody[];
37
  }
38
 
39
  export const ChatMessage: React.FC<ChatMessageProps> = ({
 
42
  loading,
43
  }) => {
44
  const [messageId, setMessageId] = useAtom(selectedMessageId);
45
+ const { id, mediaUrl, prompt, response, result, responseBody } = message;
46
  const [formattedSections, finalResult, finalError] = useMemo(
47
+ () =>
48
+ formatStreamLogs(
49
+ responseBody ??
50
+ wipAssistantMessage ??
51
+ (response ? JSON.parse(response) : ''),
52
+ ),
53
+ [response, wipAssistantMessage, result, responseBody],
54
  );
55
  // prioritize the result from the message over the WIP message
56
  const codeResult = result?.payload ?? finalResult;
 
271
  </DialogContent>
272
  </Dialog>
273
  );
274
+ } else if ((payload as PrismaJson.FinalCodeBody['payload']).code) {
275
  return (
276
  <Dialog>
277
  <DialogTrigger asChild>
 
281
  </DialogTrigger>
282
  <DialogContent className="max-w-5xl">
283
  <CodeResultDisplay
284
+ codeResult={payload as PrismaJson.FinalCodeBody['payload']}
285
  />
286
  </DialogContent>
287
  </Dialog>
lib/db/functions.ts CHANGED
@@ -188,6 +188,7 @@ export async function dbPostUpdateMessageResponse(
188
  data: {
189
  response: messageResponse.response,
190
  result: messageResponse.result ?? undefined,
 
191
  },
192
  where: {
193
  id: messageId,
 
188
  data: {
189
  response: messageResponse.response,
190
  result: messageResponse.result ?? undefined,
191
+ responseBody: messageResponse.responseBody,
192
  },
193
  where: {
194
  id: messageId,
lib/db/prisma.ts CHANGED
@@ -5,6 +5,12 @@ declare global {
5
  namespace PrismaJson {
6
  // you can use classes, interfaces, types, etc.
7
 
 
 
 
 
 
 
8
  type StructuredResult = {
9
  logs: {
10
  stderr: string[];
@@ -16,25 +22,50 @@ declare global {
16
  text: string;
17
  is_main_result: boolean;
18
  }>;
19
- error: {
20
- name: string;
21
- value: string;
22
- traceback_raw: string[];
23
- };
24
  };
25
 
26
- type FinalChatResult = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  type: 'final_code';
28
  status: 'completed' | 'failed';
29
  payload: {
30
  code: string;
31
  test: string;
32
- // Change introduces https://github.com/landing-ai/vision-agent-ui/pull/86
33
- // 1. Backward compatibility, it could be stringified StructuredResult
34
- // 2. result not modified in stream server, could still be stringified StructuredResult
35
- result: string | StructuredResult;
36
  };
37
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  }
39
  }
40
 
 
5
  namespace PrismaJson {
6
  // you can use classes, interfaces, types, etc.
7
 
8
+ interface StructuredError {
9
+ name: string;
10
+ value: string;
11
+ traceback_raw: string[];
12
+ }
13
+
14
  type StructuredResult = {
15
  logs: {
16
  stderr: string[];
 
22
  text: string;
23
  is_main_result: boolean;
24
  }>;
25
+ error?: StructuredError;
 
 
 
 
26
  };
27
 
28
+ interface PlanAndToolsBody {
29
+ type: 'plans' | 'tools';
30
+ status: 'started' | 'completed';
31
+ payload?: Array<Record<string, string>>;
32
+ }
33
+
34
+ interface CodeBody {
35
+ type: 'code';
36
+ status: 'completed' | 'failed' | 'started' | 'running';
37
+ payload?: {
38
+ code: string;
39
+ test: string;
40
+ result?: StructuredResult | string;
41
+ };
42
+ }
43
+
44
+ interface FinalCodeBody {
45
  type: 'final_code';
46
  status: 'completed' | 'failed';
47
  payload: {
48
  code: string;
49
  test: string;
50
+ result: StructuredResult | string;
 
 
 
51
  };
52
+ }
53
+
54
+ interface FinalErrorBody {
55
+ type: 'final_error';
56
+ status: 'failed';
57
+ payload: StructuredError;
58
+ }
59
+
60
+ type MessageBody =
61
+ | PlanAndToolsBody
62
+ | CodeBody
63
+ | FinalErrorBody
64
+ | FinalCodeBody;
65
+
66
+ type AgentResponseBodies = Array<
67
+ PlanAndToolsBody | CodeBody | FinalErrorBody
68
+ >;
69
  }
70
  }
71
 
lib/hooks/useVisionAgent.ts CHANGED
@@ -1,12 +1,8 @@
1
  import { useChat } from 'ai/react';
2
  import { toast } from 'react-hot-toast';
3
- import { useEffect, useRef, useState } from 'react';
4
- import { ChatWithMessages, MessageUI, MessageUserInput } from '../types';
5
- import { dbPostUpdateMessageResponse } from '../db/functions';
6
- import {
7
- convertAssistantUIMessageToDBMessageResponse,
8
- convertDBMessageToAPIMessage,
9
- } from '../utils/message';
10
  import { useSetAtom } from 'jotai';
11
  import { selectedMessageId } from '@/state/chat';
12
  import { Message } from '@prisma/client';
@@ -20,24 +16,20 @@ const useVisionAgent = (chat: ChatWithMessages) => {
20
  const currMediaUrl = useRef<string>(mediaUrl);
21
  const currMessageId = useRef<string>(latestDbMessage?.id);
22
 
23
- const { messages, append, isLoading, reload } = useChat({
24
  api: '/api/vision-agent',
25
- streamMode: 'text',
26
  onResponse(response) {
27
  if (response.status !== 200) {
28
  toast.error(response.statusText);
29
  }
30
  },
31
- onFinish: async message => {
32
- await dbPostUpdateMessageResponse(
33
- currMessageId.current,
34
- convertAssistantUIMessageToDBMessageResponse(message),
35
- );
36
  setMessageId(currMessageId.current);
37
  },
38
  body: {
39
  mediaUrl: currMediaUrl.current,
40
- id,
 
41
  // for some reason, the messages has to be stringified to be sent to the API
42
  apiMessages: JSON.stringify(convertDBMessageToAPIMessage(dbMessages)),
43
  },
@@ -56,10 +48,9 @@ const useVisionAgent = (chat: ChatWithMessages) => {
56
  once.current = false;
57
  reload();
58
  }
59
- }, [isLoading, latestDbMessage.result, messages, reload]);
60
 
61
  return {
62
- messages: messages as MessageUI[],
63
  append: (message: Message) => {
64
  currMediaUrl.current = message.mediaUrl;
65
  currMessageId.current = message.id;
@@ -69,6 +60,7 @@ const useVisionAgent = (chat: ChatWithMessages) => {
69
  content: message.prompt,
70
  });
71
  },
 
72
  reload,
73
  isLoading,
74
  };
 
1
  import { useChat } from 'ai/react';
2
  import { toast } from 'react-hot-toast';
3
+ import { useEffect, useRef } from 'react';
4
+ import { ChatWithMessages, MessageUI } from '../types';
5
+ import { convertDBMessageToAPIMessage } from '../utils/message';
 
 
 
 
6
  import { useSetAtom } from 'jotai';
7
  import { selectedMessageId } from '@/state/chat';
8
  import { Message } from '@prisma/client';
 
16
  const currMediaUrl = useRef<string>(mediaUrl);
17
  const currMessageId = useRef<string>(latestDbMessage?.id);
18
 
19
+ const { append, isLoading, data, reload } = useChat({
20
  api: '/api/vision-agent',
 
21
  onResponse(response) {
22
  if (response.status !== 200) {
23
  toast.error(response.statusText);
24
  }
25
  },
26
+ onFinish: message => {
 
 
 
 
27
  setMessageId(currMessageId.current);
28
  },
29
  body: {
30
  mediaUrl: currMediaUrl.current,
31
+ chatId: id,
32
+ messageId: currMessageId.current,
33
  // for some reason, the messages has to be stringified to be sent to the API
34
  apiMessages: JSON.stringify(convertDBMessageToAPIMessage(dbMessages)),
35
  },
 
48
  once.current = false;
49
  reload();
50
  }
51
+ }, [isLoading, latestDbMessage.result, reload]);
52
 
53
  return {
 
54
  append: (message: Message) => {
55
  currMediaUrl.current = message.mediaUrl;
56
  currMessageId.current = message.id;
 
60
  content: message.prompt,
61
  });
62
  },
63
+ data: data as unknown as PrismaJson.MessageBody[],
64
  reload,
65
  isLoading,
66
  };
lib/types.ts CHANGED
@@ -4,9 +4,11 @@ import { type Message as MessageAI } from 'ai';
4
  export type ChatWithMessages = Chat & { messages: Message[] };
5
 
6
  export type MessageUserInput = Pick<Message, 'prompt' | 'mediaUrl'>;
7
- export type MessageAssistantResponse = Partial<
8
- Pick<Message, 'response' | 'result'>
9
- >;
 
 
10
 
11
  export type MessageUI = Pick<MessageAI, 'role' | 'content' | 'id'>;
12
 
 
4
  export type ChatWithMessages = Chat & { messages: Message[] };
5
 
6
  export type MessageUserInput = Pick<Message, 'prompt' | 'mediaUrl'>;
7
+ export type MessageAssistantResponse = {
8
+ result?: PrismaJson.FinalCodeBody;
9
+ response: string;
10
+ responseBody: PrismaJson.AgentResponseBodies;
11
+ };
12
 
13
  export type MessageUI = Pick<MessageAI, 'role' | 'content' | 'id'>;
14
 
lib/utils/content.ts CHANGED
@@ -15,11 +15,12 @@ export type ChunkBody = {
15
  timestamp?: string;
16
  payload:
17
  | Array<Record<string, string>> // PlansBody | ToolsBody
18
- | PrismaJson.FinalChatResult['payload'] // CodeBody & FinalCodeBody
19
- | PrismaJson.StructuredResult['error']; // ErrorBody
20
  };
21
 
22
- export type WIPChunkBodyGroup = ChunkBody & {
 
23
  duration?: number;
24
  };
25
 
@@ -30,37 +31,16 @@ export type WIPChunkBodyGroup = ChunkBody & {
30
  * @returns An array of grouped sections and an optional final code result.
31
  */
32
  export const formatStreamLogs = (
33
- content: string | null | undefined,
34
  ): [
35
  WIPChunkBodyGroup[],
36
- PrismaJson.FinalChatResult['payload']?,
37
- PrismaJson.StructuredResult['error']?,
38
  ] => {
39
- if (!content) return [[], undefined];
40
- const streamLogs = content.split('\n').filter(log => !!log);
41
-
42
- const buffer = streamLogs.pop();
43
- const parsedStreamLogs: ChunkBody[] = [];
44
- try {
45
- streamLogs.forEach(streamLog =>
46
- parsedStreamLogs.push(JSON.parse(streamLog)),
47
- );
48
- } catch {
49
- toast.error('Error parsing stream logs');
50
- return [[], undefined];
51
- }
52
-
53
- if (buffer) {
54
- try {
55
- const lastLog = JSON.parse(buffer);
56
- parsedStreamLogs.push(lastLog);
57
- } catch {
58
- console.log(buffer);
59
- }
60
- }
61
 
62
  // Merge consecutive logs of the same type to the latest status
63
- const groupedSections = parsedStreamLogs.reduce((acc, curr) => {
64
  const lastGroup = acc[acc.length - 1];
65
  if (
66
  acc.length > 0 &&
@@ -81,13 +61,13 @@ export const formatStreamLogs = (
81
  acc.push(curr);
82
  }
83
  return acc;
84
- }, [] as WIPChunkBodyGroup[]);
85
 
86
  return [
87
  groupedSections.filter(section => WIPLogTypes.includes(section.type)),
88
  groupedSections.find(section => section.type === 'final_code')
89
- ?.payload as PrismaJson.FinalChatResult['payload'],
90
  groupedSections.find(section => section.type === 'final_error')
91
- ?.payload as PrismaJson.StructuredResult['error'],
92
  ];
93
  };
 
15
  timestamp?: string;
16
  payload:
17
  | Array<Record<string, string>> // PlansBody | ToolsBody
18
+ | PrismaJson.FinalCodeBody['payload'] // CodeBody & FinalCodeBody
19
+ | PrismaJson.StructuredError; // ErrorBody
20
  };
21
 
22
+ export type WIPChunkBodyGroup = PrismaJson.MessageBody & {
23
+ timestamp?: string;
24
  duration?: number;
25
  };
26
 
 
31
  * @returns An array of grouped sections and an optional final code result.
32
  */
33
  export const formatStreamLogs = (
34
+ content: WIPChunkBodyGroup[] | null,
35
  ): [
36
  WIPChunkBodyGroup[],
37
+ PrismaJson.FinalCodeBody['payload']?,
38
+ PrismaJson.StructuredError?,
39
  ] => {
40
+ if (!content) return [[], undefined, undefined];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  // Merge consecutive logs of the same type to the latest status
43
+ const groupedSections = content.reduce((acc: WIPChunkBodyGroup[], curr) => {
44
  const lastGroup = acc[acc.length - 1];
45
  if (
46
  acc.length > 0 &&
 
61
  acc.push(curr);
62
  }
63
  return acc;
64
+ }, []);
65
 
66
  return [
67
  groupedSections.filter(section => WIPLogTypes.includes(section.type)),
68
  groupedSections.find(section => section.type === 'final_code')
69
+ ?.payload as PrismaJson.FinalCodeBody['payload'],
70
  groupedSections.find(section => section.type === 'final_error')
71
+ ?.payload as PrismaJson.StructuredError,
72
  ];
73
  };
lib/utils/message.ts CHANGED
@@ -1,6 +1,5 @@
1
  import { Message } from '@prisma/client';
2
  import { MessageAssistantResponse, MessageUI } from '../types';
3
- import { ChunkBody } from './content';
4
 
5
  /**
6
  * The Message we saved to database consists of a prompt and a response
@@ -28,25 +27,3 @@ export const convertDBMessageToAPIMessage = (
28
  return acc;
29
  }, [] as MessageUI[]);
30
  };
31
-
32
- export const convertAssistantUIMessageToDBMessageResponse = (
33
- message: MessageUI,
34
- ): MessageAssistantResponse => {
35
- let result = null;
36
- const lines = message.content.split('\n');
37
- for (let line of lines) {
38
- try {
39
- const json = JSON.parse(line) as ChunkBody;
40
- if (json.type == 'final_code') {
41
- result = json as PrismaJson.FinalChatResult;
42
- break;
43
- }
44
- } catch (e) {
45
- console.error((e as Error).message);
46
- }
47
- }
48
- return {
49
- response: message.content,
50
- result,
51
- };
52
- };
 
1
  import { Message } from '@prisma/client';
2
  import { MessageAssistantResponse, MessageUI } from '../types';
 
3
 
4
  /**
5
  * The Message we saved to database consists of a prompt and a response
 
27
  return acc;
28
  }, [] as MessageUI[]);
29
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prisma/migrations/20240612024011_add_response_body_in_message/migration.sql ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ -- AlterTable
2
+ ALTER TABLE "message" ADD COLUMN "responseBody" JSONB;
prisma/schema.prisma CHANGED
@@ -47,8 +47,10 @@ model Message {
47
  mediaUrl String
48
  prompt String
49
  response String?
50
- /// [FinalChatResult]
51
  result Json?
 
 
52
  chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
53
  user User? @relation(fields: [userId], references: [id])
54
 
 
47
  mediaUrl String
48
  prompt String
49
  response String?
50
+ /// [FinalCodeBody]
51
  result Json?
52
+ /// [AgentResponseBodies]
53
+ responseBody Json?
54
  chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
55
  user User? @relation(fields: [userId], references: [id])
56