Spaces:
Running
Running
wuyiqunLu
commited on
feat: show image in user input (#30)
Browse files<img width="854" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/132986242/789a38c8-f491-456c-ad9a-eb22e19a5bc7">
<img width="869" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/132986242/864a6d3e-44c1-4f52-96cd-7f4e55b1178d">
<img width="1288" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/132986242/f8086c87-7a8e-4ec5-bcd5-84572462a964">
https://app.asana.com/0/1204554785675703/1207242625376120/f
app/api/vision-agent/route.ts
CHANGED
@@ -4,6 +4,7 @@ import { StreamingTextResponse } from 'ai';
|
|
4 |
import { MessageBase } from '../../../lib/types';
|
5 |
import { withLogging } from '@/lib/logger';
|
6 |
import { CLEANED_SEPARATOR } from '@/lib/constants';
|
|
|
7 |
|
8 |
// export const runtime = 'edge';
|
9 |
export const dynamic = 'force-dynamic';
|
@@ -33,14 +34,17 @@ export const POST = withLogging(
|
|
33 |
JSON.stringify(
|
34 |
messages.map(message => {
|
35 |
if (message.role !== 'assistant') {
|
36 |
-
return
|
|
|
|
|
|
|
37 |
} else {
|
38 |
const splitedContent = message.content.split(CLEANED_SEPARATOR);
|
39 |
return {
|
40 |
...message,
|
41 |
content:
|
42 |
splitedContent.length > 1
|
43 |
-
? splitedContent[1]
|
44 |
: message.content,
|
45 |
};
|
46 |
}
|
|
|
4 |
import { MessageBase } from '../../../lib/types';
|
5 |
import { withLogging } from '@/lib/logger';
|
6 |
import { CLEANED_SEPARATOR } from '@/lib/constants';
|
7 |
+
import { cleanAnswerMessage, cleanInputMessage } from '@/lib/messageUtils';
|
8 |
|
9 |
// export const runtime = 'edge';
|
10 |
export const dynamic = 'force-dynamic';
|
|
|
34 |
JSON.stringify(
|
35 |
messages.map(message => {
|
36 |
if (message.role !== 'assistant') {
|
37 |
+
return {
|
38 |
+
...message,
|
39 |
+
content: cleanInputMessage(message.content),
|
40 |
+
};
|
41 |
} else {
|
42 |
const splitedContent = message.content.split(CLEANED_SEPARATOR);
|
43 |
return {
|
44 |
...message,
|
45 |
content:
|
46 |
splitedContent.length > 1
|
47 |
+
? cleanAnswerMessage(splitedContent[1])
|
48 |
: message.content,
|
49 |
};
|
50 |
}
|
app/chat/page.tsx
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
'use client';
|
2 |
|
3 |
import ImageSelector from '@/components/chat/ImageSelector';
|
|
|
4 |
import { ChatEntity, MessageBase } from '@/lib/types';
|
5 |
import { fetcher } from '@/lib/utils';
|
6 |
import Image from 'next/image';
|
@@ -17,7 +18,12 @@ const examples: Example[] = [
|
|
17 |
initMessages: [
|
18 |
{
|
19 |
role: 'user',
|
20 |
-
content:
|
|
|
|
|
|
|
|
|
|
|
21 |
id: 'fake-id-1',
|
22 |
},
|
23 |
],
|
|
|
1 |
'use client';
|
2 |
|
3 |
import ImageSelector from '@/components/chat/ImageSelector';
|
4 |
+
import { generateInputImageMarkdown } from '@/lib/messageUtils';
|
5 |
import { ChatEntity, MessageBase } from '@/lib/types';
|
6 |
import { fetcher } from '@/lib/utils';
|
7 |
import Image from 'next/image';
|
|
|
18 |
initMessages: [
|
19 |
{
|
20 |
role: 'user',
|
21 |
+
content:
|
22 |
+
'how many cereals are there in the image?' +
|
23 |
+
'\n\n' +
|
24 |
+
generateInputImageMarkdown(
|
25 |
+
'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
|
26 |
+
),
|
27 |
id: 'fake-id-1',
|
28 |
},
|
29 |
],
|
components/chat-sidebar/ChatCard.tsx
CHANGED
@@ -9,6 +9,7 @@ import Image from 'next/image';
|
|
9 |
import clsx from 'clsx';
|
10 |
import Img from '../ui/Img';
|
11 |
import { format } from 'date-fns';
|
|
|
12 |
// import { format } from 'date-fns';
|
13 |
|
14 |
type ChatCardProps = PropsWithChildren<{
|
@@ -35,7 +36,7 @@ const ChatCard: React.FC<ChatCardProps> = ({ chat }) => {
|
|
35 |
const { id: chatIdFromParam } = useParams();
|
36 |
const pathname = usePathname();
|
37 |
const { id, url, messages, user, updatedAt } = chat;
|
38 |
-
const firstMessage = messages?.[0]?.content;
|
39 |
const title = firstMessage
|
40 |
? firstMessage.length > 50
|
41 |
? firstMessage.slice(0, 50) + '...'
|
|
|
9 |
import clsx from 'clsx';
|
10 |
import Img from '../ui/Img';
|
11 |
import { format } from 'date-fns';
|
12 |
+
import { cleanInputMessage } from '@/lib/messageUtils';
|
13 |
// import { format } from 'date-fns';
|
14 |
|
15 |
type ChatCardProps = PropsWithChildren<{
|
|
|
36 |
const { id: chatIdFromParam } = useParams();
|
37 |
const pathname = usePathname();
|
38 |
const { id, url, messages, user, updatedAt } = chat;
|
39 |
+
const firstMessage = cleanInputMessage(messages?.[0]?.content ?? '');
|
40 |
const title = firstMessage
|
41 |
? firstMessage.length > 50
|
42 |
? firstMessage.slice(0, 50) + '...'
|
components/chat/ChatMessage.tsx
CHANGED
@@ -4,26 +4,31 @@
|
|
4 |
import remarkGfm from 'remark-gfm';
|
5 |
import remarkMath from 'remark-math';
|
6 |
|
|
|
7 |
import { cn } from '@/lib/utils';
|
8 |
import { CodeBlock } from '@/components/ui/CodeBlock';
|
9 |
import { MemoizedReactMarkdown } from '@/components/chat/MemoizedReactMarkdown';
|
10 |
import { IconOpenAI, IconUser } from '@/components/ui/Icons';
|
11 |
-
import { ChatMessageActions } from '@/components/chat/ChatMessageActions';
|
12 |
import { MessageBase } from '../../lib/types';
|
13 |
-
import { useCleanedUpMessages } from '@/lib/hooks/useCleanedUpMessages';
|
14 |
import {
|
15 |
Tooltip,
|
16 |
TooltipContent,
|
17 |
TooltipTrigger,
|
18 |
} from '@/components/ui/Tooltip';
|
19 |
import Img from '../ui/Img';
|
|
|
20 |
|
21 |
export interface ChatMessageProps {
|
22 |
message: MessageBase;
|
23 |
}
|
24 |
|
25 |
export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
26 |
-
const { logs, content } =
|
|
|
|
|
|
|
|
|
|
|
27 |
return (
|
28 |
<div className={cn('group relative mb-4 flex items-start')} {...props}>
|
29 |
<div
|
|
|
4 |
import remarkGfm from 'remark-gfm';
|
5 |
import remarkMath from 'remark-math';
|
6 |
|
7 |
+
import { useMemo } from 'react';
|
8 |
import { cn } from '@/lib/utils';
|
9 |
import { CodeBlock } from '@/components/ui/CodeBlock';
|
10 |
import { MemoizedReactMarkdown } from '@/components/chat/MemoizedReactMarkdown';
|
11 |
import { IconOpenAI, IconUser } from '@/components/ui/Icons';
|
|
|
12 |
import { MessageBase } from '../../lib/types';
|
|
|
13 |
import {
|
14 |
Tooltip,
|
15 |
TooltipContent,
|
16 |
TooltipTrigger,
|
17 |
} from '@/components/ui/Tooltip';
|
18 |
import Img from '../ui/Img';
|
19 |
+
import { getCleanedUpMessages } from '@/lib/messageUtils';
|
20 |
|
21 |
export interface ChatMessageProps {
|
22 |
message: MessageBase;
|
23 |
}
|
24 |
|
25 |
export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
26 |
+
const { logs, content } = useMemo(() => {
|
27 |
+
return getCleanedUpMessages({
|
28 |
+
content: message.content,
|
29 |
+
role: message.role,
|
30 |
+
});
|
31 |
+
}, [message.content, message.role]);
|
32 |
return (
|
33 |
<div className={cn('group relative mb-4 flex items-start')} {...props}>
|
34 |
<div
|
components/chat/Composer.tsx
CHANGED
@@ -20,6 +20,7 @@ import {
|
|
20 |
IconStop,
|
21 |
} from '@/components/ui/Icons';
|
22 |
import { cn } from '@/lib/utils';
|
|
|
23 |
|
24 |
export interface ComposerProps
|
25 |
extends Pick<
|
@@ -69,7 +70,8 @@ export function Composer({
|
|
69 |
setInput('');
|
70 |
await append({
|
71 |
id,
|
72 |
-
content:
|
|
|
73 |
role: 'user',
|
74 |
});
|
75 |
scrollToBottom();
|
|
|
20 |
IconStop,
|
21 |
} from '@/components/ui/Icons';
|
22 |
import { cn } from '@/lib/utils';
|
23 |
+
import { generateInputImageMarkdown } from '@/lib/messageUtils';
|
24 |
|
25 |
export interface ComposerProps
|
26 |
extends Pick<
|
|
|
70 |
setInput('');
|
71 |
await append({
|
72 |
id,
|
73 |
+
content:
|
74 |
+
input + (url ? '\n\n' + generateInputImageMarkdown(url) : ''),
|
75 |
role: 'user',
|
76 |
});
|
77 |
scrollToBottom();
|
lib/hooks/{useVisionAgent.tsx β useVisionAgent.ts}
RENAMED
@@ -4,7 +4,11 @@ import { useEffect, useState } from 'react';
|
|
4 |
import { ChatEntity, MessageBase, SignedPayload } from '../types';
|
5 |
import { saveKVChatMessage } from '../kv/chat';
|
6 |
import { fetcher, nanoid } from '../utils';
|
7 |
-
import {
|
|
|
|
|
|
|
|
|
8 |
import { CLEANED_SEPARATOR } from '../constants';
|
9 |
|
10 |
const uploadBase64 = async (
|
@@ -74,8 +78,8 @@ const useVisionAgent = (chat: ChatEntity) => {
|
|
74 |
);
|
75 |
const newContent = publicUrls.reduce((accum, url, index) => {
|
76 |
return accum.replace(
|
77 |
-
|
78 |
-
|
79 |
);
|
80 |
}, content);
|
81 |
const newMessage = {
|
@@ -93,7 +97,7 @@ const useVisionAgent = (chat: ChatEntity) => {
|
|
93 |
{
|
94 |
id: nanoid(),
|
95 |
role: 'user',
|
96 |
-
content: input,
|
97 |
createdAt: new Date(),
|
98 |
} satisfies Message,
|
99 |
]
|
|
|
4 |
import { ChatEntity, MessageBase, SignedPayload } from '../types';
|
5 |
import { saveKVChatMessage } from '../kv/chat';
|
6 |
import { fetcher, nanoid } from '../utils';
|
7 |
+
import {
|
8 |
+
getCleanedUpMessages,
|
9 |
+
generateAnswersImageMarkdown,
|
10 |
+
generateInputImageMarkdown,
|
11 |
+
} from '../messageUtils';
|
12 |
import { CLEANED_SEPARATOR } from '../constants';
|
13 |
|
14 |
const uploadBase64 = async (
|
|
|
78 |
);
|
79 |
const newContent = publicUrls.reduce((accum, url, index) => {
|
80 |
return accum.replace(
|
81 |
+
generateAnswersImageMarkdown(index, '/loading.gif'),
|
82 |
+
generateAnswersImageMarkdown(index, url),
|
83 |
);
|
84 |
}, content);
|
85 |
const newMessage = {
|
|
|
97 |
{
|
98 |
id: nanoid(),
|
99 |
role: 'user',
|
100 |
+
content: input + '\n\n' + generateInputImageMarkdown(url),
|
101 |
createdAt: new Date(),
|
102 |
} satisfies Message,
|
103 |
]
|
lib/{hooks/useCleanedUpMessages.ts β messageUtils.ts}
RENAMED
@@ -1,6 +1,5 @@
|
|
1 |
-
import {
|
2 |
-
import {
|
3 |
-
import { CLEANED_SEPARATOR } from '../constants';
|
4 |
|
5 |
const PAIRS: Record<string, string> = {
|
6 |
'β': 'β',
|
@@ -12,6 +11,26 @@ const PAIRS: Record<string, string> = {
|
|
12 |
const MIDDLE_STARTER = 'β';
|
13 |
const MIDDLE_SEPARATOR = 'βΏ';
|
14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
export const getCleanedUpMessages = ({
|
16 |
content,
|
17 |
role,
|
@@ -70,15 +89,11 @@ export const getCleanedUpMessages = ({
|
|
70 |
logs: cleanedLogs.join('').replace(/β/g, '|').split('|\n\n|').join('|\n|'),
|
71 |
content:
|
72 |
answerText.replace('</</ANSWER>', '').replace('</ANSWER>', '') +
|
73 |
-
|
|
|
|
|
|
|
74 |
rest.join(''),
|
75 |
images: images,
|
76 |
};
|
77 |
};
|
78 |
-
|
79 |
-
export const useCleanedUpMessages = ({ content, role }: MessageBase) => {
|
80 |
-
const cleanedMessage = useMemo(() => {
|
81 |
-
return getCleanedUpMessages({ content, role });
|
82 |
-
}, [content, role]);
|
83 |
-
return cleanedMessage;
|
84 |
-
};
|
|
|
1 |
+
import { MessageBase } from './types';
|
2 |
+
import { CLEANED_SEPARATOR } from './constants';
|
|
|
3 |
|
4 |
const PAIRS: Record<string, string> = {
|
5 |
'β': 'β',
|
|
|
11 |
const MIDDLE_STARTER = 'β';
|
12 |
const MIDDLE_SEPARATOR = 'βΏ';
|
13 |
|
14 |
+
const ANSWERS_PREFIX = 'answers';
|
15 |
+
const INPUT_PREFIX = 'input';
|
16 |
+
|
17 |
+
export const generateAnswersImageMarkdown = (index: number, url: string) => {
|
18 |
+
return `![${ANSWERS_PREFIX}-${index}](${url})`;
|
19 |
+
};
|
20 |
+
|
21 |
+
export const generateInputImageMarkdown = (url: string, index = 0) => {
|
22 |
+
const prefix = 'input';
|
23 |
+
return `![${INPUT_PREFIX}-${index}](${url})`;
|
24 |
+
};
|
25 |
+
|
26 |
+
export const cleanInputMessage = (content: string) => {
|
27 |
+
return content.replace(/!\[input-.*?\)/g, '');
|
28 |
+
};
|
29 |
+
|
30 |
+
export const cleanAnswerMessage = (content: string) => {
|
31 |
+
return content.replace(/!\[answers.*?\.png\)/g, '');
|
32 |
+
};
|
33 |
+
|
34 |
export const getCleanedUpMessages = ({
|
35 |
content,
|
36 |
role,
|
|
|
89 |
logs: cleanedLogs.join('').replace(/β/g, '|').split('|\n\n|').join('|\n|'),
|
90 |
content:
|
91 |
answerText.replace('</</ANSWER>', '').replace('</ANSWER>', '') +
|
92 |
+
'\n\n' +
|
93 |
+
images
|
94 |
+
.map((_, index) => generateAnswersImageMarkdown(index, '/loading.gif'))
|
95 |
+
.join('') +
|
96 |
rest.join(''),
|
97 |
images: images,
|
98 |
};
|
99 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|