Spaces:
Sleeping
Sleeping
wuyiqunLu
commited on
Commit
•
5d7d435
1
Parent(s):
8e3dbd3
feat: support image rendering on answer (#19)
Browse fileshttps://app.asana.com/0/1204554785675703/1207155143920392/f
<img width="769" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/132986242/e20eac86-73ec-4dc2-8764-afb0f0c153da">
- app/api/sign/route.ts +3 -4
- app/api/vision-agent/route.ts +1 -1
- components/chat/index.tsx +0 -1
- lib/hooks/useCleanedUpMessages.ts +62 -47
- lib/hooks/useVisionAgent.tsx +68 -3
app/api/sign/route.ts
CHANGED
@@ -16,14 +16,13 @@ export async function POST(req: Request): Promise<Response> {
|
|
16 |
// }
|
17 |
|
18 |
try {
|
19 |
-
const { fileName, fileType } = (await req.json()) as {
|
|
|
20 |
fileName: string;
|
21 |
fileType: string;
|
22 |
};
|
23 |
|
24 |
-
const
|
25 |
-
|
26 |
-
const signedFileName = `${user}/${id}/${fileName}`;
|
27 |
const res = await getPresignedUrl(signedFileName, fileType);
|
28 |
return Response.json({
|
29 |
id,
|
|
|
16 |
// }
|
17 |
|
18 |
try {
|
19 |
+
const { fileName, fileType, id } = (await req.json()) as {
|
20 |
+
id?: string;
|
21 |
fileName: string;
|
22 |
fileType: string;
|
23 |
};
|
24 |
|
25 |
+
const signedFileName = `${user}/${id ?? nanoid()}/${fileName}`;
|
|
|
|
|
26 |
const res = await getPresignedUrl(signedFileName, fileType);
|
27 |
return Response.json({
|
28 |
id,
|
app/api/vision-agent/route.ts
CHANGED
@@ -27,7 +27,7 @@ export async function POST(req: Request) {
|
|
27 |
formData.append('image', url);
|
28 |
|
29 |
const fetchResponse = await fetch(
|
30 |
-
'https://api.dev.landing.ai/v1/agent/chat?agent_class=vision_agent',
|
31 |
// 'http://localhost:5050/v1/agent/chat?agent_class=vision_agent',
|
32 |
{
|
33 |
method: 'POST',
|
|
|
27 |
formData.append('image', url);
|
28 |
|
29 |
const fetchResponse = await fetch(
|
30 |
+
'https://api.dev.landing.ai/v1/agent/chat?agent_class=vision_agent&visualize_output=true',
|
31 |
// 'http://localhost:5050/v1/agent/chat?agent_class=vision_agent',
|
32 |
{
|
33 |
method: 'POST',
|
components/chat/index.tsx
CHANGED
@@ -20,7 +20,6 @@ export function Chat({ chat }: ChatProps) {
|
|
20 |
|
21 |
const { messagesRef, scrollRef, visibilityRef, isAtBottom, scrollToBottom } =
|
22 |
useScrollAnchor();
|
23 |
-
console.log('[Ming] ~ Chat ~ isAtBottom:', isAtBottom);
|
24 |
|
25 |
return (
|
26 |
<>
|
|
|
20 |
|
21 |
const { messagesRef, scrollRef, visibilityRef, isAtBottom, scrollToBottom } =
|
22 |
useScrollAnchor();
|
|
|
23 |
|
24 |
return (
|
25 |
<>
|
lib/hooks/useCleanedUpMessages.ts
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
-
import { useMemo } from 'react';
|
2 |
-
import { MessageBase } from '../types';
|
|
|
3 |
|
4 |
const PAIRS: Record<string, string> = {
|
5 |
'┍': '┑',
|
@@ -10,54 +11,68 @@ const PAIRS: Record<string, string> = {
|
|
10 |
|
11 |
const MIDDLE_STARTER = '┝';
|
12 |
const MIDDLE_SEPARATOR = '┿';
|
|
|
13 |
|
14 |
-
export const
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
cleanedLogs.push(
|
42 |
-
Array(separators + 1)
|
43 |
-
.fill('|')
|
44 |
-
.join(' :- '),
|
45 |
-
);
|
46 |
-
}
|
47 |
-
}
|
48 |
-
left = ++right;
|
49 |
-
} else {
|
50 |
right++;
|
51 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
}
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
|
|
|
|
|
|
|
|
|
|
62 |
}, [content, role]);
|
|
|
63 |
};
|
|
|
1 |
+
import { useMemo, useEffect } from 'react';
|
2 |
+
import { MessageBase, SignedPayload } from '../types';
|
3 |
+
import { fetcher } from '../utils';
|
4 |
|
5 |
const PAIRS: Record<string, string> = {
|
6 |
'┍': '┑',
|
|
|
11 |
|
12 |
const MIDDLE_STARTER = '┝';
|
13 |
const MIDDLE_SEPARATOR = '┿';
|
14 |
+
export const CLEANED_SEPARATOR = '|CLEANED|';
|
15 |
|
16 |
+
export const getCleanedUpMessages = ({
|
17 |
+
content,
|
18 |
+
role,
|
19 |
+
}: Pick<MessageBase, 'role' | 'content'>) => {
|
20 |
+
if (role === 'user') {
|
21 |
+
return {
|
22 |
+
content,
|
23 |
+
};
|
24 |
+
}
|
25 |
+
if (content.split(CLEANED_SEPARATOR).length === 2) {
|
26 |
+
return {
|
27 |
+
logs: content.split(CLEANED_SEPARATOR)[0],
|
28 |
+
content: content.split(CLEANED_SEPARATOR)[1],
|
29 |
+
};
|
30 |
+
}
|
31 |
+
const [logs = '', answer = ''] = content.split('<ANSWER>');
|
32 |
+
const cleanedLogs = [];
|
33 |
+
let left = 0;
|
34 |
+
let right = 0;
|
35 |
+
while (right < logs.length) {
|
36 |
+
if (Object.keys(PAIRS).includes(content[right])) {
|
37 |
+
cleanedLogs.push(content.substring(left, right));
|
38 |
+
left = right++;
|
39 |
+
while (
|
40 |
+
right < content.length &&
|
41 |
+
PAIRS[content[left]] !== content[right]
|
42 |
+
) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
right++;
|
44 |
}
|
45 |
+
if (content[left] === MIDDLE_STARTER) {
|
46 |
+
// add the text alignment so it can be shown as a table
|
47 |
+
const separators = logs
|
48 |
+
.substring(left, right)
|
49 |
+
.split(MIDDLE_SEPARATOR).length;
|
50 |
+
if (separators > 0) {
|
51 |
+
cleanedLogs.push(
|
52 |
+
Array(separators + 1)
|
53 |
+
.fill('|')
|
54 |
+
.join(' :- '),
|
55 |
+
);
|
56 |
+
}
|
57 |
+
}
|
58 |
+
left = ++right;
|
59 |
+
} else {
|
60 |
+
right++;
|
61 |
}
|
62 |
+
}
|
63 |
+
cleanedLogs.push(content.substring(left, right));
|
64 |
+
const [answerText, imagesStr = ''] = answer.split('<VIZ>');
|
65 |
+
const images = imagesStr.split('</IMG>').map(str => str.replace('<IMG>', ''));
|
66 |
+
return {
|
67 |
+
logs: cleanedLogs.join('').replace(/│/g, '|').split('|\n\n|').join('|\n|'),
|
68 |
+
content: answerText.replace('</</ANSWER>', '').replace('</ANSWER>', ''),
|
69 |
+
images: images.slice(0, -1),
|
70 |
+
};
|
71 |
+
};
|
72 |
+
|
73 |
+
export const useCleanedUpMessages = ({ content, role }: MessageBase) => {
|
74 |
+
const cleanedMessage = useMemo(() => {
|
75 |
+
return getCleanedUpMessages({ content, role });
|
76 |
}, [content, role]);
|
77 |
+
return cleanedMessage;
|
78 |
};
|
lib/hooks/useVisionAgent.tsx
CHANGED
@@ -1,8 +1,49 @@
|
|
1 |
import { useChat, type Message, UseChatHelpers } from 'ai/react';
|
2 |
import { toast } from 'react-hot-toast';
|
3 |
import { useEffect, useState } from 'react';
|
4 |
-
import { ChatEntity, MessageBase } from '../types';
|
5 |
import { saveKVChatMessage } from '../kv/chat';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
|
7 |
const useVisionAgent = (chat: ChatEntity) => {
|
8 |
const { messages: initialMessages, id, url } = chat;
|
@@ -23,8 +64,32 @@ const useVisionAgent = (chat: ChatEntity) => {
|
|
23 |
toast.error(response.statusText);
|
24 |
}
|
25 |
},
|
26 |
-
onFinish
|
27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
},
|
29 |
initialMessages: initialMessages,
|
30 |
body: {
|
|
|
1 |
import { useChat, type Message, UseChatHelpers } from 'ai/react';
|
2 |
import { toast } from 'react-hot-toast';
|
3 |
import { useEffect, useState } from 'react';
|
4 |
+
import { ChatEntity, MessageBase, SignedPayload } from '../types';
|
5 |
import { saveKVChatMessage } from '../kv/chat';
|
6 |
+
import { fetcher } from '../utils';
|
7 |
+
import {
|
8 |
+
getCleanedUpMessages,
|
9 |
+
CLEANED_SEPARATOR,
|
10 |
+
} from './useCleanedUpMessages';
|
11 |
+
|
12 |
+
const uploadBase64 = async (
|
13 |
+
base64: string,
|
14 |
+
messageId: string,
|
15 |
+
chatId: string,
|
16 |
+
index: number,
|
17 |
+
) => {
|
18 |
+
const res = await fetch('data:image/png;base64,' + base64);
|
19 |
+
const blob = await res.blob();
|
20 |
+
const { signedUrl, publicUrl, fields } = await fetcher<SignedPayload>(
|
21 |
+
'/api/sign',
|
22 |
+
{
|
23 |
+
method: 'POST',
|
24 |
+
body: JSON.stringify({
|
25 |
+
id: `${chatId}/${messageId}`,
|
26 |
+
fileType: blob.type,
|
27 |
+
fileName: `answer-${index}.${blob.type.split('/')[1]}`,
|
28 |
+
}),
|
29 |
+
},
|
30 |
+
);
|
31 |
+
const formData = new FormData();
|
32 |
+
Object.entries(fields).forEach(([key, value]) => {
|
33 |
+
formData.append(key, value as string);
|
34 |
+
});
|
35 |
+
formData.append('file', blob);
|
36 |
+
|
37 |
+
const uploadResponse = await fetch(signedUrl, {
|
38 |
+
method: 'POST',
|
39 |
+
body: formData,
|
40 |
+
});
|
41 |
+
if (uploadResponse.ok) {
|
42 |
+
return publicUrl;
|
43 |
+
} else {
|
44 |
+
throw new Error('Upload failed');
|
45 |
+
}
|
46 |
+
};
|
47 |
|
48 |
const useVisionAgent = (chat: ChatEntity) => {
|
49 |
const { messages: initialMessages, id, url } = chat;
|
|
|
64 |
toast.error(response.statusText);
|
65 |
}
|
66 |
},
|
67 |
+
onFinish: async message => {
|
68 |
+
const { logs = '', content, images } = getCleanedUpMessages(message);
|
69 |
+
if (images?.length) {
|
70 |
+
const publicUrls = await Promise.all(
|
71 |
+
images.map((image, index) =>
|
72 |
+
uploadBase64(image, message.id, id, index),
|
73 |
+
),
|
74 |
+
);
|
75 |
+
const newMessage = {
|
76 |
+
...message,
|
77 |
+
content:
|
78 |
+
logs +
|
79 |
+
CLEANED_SEPARATOR +
|
80 |
+
content +
|
81 |
+
'\n' +
|
82 |
+
publicUrls
|
83 |
+
.map((url, index) => `![image-${index}](${url})`)
|
84 |
+
.join('\n'),
|
85 |
+
};
|
86 |
+
saveKVChatMessage(id, newMessage);
|
87 |
+
} else {
|
88 |
+
saveKVChatMessage(id, {
|
89 |
+
...message,
|
90 |
+
content: logs + CLEANED_SEPARATOR + content,
|
91 |
+
});
|
92 |
+
}
|
93 |
},
|
94 |
initialMessages: initialMessages,
|
95 |
body: {
|