wuyiqunLu commited on
Commit
c7e97b5
1 Parent(s): 7286745

feat: encode uri for s3 url when loading (#104)

Browse files

file name has special characters (%, & etc):
<img width="737" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/132986242/33706c60-f7f5-4f3e-8ace-3d1e6afb77be">
<img width="1524" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/132986242/80edeb03-b10d-4463-bd85-68d1d1d157a8">

normal file name without special characters:
<img width="883" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/132986242/2029ac6b-8605-4849-8661-93b3008acc02">

app/api/vision-agent/route.ts CHANGED
@@ -119,16 +119,16 @@ export const POST = withLogging(
119
 
120
  const formData = new FormData();
121
  formData.append('input', apiMessages);
122
- formData.append('image', mediaUrl);
123
 
124
  const agentHost = process.env.LND_TIER
125
  ? 'http://publicrestapi-app-lndsvc.publicrestapi.svc.cluster.local:5000'
126
  : 'https://api.dev.landing.ai';
127
 
128
  const fetchResponse = await fetch(
129
- `${agentHost}/v1/agent/chat?agent_class=vision_agent&self_reflection=false`,
130
  // `https://api.dev.landing.ai/v1/agent/chat?agent_class=vision_agent&self_reflection=false`,
131
- // `http://localhost:5001/v1/agent/chat?agent_class=vision_agent&self_reflection=false`,
132
  {
133
  method: 'POST',
134
  headers: {
@@ -349,6 +349,20 @@ export const POST = withLogging(
349
  };
350
 
351
  let timeout = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  for await (const chunk of fetchResponse.body as any) {
353
  const data = decoder.decode(chunk);
354
  buffer += data;
 
119
 
120
  const formData = new FormData();
121
  formData.append('input', apiMessages);
122
+ formData.append('image', encodeURI(mediaUrl));
123
 
124
  const agentHost = process.env.LND_TIER
125
  ? 'http://publicrestapi-app-lndsvc.publicrestapi.svc.cluster.local:5000'
126
  : 'https://api.dev.landing.ai';
127
 
128
  const fetchResponse = await fetch(
129
+ // `${agentHost}/v1/agent/chat?agent_class=vision_agent&self_reflection=false`,
130
  // `https://api.dev.landing.ai/v1/agent/chat?agent_class=vision_agent&self_reflection=false`,
131
+ `http://localhost:5001/v1/agent/chat?agent_class=vision_agent&self_reflection=false`,
132
  {
133
  method: 'POST',
134
  headers: {
 
349
  };
350
 
351
  let timeout = null;
352
+ controller.enqueue(
353
+ encoder.encode(
354
+ '2:' +
355
+ JSON.stringify([
356
+ {
357
+ type: 'init',
358
+ payload: {
359
+ messageId,
360
+ },
361
+ },
362
+ ]) +
363
+ '\n',
364
+ ),
365
+ );
366
  for await (const chunk of fetchResponse.body as any) {
367
  const data = decoder.decode(chunk);
368
  buffer += data;
components/ChatInterface.tsx CHANGED
@@ -1,7 +1,7 @@
1
  'use client';
2
 
3
  import { ChatWithMessages } from '@/lib/types';
4
- import React from 'react';
5
  import ChatList from './chat/ChatList';
6
  import { Card } from './ui/Card';
7
  import { useAtom, useAtomValue } from 'jotai';
@@ -14,10 +14,18 @@ export interface ChatInterfaceProps {
14
  }
15
 
16
  const ChatInterface: React.FC<ChatInterfaceProps> = ({ chat, userId }) => {
17
- const messageId = useAtomValue(selectedMessageId);
18
  const messageCodeResult = chat.messages.find(
19
  message => message.id === messageId,
20
  )?.result;
 
 
 
 
 
 
 
 
21
  return (
22
  <div className="relative flex overflow-hidden space-x-4 size-full">
23
  <div
 
1
  'use client';
2
 
3
  import { ChatWithMessages } from '@/lib/types';
4
+ import React, { useEffect } from 'react';
5
  import ChatList from './chat/ChatList';
6
  import { Card } from './ui/Card';
7
  import { useAtom, useAtomValue } from 'jotai';
 
14
  }
15
 
16
  const ChatInterface: React.FC<ChatInterfaceProps> = ({ chat, userId }) => {
17
+ const [messageId, setMessageId] = useAtom(selectedMessageId);
18
  const messageCodeResult = chat.messages.find(
19
  message => message.id === messageId,
20
  )?.result;
21
+
22
+ useEffect(() => {
23
+ if (messageId) return;
24
+ const lastMessageWithResult =
25
+ chat.messages.findLast(message => !!message.result) ??
26
+ chat.messages[chat.messages.length - 1];
27
+ setMessageId(lastMessageWithResult?.id);
28
+ }, [messageId]);
29
  return (
30
  <div className="relative flex overflow-hidden space-x-4 size-full">
31
  <div
components/chat/ChatList.tsx CHANGED
@@ -12,8 +12,6 @@ import { cn } from '@/lib/utils';
12
  import { IconArrowDown } from '../ui/Icons';
13
  import { dbPostCreateMessage } from '@/lib/db/functions';
14
  import { Card } from '../ui/Card';
15
- import { useSetAtom } from 'jotai';
16
- import { selectedMessageId } from '@/state/chat';
17
 
18
  export interface ChatListProps {
19
  chat: ChatWithMessages;
@@ -24,25 +22,18 @@ export const SCROLL_BOTTOM = 120;
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
-
35
  const { messagesRef, scrollRef, visibilityRef, isVisible, scrollToBottom } =
36
  useScrollAnchor(SCROLL_BOTTOM);
37
 
38
- // Scroll to bottom on init and highlight last message
39
  useEffect(() => {
40
  scrollToBottom();
41
- if (lastDbMessage.result) {
42
- setMessageId(lastDbMessage.id);
43
- }
44
- // eslint-disable-next-line react-hooks/exhaustive-deps
45
- }, []);
46
 
47
  return (
48
  <Card
@@ -57,7 +48,9 @@ const ChatList: React.FC<ChatListProps> = ({ chat, userId }) => {
57
  key={message.id}
58
  message={message}
59
  loading={isLastMessage && isLoading}
60
- wipAssistantMessage={data}
 
 
61
  />
62
  );
63
  })}
@@ -78,8 +71,7 @@ const ChatList: React.FC<ChatListProps> = ({ chat, userId }) => {
78
  prompt: input,
79
  mediaUrl: newMediaUrl,
80
  };
81
- const resp = await dbPostCreateMessage(id, messageInput);
82
- append(resp);
83
  }}
84
  />
85
  </div>
 
12
  import { IconArrowDown } from '../ui/Icons';
13
  import { dbPostCreateMessage } from '@/lib/db/functions';
14
  import { Card } from '../ui/Card';
 
 
15
 
16
  export interface ChatListProps {
17
  chat: ChatWithMessages;
 
22
 
23
  const ChatList: React.FC<ChatListProps> = ({ chat, userId }) => {
24
  const { id, messages: dbMessages, userId: chatUserId } = chat;
25
+ const { isLoading, data } = useVisionAgent(chat);
26
 
27
  // Only login and chat owner can compose
28
  const canCompose = !chatUserId || userId === chatUserId;
29
 
 
 
 
30
  const { messagesRef, scrollRef, visibilityRef, isVisible, scrollToBottom } =
31
  useScrollAnchor(SCROLL_BOTTOM);
32
 
33
+ // Scroll to bottom on init
34
  useEffect(() => {
35
  scrollToBottom();
36
+ }, [scrollToBottom]);
 
 
 
 
37
 
38
  return (
39
  <Card
 
48
  key={message.id}
49
  message={message}
50
  loading={isLastMessage && isLoading}
51
+ wipAssistantMessage={
52
+ isLastMessage && data.length > 0 ? data : undefined
53
+ }
54
  />
55
  );
56
  })}
 
71
  prompt: input,
72
  mediaUrl: newMediaUrl,
73
  };
74
+ await dbPostCreateMessage(id, messageInput);
 
75
  }}
76
  />
77
  </div>
components/ui/Img.tsx CHANGED
@@ -21,12 +21,15 @@ const Img = React.forwardRef<
21
  const isVideo =
22
  typeof src === 'string' ? src.toLowerCase().endsWith('.mp4') : false;
23
 
 
 
 
24
  return (
25
  <Image
26
  src={
27
- isVideo
28
- ? (src as string).replace('.mp4', '.png').replace('.MP4', '.png')
29
- : src
30
  }
31
  placeholder={placeholder}
32
  width={dimensions.width}
 
21
  const isVideo =
22
  typeof src === 'string' ? src.toLowerCase().endsWith('.mp4') : false;
23
 
24
+ const srcString = isVideo
25
+ ? (src as string).replace('.mp4', '.png').replace('.MP4', '.png')
26
+ : (src as string);
27
  return (
28
  <Image
29
  src={
30
+ srcString.includes('vision-agent-dev.s3')
31
+ ? encodeURI(srcString)
32
+ : srcString
33
  }
34
  placeholder={placeholder}
35
  width={dimensions.width}
lib/hooks/useVisionAgent.ts CHANGED
@@ -3,22 +3,26 @@ import { toast } from 'react-hot-toast';
3
  import { useEffect, useRef } from 'react';
4
  import { ChatWithMessages } 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';
9
- import { useRouter } from 'next/navigation';
10
 
11
  const useVisionAgent = (chat: ChatWithMessages) => {
12
  const { messages: dbMessages, id, mediaUrl } = chat;
13
  const latestDbMessage = dbMessages[dbMessages.length - 1];
14
- const setMessageId = useSetAtom(selectedMessageId);
15
 
16
  // Temporary solution for now while single we have to pass mediaUrl separately outside of the messages
17
- const currMediaUrl = useRef<string>(mediaUrl);
18
- const currMessageId = useRef<string>(latestDbMessage?.id);
19
  const router = useRouter();
 
20
 
21
- const { append, isLoading, data, reload } = useChat({
 
 
 
 
 
 
22
  api: '/api/vision-agent',
23
  onResponse(response) {
24
  if (response.status !== 200) {
@@ -26,48 +30,62 @@ const useVisionAgent = (chat: ChatWithMessages) => {
26
  }
27
  },
28
  onFinish: () => {
 
29
  router.refresh();
30
- setMessageId(currMessageId.current);
31
  },
32
  body: {
33
- mediaUrl: currMediaUrl.current,
34
  chatId: id,
35
- messageId: currMessageId.current,
36
  // for some reason, the messages has to be stringified to be sent to the API
37
  apiMessages: JSON.stringify(convertDBMessageToAPIMessage(dbMessages)),
38
  },
39
  onError: err => {
40
  err && toast.error(err.message);
41
  },
42
- initialMessages: convertDBMessageToAPIMessage(dbMessages),
43
  });
44
 
45
  /**
46
  * If case this is first time user navigated with init message, we need to reload the chat for the first response
47
  */
48
  const once = useRef(true);
 
49
  useEffect(() => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  if (
51
- !isLoading &&
52
- !(latestDbMessage.response || latestDbMessage.responseBody) &&
53
- once.current
54
  ) {
55
- once.current = false;
56
- reload();
57
  }
58
- }, [isLoading, latestDbMessage.result, reload]);
 
 
 
 
 
59
 
60
  return {
61
- append: (message: Message) => {
62
- currMediaUrl.current = message.mediaUrl;
63
- currMessageId.current = message.id;
64
- append({
65
- id,
66
- role: 'user',
67
- content: message.prompt,
68
- });
69
- },
70
- data: data as unknown as PrismaJson.MessageBody[],
71
  reload,
72
  isLoading,
73
  };
 
3
  import { useEffect, useRef } from 'react';
4
  import { ChatWithMessages } from '../types';
5
  import { convertDBMessageToAPIMessage } from '../utils/message';
6
+ import { useRouter } from 'next/navigation';
7
+ import { Message } from '@prisma/client';
8
  import { useSetAtom } from 'jotai';
9
  import { selectedMessageId } from '@/state/chat';
 
 
10
 
11
  const useVisionAgent = (chat: ChatWithMessages) => {
12
  const { messages: dbMessages, id, mediaUrl } = chat;
13
  const latestDbMessage = dbMessages[dbMessages.length - 1];
 
14
 
15
  // Temporary solution for now while single we have to pass mediaUrl separately outside of the messages
 
 
16
  const router = useRouter();
17
+ const setMessageId = useSetAtom(selectedMessageId);
18
 
19
+ const {
20
+ data = [],
21
+ reload,
22
+ append,
23
+ messages,
24
+ isLoading,
25
+ } = useChat({
26
  api: '/api/vision-agent',
27
  onResponse(response) {
28
  if (response.status !== 200) {
 
30
  }
31
  },
32
  onFinish: () => {
33
+ setMessageId(latestDbMessage.id);
34
  router.refresh();
 
35
  },
36
  body: {
37
+ mediaUrl: latestDbMessage.mediaUrl,
38
  chatId: id,
39
+ messageId: latestDbMessage.id,
40
  // for some reason, the messages has to be stringified to be sent to the API
41
  apiMessages: JSON.stringify(convertDBMessageToAPIMessage(dbMessages)),
42
  },
43
  onError: err => {
44
  err && toast.error(err.message);
45
  },
 
46
  });
47
 
48
  /**
49
  * If case this is first time user navigated with init message, we need to reload the chat for the first response
50
  */
51
  const once = useRef(true);
52
+
53
  useEffect(() => {
54
+ const appendDbMessage = async (latestDbMessage: Message) => {
55
+ await append({
56
+ id: latestDbMessage.id + '-user',
57
+ content: latestDbMessage.prompt,
58
+ role: 'user',
59
+ });
60
+ };
61
+ if (isLoading || latestDbMessage.response || latestDbMessage.responseBody) {
62
+ return;
63
+ }
64
+ if (messages.length === 0) {
65
+ if (once.current) {
66
+ once.current = false;
67
+ appendDbMessage(latestDbMessage);
68
+ }
69
+ return;
70
+ }
71
  if (
72
+ messages.findIndex(message => message.id.includes(latestDbMessage.id)) ===
73
+ -1
 
74
  ) {
75
+ appendDbMessage(latestDbMessage);
 
76
  }
77
+ }, [latestDbMessage, messages, isLoading]);
78
+
79
+ const initDataIndex = data.findIndex(
80
+ (m: any) =>
81
+ m.type === 'init' && m.payload?.messageId === latestDbMessage.id,
82
+ );
83
 
84
  return {
85
+ data:
86
+ initDataIndex >= 0
87
+ ? (data.slice(initDataIndex + 1) as unknown as PrismaJson.MessageBody[])
88
+ : [],
 
 
 
 
 
 
89
  reload,
90
  isLoading,
91
  };