Spaces:
Running
Running
wuyiqunLu
commited on
Commit
•
c7e97b5
1
Parent(s):
7286745
feat: encode uri for s3 url when loading (#104)
Browse filesfile 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 +17 -3
- components/ChatInterface.tsx +10 -2
- components/chat/ChatList.tsx +7 -15
- components/ui/Img.tsx +6 -3
- lib/hooks/useVisionAgent.ts +44 -26
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 |
-
|
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 =
|
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 {
|
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
|
39 |
useEffect(() => {
|
40 |
scrollToBottom();
|
41 |
-
|
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={
|
|
|
|
|
61 |
/>
|
62 |
);
|
63 |
})}
|
@@ -78,8 +71,7 @@ const ChatList: React.FC<ChatListProps> = ({ chat, userId }) => {
|
|
78 |
prompt: input,
|
79 |
mediaUrl: newMediaUrl,
|
80 |
};
|
81 |
-
|
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 |
-
|
28 |
-
? (
|
29 |
-
:
|
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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
34 |
chatId: id,
|
35 |
-
messageId:
|
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 |
-
|
52 |
-
|
53 |
-
once.current
|
54 |
) {
|
55 |
-
|
56 |
-
reload();
|
57 |
}
|
58 |
-
}, [
|
|
|
|
|
|
|
|
|
|
|
59 |
|
60 |
return {
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
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 |
};
|