wuyiqunLu commited on
Commit
0d6f04b
β€’
1 Parent(s): dfa7532

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 message;
 
 
 
37
  } else {
38
  const splitedContent = message.content.split(CLEANED_SEPARATOR);
39
  return {
40
  ...message,
41
  content:
42
  splitedContent.length > 1
43
- ? splitedContent[1].replace(/!\[answers.*?\.png\)/g, '')
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: 'how many cereals are there in the image?',
 
 
 
 
 
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 } = useCleanedUpMessages(message);
 
 
 
 
 
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: input,
 
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 { getCleanedUpMessages } from './useCleanedUpMessages';
 
 
 
 
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
- `![answers-${index}](/loading.gif)`,
78
- `![answers-${index}](${url})`,
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 { useMemo } from 'react';
2
- import { MessageBase } from '../types';
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
- images.map((_, index) => `![answers-${index}](/loading.gif)`).join('') +
 
 
 
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
  };