Spaces:
Sleeping
Sleeping
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 ``;
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
export const generateInputImageMarkdown = (url: string, index = 0) => {
|
| 22 |
+
const prefix = 'input';
|
| 23 |
+
return ``;
|
| 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 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|