MingruiZhang commited on
Commit
0fd8446
·
unverified ·
1 Parent(s): a1c5622

feat: UI Improvements (#72)

Browse files

1. Tooltip -> Dialog
2. Show `print` and hide `print` / `output` if empty
3. Put `mediaUrl` in Message as `sendExtraMessageFields`
4. Change started status icon to glowing dot
<img width="680" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/5669963/a7a84d0a-2b4b-4e2a-b3cf-f7b6cb1b8221">

app/globals.css CHANGED
@@ -87,3 +87,24 @@ h1 {
87
  background-size: cover;
88
  background: linear-gradient(to bottom, rgb(9, 9, 11), rgb(32, 32, 39));
89
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  background-size: cover;
88
  background: linear-gradient(to bottom, rgb(9, 9, 11), rgb(32, 32, 39));
89
  }
90
+
91
+ .text-webkit-center {
92
+ text-align: -webkit-center;
93
+ }
94
+
95
+ .svg-shadow {
96
+ border-radius: 100%;
97
+ animation: svg-shadow 1.5s ease-in-out infinite alternate;
98
+ }
99
+
100
+ @keyframes svg-shadow {
101
+ from {
102
+ filter: drop-shadow(0 0 5px #fff) drop-shadow(0 0 5px transparent)
103
+ drop-shadow(0 0 10px transparent);
104
+ }
105
+
106
+ to {
107
+ filter: drop-shadow(0 0 20px #fff) drop-shadow(0 0 10px transparent)
108
+ drop-shadow(0 0 20px transparent);
109
+ }
110
+ }
components/chat/ChatMessage.tsx CHANGED
@@ -1,17 +1,8 @@
1
- // Inspired by Chatbot-UI and modified to fit the needs of this project
2
- // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx
3
-
4
- import remarkGfm from 'remark-gfm';
5
- import remarkMath from 'remark-math';
6
- import rehypeRaw from 'rehype-raw';
7
-
8
  import { useMemo, useState } from 'react';
9
  import { cn } from '@/lib/utils';
10
  import { CodeBlock } from '@/components/ui/CodeBlock';
11
- import { MemoizedReactMarkdown } from '@/components/chat/MemoizedReactMarkdown';
12
  import {
13
  IconCheckCircle,
14
- IconChevronDoubleRight,
15
  IconCodeWrap,
16
  IconCrossCircle,
17
  IconLandingAI,
@@ -20,9 +11,9 @@ import {
20
  IconUser,
21
  IconOutput,
22
  IconLog,
 
23
  } from '@/components/ui/Icons';
24
  import { MessageUI } from '@/lib/types';
25
- import Img from '../ui/Img';
26
  import { ChunkBody, CodeResult, formatStreamLogs } from '@/lib/utils/content';
27
  import {
28
  Table,
@@ -41,107 +32,19 @@ import {
41
  } from '@/components/ui/Tooltip';
42
 
43
  import { Dialog, DialogContent, DialogTrigger } from '../ui/Dialog';
 
 
44
 
45
  export interface ChatMessageProps {
46
  message: MessageUI;
47
  isLoading: boolean;
48
  }
49
 
50
- const Markdown: React.FC<{
51
- content: string;
52
- setDetails?: (val: string) => void;
53
- }> = ({ content, setDetails }) => {
54
- return (
55
- <>
56
- <MemoizedReactMarkdown
57
- className="break-words overflow-auto"
58
- remarkPlugins={[remarkGfm, remarkMath]}
59
- rehypePlugins={[rehypeRaw] as any}
60
- components={{
61
- table({ children, ...props }) {
62
- return <Table {...props}>{children}</Table>;
63
- },
64
- thead({ children, ...props }) {
65
- return <TableHeader {...props}>{children}</TableHeader>;
66
- },
67
- th({ children, ...props }) {
68
- return <TableHead {...props}>{children}</TableHead>;
69
- },
70
- tr({ children, ...props }) {
71
- return <TableRow {...props}>{children}</TableRow>;
72
- },
73
- td({ children, ...props }) {
74
- return <TableCell {...props}>{children}</TableCell>;
75
- },
76
- button({ children, ...props }) {
77
- if ('data-details' in props && setDetails) {
78
- return (
79
- <Button
80
- {...props}
81
- onClick={() =>
82
- setDetails(decodeURI(props['data-details'] as string))
83
- }
84
- >
85
- {children}
86
- </Button>
87
- );
88
- }
89
- return <Button {...props}>{children}</Button>;
90
- },
91
- p({ children, ...props }) {
92
- if (
93
- props.node.children.some(
94
- child => child.type === 'element' && child.tagName === 'img',
95
- )
96
- ) {
97
- return (
98
- <p className="flex flex-wrap gap-2 items-start">{children}</p>
99
- );
100
- }
101
- return <p className="mb-2 whitespace-pre-line">{children}</p>;
102
- },
103
- img(props) {
104
- if (props.src?.endsWith('.mp4')) {
105
- return (
106
- <video src={props.src} controls width={500} height={500} />
107
- );
108
- }
109
- return (
110
- <Img
111
- src={props.src ?? '/landing.png'}
112
- alt={props.alt ?? 'answer-image'}
113
- quality={100}
114
- sizes="(min-width: 66em) 15vw,
115
- (min-width: 44em) 20vw,
116
- 100vw"
117
- />
118
- );
119
- },
120
- code({ node, inline, className, children, ...props }) {
121
- const match = /language-(\w+)/.exec(className || '');
122
-
123
- return (
124
- <CodeBlock
125
- key={Math.random()}
126
- language={(match && match[1]) || ''}
127
- value={String(children).replace(/\n$/, '')}
128
- {...props}
129
- />
130
- );
131
- },
132
- }}
133
- >
134
- {content}
135
- </MemoizedReactMarkdown>
136
- </>
137
- );
138
- };
139
-
140
  export function ChatMessage({ message, isLoading }: ChatMessageProps) {
141
- const { role, content } = message;
142
 
143
  return role === 'user' ? (
144
- <UserChatMessage content={content} />
145
  ) : (
146
  <AssistantChatMessage content={content} />
147
  );
@@ -149,23 +52,40 @@ export function ChatMessage({ message, isLoading }: ChatMessageProps) {
149
 
150
  const UserChatMessage: React.FC<{
151
  content: string;
152
- }> = ({ content }) => {
 
153
  return (
154
- <div className="group relative mb-6 flex rounded-md bg-muted p-4 ml-auto mr-0 w-3/5">
155
  <div className="flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-background">
156
  <IconUser />
157
  </div>
158
- <div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
159
- {content && <Markdown content={content} />}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  </div>
161
  </div>
162
  );
163
  };
164
 
165
  const ChunkStatusToIconDict: Record<ChunkBody['status'], React.ReactElement> = {
166
- started: <IconChevronDoubleRight className="text-cyan-500" />,
167
  completed: <IconCheckCircle className="text-green-500" />,
168
- running: <IconTerminalWindow className="text-teal-500" />,
169
  failed: <IconCrossCircle className="text-red-500" />,
170
  };
171
  const ChunkTypeToTextDict: Record<ChunkBody['type'], string> = {
@@ -177,6 +97,7 @@ const ChunkTypeToTextDict: Record<ChunkBody['type'], string> = {
177
  const ChunkPayloadAction: React.FC<{
178
  payload: ChunkBody['payload'];
179
  }> = ({ payload }) => {
 
180
  if (Array.isArray(payload)) {
181
  // [{title: 123, content, 345}, {title: ..., content: ...}] => ['title', 'content']
182
  const keyArray = Array.from(
@@ -208,8 +129,8 @@ const ChunkPayloadAction: React.FC<{
208
  {keyArray.map(header =>
209
  header === 'documentation' ? (
210
  <TableCell key={header}>
211
- <Tooltip>
212
- <TooltipTrigger asChild>
213
  <Button
214
  variant="ghost"
215
  size="icon"
@@ -217,11 +138,11 @@ const ChunkPayloadAction: React.FC<{
217
  >
218
  <IconTerminalWindow className="text-teal-500 size-4" />
219
  </Button>
220
- </TooltipTrigger>
221
- <TooltipContent>
222
  <CodeBlock language="md" value={line[header]} />
223
- </TooltipContent>
224
- </Tooltip>
225
  </TableCell>
226
  ) : (
227
  <TableCell key={header}>{line[header]}</TableCell>
@@ -274,45 +195,44 @@ const CodeResultDisplay: React.FC<{
274
  <div className="rounded-lg overflow-hidden relative max-w-5xl">
275
  <CodeBlock language="python" value={code} />
276
  <div className="rounded-lg relative">
277
- <Separator />
278
  <div className="absolute left-1/2 -translate-x-1/2 -top-4 z-10">
279
- <Tooltip>
280
- <TooltipTrigger asChild>
281
  <Button variant="ghost" size="icon" className="size-8">
282
  <IconTerminalWindow className="text-teal-500 size-4" />
283
  </Button>
284
- </TooltipTrigger>
285
- <TooltipContent>
286
  <CodeBlock language="python" value={test} />
287
- </TooltipContent>
288
- </Tooltip>
289
- {Array.isArray(stdout) && (
290
- <Tooltip>
291
- <TooltipTrigger asChild>
292
- <Button variant="ghost" size="icon" className="size-8">
293
- <IconOutput className="text-blue-500 size-4" />
294
- </Button>
295
- </TooltipTrigger>
296
- <TooltipContent>
297
- <CodeBlock language="vim" value={stdout.join('').trim()} />
298
- </TooltipContent>
299
- </Tooltip>
300
- )}
301
  {Array.isArray(stderr) && (
302
- <Tooltip>
303
- <TooltipTrigger asChild>
304
  <Button variant="ghost" size="icon" className="size-8">
305
  <IconLog className="text-gray-500 size-4" />
306
  </Button>
307
- </TooltipTrigger>
308
- <TooltipContent>
309
  <CodeBlock language="vim" value={stderr.join('').trim()} />
310
- </TooltipContent>
311
- </Tooltip>
312
  )}
313
  </div>
314
  </div>
315
- <CodeBlock language="output" value={results} />
 
 
 
 
 
 
 
 
 
 
 
 
316
  </div>
317
  );
318
  };
@@ -326,7 +246,7 @@ const AssistantChatMessage: React.FC<{
326
  );
327
 
328
  return (
329
- <div className="group relative mb-6 flex rounded-md bg-muted p-4 w-full">
330
  <div className="flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-primary text-primary-foreground">
331
  <IconLandingAI />
332
  </div>
@@ -334,8 +254,13 @@ const AssistantChatMessage: React.FC<{
334
  <Table className="w-[400px]">
335
  <TableBody>
336
  {formattedSections.map(section => (
337
- <TableRow className="border-primary/50" key={section.type}>
338
- <TableCell>{ChunkStatusToIconDict[section.status]}</TableCell>
 
 
 
 
 
339
  <TableCell className="font-medium">
340
  {ChunkTypeToTextDict[section.type]}
341
  </TableCell>
 
 
 
 
 
 
 
 
1
  import { useMemo, useState } from 'react';
2
  import { cn } from '@/lib/utils';
3
  import { CodeBlock } from '@/components/ui/CodeBlock';
 
4
  import {
5
  IconCheckCircle,
 
6
  IconCodeWrap,
7
  IconCrossCircle,
8
  IconLandingAI,
 
11
  IconUser,
12
  IconOutput,
13
  IconLog,
14
+ IconGlowingDot,
15
  } from '@/components/ui/Icons';
16
  import { MessageUI } from '@/lib/types';
 
17
  import { ChunkBody, CodeResult, formatStreamLogs } from '@/lib/utils/content';
18
  import {
19
  Table,
 
32
  } from '@/components/ui/Tooltip';
33
 
34
  import { Dialog, DialogContent, DialogTrigger } from '../ui/Dialog';
35
+ import { Markdown } from './MemoizedReactMarkdown';
36
+ import Img from '../ui/Img';
37
 
38
  export interface ChatMessageProps {
39
  message: MessageUI;
40
  isLoading: boolean;
41
  }
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  export function ChatMessage({ message, isLoading }: ChatMessageProps) {
44
+ const { role, content, mediaUrl } = message;
45
 
46
  return role === 'user' ? (
47
+ <UserChatMessage content={content} mediaUrl={mediaUrl} />
48
  ) : (
49
  <AssistantChatMessage content={content} />
50
  );
 
52
 
53
  const UserChatMessage: React.FC<{
54
  content: string;
55
+ mediaUrl?: string;
56
+ }> = ({ content, mediaUrl }) => {
57
  return (
58
+ <div className="group relative mb-6 flex rounded-md bg-muted p-6 ml-auto mr-0 w-3/5">
59
  <div className="flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-background">
60
  <IconUser />
61
  </div>
62
+ <div className="flex-1 px-1 ml-4 space-y-3 overflow-hidden">
63
+ <p>{content}</p>
64
+ {mediaUrl && (
65
+ <>
66
+ {mediaUrl?.endsWith('.mp4') ? (
67
+ <video src={mediaUrl} controls width={500} height={500} />
68
+ ) : (
69
+ <Img
70
+ src={mediaUrl}
71
+ alt={mediaUrl}
72
+ quality={100}
73
+ sizes="(min-width: 66em) 15vw,
74
+ (min-width: 44em) 20vw,
75
+ 100vw"
76
+ />
77
+ )}
78
+ </>
79
+ )}
80
  </div>
81
  </div>
82
  );
83
  };
84
 
85
  const ChunkStatusToIconDict: Record<ChunkBody['status'], React.ReactElement> = {
86
+ started: <IconGlowingDot className="bg-yellow-500/80" />,
87
  completed: <IconCheckCircle className="text-green-500" />,
88
+ running: <IconGlowingDot className="bg-teal-500/80" />,
89
  failed: <IconCrossCircle className="text-red-500" />,
90
  };
91
  const ChunkTypeToTextDict: Record<ChunkBody['type'], string> = {
 
97
  const ChunkPayloadAction: React.FC<{
98
  payload: ChunkBody['payload'];
99
  }> = ({ payload }) => {
100
+ if (!payload) return null;
101
  if (Array.isArray(payload)) {
102
  // [{title: 123, content, 345}, {title: ..., content: ...}] => ['title', 'content']
103
  const keyArray = Array.from(
 
129
  {keyArray.map(header =>
130
  header === 'documentation' ? (
131
  <TableCell key={header}>
132
+ <Dialog>
133
+ <DialogTrigger asChild>
134
  <Button
135
  variant="ghost"
136
  size="icon"
 
138
  >
139
  <IconTerminalWindow className="text-teal-500 size-4" />
140
  </Button>
141
+ </DialogTrigger>
142
+ <DialogContent className="max-w-5xl">
143
  <CodeBlock language="md" value={line[header]} />
144
+ </DialogContent>
145
+ </Dialog>
146
  </TableCell>
147
  ) : (
148
  <TableCell key={header}>{line[header]}</TableCell>
 
195
  <div className="rounded-lg overflow-hidden relative max-w-5xl">
196
  <CodeBlock language="python" value={code} />
197
  <div className="rounded-lg relative">
 
198
  <div className="absolute left-1/2 -translate-x-1/2 -top-4 z-10">
199
+ <Dialog>
200
+ <DialogTrigger asChild>
201
  <Button variant="ghost" size="icon" className="size-8">
202
  <IconTerminalWindow className="text-teal-500 size-4" />
203
  </Button>
204
+ </DialogTrigger>
205
+ <DialogContent className="max-w-5xl">
206
  <CodeBlock language="python" value={test} />
207
+ </DialogContent>
208
+ </Dialog>
 
 
 
 
 
 
 
 
 
 
 
 
209
  {Array.isArray(stderr) && (
210
+ <Dialog>
211
+ <DialogTrigger asChild>
212
  <Button variant="ghost" size="icon" className="size-8">
213
  <IconLog className="text-gray-500 size-4" />
214
  </Button>
215
+ </DialogTrigger>
216
+ <DialogContent className="max-w-5xl">
217
  <CodeBlock language="vim" value={stderr.join('').trim()} />
218
+ </DialogContent>
219
+ </Dialog>
220
  )}
221
  </div>
222
  </div>
223
+ {Array.isArray(stdout) && !!stdout.join('').trim() && (
224
+ <>
225
+ <Separator />
226
+ <CodeBlock language="print" value={stdout.join('').trim()} />
227
+ </>
228
+ )}
229
+ {!!results.length && (
230
+ <>
231
+ <Separator />
232
+ <CodeBlock language="output" value={results} />
233
+ </>
234
+ )}
235
+ <Separator />
236
  </div>
237
  );
238
  };
 
246
  );
247
 
248
  return (
249
+ <div className="group relative mb-6 flex rounded-md bg-muted p-6 w-full">
250
  <div className="flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-primary text-primary-foreground">
251
  <IconLandingAI />
252
  </div>
 
254
  <Table className="w-[400px]">
255
  <TableBody>
256
  {formattedSections.map(section => (
257
+ <TableRow
258
+ className="border-primary/50 h-[56px]"
259
+ key={section.type}
260
+ >
261
+ <TableCell className="text-center text-webkit-center">
262
+ {ChunkStatusToIconDict[section.status]}
263
+ </TableCell>
264
  <TableCell className="font-medium">
265
  {ChunkTypeToTextDict[section.type]}
266
  </TableCell>
components/chat/MemoizedReactMarkdown.tsx CHANGED
@@ -1,5 +1,11 @@
1
  import { FC, memo } from 'react';
2
  import ReactMarkdown, { Options } from 'react-markdown';
 
 
 
 
 
 
3
 
4
  export const MemoizedReactMarkdown: FC<Options> = memo(
5
  ReactMarkdown,
@@ -7,3 +13,62 @@ export const MemoizedReactMarkdown: FC<Options> = memo(
7
  prevProps.children === nextProps.children &&
8
  prevProps.className === nextProps.className,
9
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { FC, memo } from 'react';
2
  import ReactMarkdown, { Options } from 'react-markdown';
3
+ import Img from '../ui/Img';
4
+
5
+ import remarkGfm from 'remark-gfm';
6
+ import remarkMath from 'remark-math';
7
+ import rehypeRaw from 'rehype-raw';
8
+ import { CodeBlock } from '../ui/CodeBlock';
9
 
10
  export const MemoizedReactMarkdown: FC<Options> = memo(
11
  ReactMarkdown,
 
13
  prevProps.children === nextProps.children &&
14
  prevProps.className === nextProps.className,
15
  );
16
+
17
+ export const Markdown: React.FC<{
18
+ content: string;
19
+ }> = ({ content }) => {
20
+ return (
21
+ <>
22
+ <MemoizedReactMarkdown
23
+ className="break-words overflow-auto"
24
+ remarkPlugins={[remarkGfm, remarkMath]}
25
+ rehypePlugins={[rehypeRaw] as any}
26
+ components={{
27
+ p({ children, ...props }) {
28
+ if (
29
+ props.node.children.some(
30
+ child => child.type === 'element' && child.tagName === 'img',
31
+ )
32
+ ) {
33
+ return (
34
+ <p className="flex flex-wrap gap-2 items-start">{children}</p>
35
+ );
36
+ }
37
+ return <p className="mb-2 whitespace-pre-line">{children}</p>;
38
+ },
39
+ img(props) {
40
+ if (props.src?.endsWith('.mp4')) {
41
+ return (
42
+ <video src={props.src} controls width={500} height={500} />
43
+ );
44
+ }
45
+ return (
46
+ <Img
47
+ src={props.src ?? '/landing.png'}
48
+ alt={props.alt ?? 'answer-image'}
49
+ quality={100}
50
+ sizes="(min-width: 66em) 15vw,
51
+ (min-width: 44em) 20vw,
52
+ 100vw"
53
+ />
54
+ );
55
+ },
56
+ code({ node, inline, className, children, ...props }) {
57
+ const match = /language-(\w+)/.exec(className || '');
58
+
59
+ return (
60
+ <CodeBlock
61
+ key={Math.random()}
62
+ language={(match && match[1]) || ''}
63
+ value={String(children).replace(/\n$/, '')}
64
+ {...props}
65
+ />
66
+ );
67
+ },
68
+ }}
69
+ >
70
+ {content}
71
+ </MemoizedReactMarkdown>
72
+ </>
73
+ );
74
+ };
components/ui/Dialog.tsx CHANGED
@@ -29,10 +29,10 @@ const DialogOverlay = React.forwardRef<
29
  ));
30
  DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31
 
32
- const DialogContent = React.forwardRef<
33
- React.ElementRef<typeof DialogPrimitive.Content>,
34
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35
- >(({ className, children, ...props }, ref) => (
36
  <DialogPortal>
37
  <DialogOverlay />
38
  <DialogPrimitive.Content
 
29
  ));
30
  DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31
 
32
+ const DialogContent: React.ForwardRefExoticComponent<
33
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &
34
+ React.RefAttributes<any>
35
+ > = React.forwardRef(({ className, children, ...props }, ref) => (
36
  <DialogPortal>
37
  <DialogOverlay />
38
  <DialogPrimitive.Content
components/ui/Icons.tsx CHANGED
@@ -740,8 +740,8 @@ function IconLog({ className, ...props }: React.ComponentProps<'svg'>) {
740
  {...props}
741
  >
742
  <path
743
- fill-rule="evenodd"
744
- clip-rule="evenodd"
745
  d="M3 2.5C3 2.22386 3.22386 2 3.5 2H9.08579C9.21839 2 9.34557 2.05268 9.43934 2.14645L11.8536 4.56066C11.9473 4.65443 12 4.78161 12 4.91421V12.5C12 12.7761 11.7761 13 11.5 13H3.5C3.22386 13 3 12.7761 3 12.5V2.5ZM3.5 1C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V4.91421C13 4.51639 12.842 4.13486 12.5607 3.85355L10.1464 1.43934C9.86514 1.15804 9.48361 1 9.08579 1H3.5ZM4.5 4C4.22386 4 4 4.22386 4 4.5C4 4.77614 4.22386 5 4.5 5H7.5C7.77614 5 8 4.77614 8 4.5C8 4.22386 7.77614 4 7.5 4H4.5ZM4.5 7C4.22386 7 4 7.22386 4 7.5C4 7.77614 4.22386 8 4.5 8H10.5C10.7761 8 11 7.77614 11 7.5C11 7.22386 10.7761 7 10.5 7H4.5ZM4.5 10C4.22386 10 4 10.2239 4 10.5C4 10.7761 4.22386 11 4.5 11H10.5C10.7761 11 11 10.7761 11 10.5C11 10.2239 10.7761 10 10.5 10H4.5Z"
746
  fill="currentColor"
747
  />
@@ -760,8 +760,8 @@ function IconOutput({ className, ...props }: React.ComponentProps<'svg'>) {
760
  {...props}
761
  >
762
  <path
763
- fill-rule="evenodd"
764
- clip-rule="evenodd"
765
  d="M5 2V1H10V2H5ZM4.75 0C4.33579 0 4 0.335786 4 0.75V1H3.5C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V2.5C13 1.67157 12.3284 1 11.5 1H11V0.75C11 0.335786 10.6642 0 10.25 0H4.75ZM11 2V2.25C11 2.66421 10.6642 3 10.25 3H4.75C4.33579 3 4 2.66421 4 2.25V2H3.5C3.22386 2 3 2.22386 3 2.5V12.5C3 12.7761 3.22386 13 3.5 13H11.5C11.7761 13 12 12.7761 12 12.5V2.5C12 2.22386 11.7761 2 11.5 2H11Z"
766
  fill="currentColor"
767
  />
@@ -769,6 +769,10 @@ function IconOutput({ className, ...props }: React.ComponentProps<'svg'>) {
769
  );
770
  }
771
 
 
 
 
 
772
  export {
773
  IconEdit,
774
  IconLandingAI,
@@ -811,4 +815,5 @@ export {
811
  IconListUnordered,
812
  IconLog,
813
  IconOutput,
 
814
  };
 
740
  {...props}
741
  >
742
  <path
743
+ fillRule="evenodd"
744
+ clipRule="evenodd"
745
  d="M3 2.5C3 2.22386 3.22386 2 3.5 2H9.08579C9.21839 2 9.34557 2.05268 9.43934 2.14645L11.8536 4.56066C11.9473 4.65443 12 4.78161 12 4.91421V12.5C12 12.7761 11.7761 13 11.5 13H3.5C3.22386 13 3 12.7761 3 12.5V2.5ZM3.5 1C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V4.91421C13 4.51639 12.842 4.13486 12.5607 3.85355L10.1464 1.43934C9.86514 1.15804 9.48361 1 9.08579 1H3.5ZM4.5 4C4.22386 4 4 4.22386 4 4.5C4 4.77614 4.22386 5 4.5 5H7.5C7.77614 5 8 4.77614 8 4.5C8 4.22386 7.77614 4 7.5 4H4.5ZM4.5 7C4.22386 7 4 7.22386 4 7.5C4 7.77614 4.22386 8 4.5 8H10.5C10.7761 8 11 7.77614 11 7.5C11 7.22386 10.7761 7 10.5 7H4.5ZM4.5 10C4.22386 10 4 10.2239 4 10.5C4 10.7761 4.22386 11 4.5 11H10.5C10.7761 11 11 10.7761 11 10.5C11 10.2239 10.7761 10 10.5 10H4.5Z"
746
  fill="currentColor"
747
  />
 
760
  {...props}
761
  >
762
  <path
763
+ fillRule="evenodd"
764
+ clipRule="evenodd"
765
  d="M5 2V1H10V2H5ZM4.75 0C4.33579 0 4 0.335786 4 0.75V1H3.5C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V2.5C13 1.67157 12.3284 1 11.5 1H11V0.75C11 0.335786 10.6642 0 10.25 0H4.75ZM11 2V2.25C11 2.66421 10.6642 3 10.25 3H4.75C4.33579 3 4 2.66421 4 2.25V2H3.5C3.22386 2 3 2.22386 3 2.5V12.5C3 12.7761 3.22386 13 3.5 13H11.5C11.7761 13 12 12.7761 12 12.5V2.5C12 2.22386 11.7761 2 11.5 2H11Z"
766
  fill="currentColor"
767
  />
 
769
  );
770
  }
771
 
772
+ function IconGlowingDot({ className, ...props }: React.ComponentProps<'div'>) {
773
+ return <div className={cn('size-3 svg-shadow', className)} {...props} />;
774
+ }
775
+
776
  export {
777
  IconEdit,
778
  IconLandingAI,
 
815
  IconListUnordered,
816
  IconLog,
817
  IconOutput,
818
+ IconGlowingDot,
819
  };
components/ui/Table.tsx CHANGED
@@ -90,7 +90,10 @@ const TableCell = React.forwardRef<
90
  >(({ className, ...props }, ref) => (
91
  <td
92
  ref={ref}
93
- className={cn('p-2 align-middle [&:has([role=checkbox])]:pr-0', className)}
 
 
 
94
  {...props}
95
  />
96
  ));
 
90
  >(({ className, ...props }, ref) => (
91
  <td
92
  ref={ref}
93
+ className={cn(
94
+ 'py-2 px-4 align-middle [&:has([role=checkbox])]:pr-0',
95
+ className,
96
+ )}
97
  {...props}
98
  />
99
  ));
lib/hooks/useVisionAgent.ts CHANGED
@@ -1,7 +1,7 @@
1
  import { useChat } from 'ai/react';
2
  import { toast } from 'react-hot-toast';
3
  import { useEffect, useRef, useState } from 'react';
4
- import { ChatWithMessages, MessageUserInput } from '../types';
5
  import {
6
  dbPostCreateMessage,
7
  dbPostUpdateMessageResponse,
@@ -61,13 +61,15 @@ const useVisionAgent = (chat: ChatWithMessages) => {
61
  }, [isLoading, messages, reload]);
62
 
63
  return {
64
- messages,
65
  append: async (messageInput: MessageUserInput) => {
66
  currMediaUrl.current = messageInput.mediaUrl;
67
  append({
68
  id,
69
  role: 'user',
70
  content: messageInput.prompt,
 
 
71
  });
72
  const resp = await dbPostCreateMessage(id, messageInput);
73
  currMessageId.current = resp.id;
 
1
  import { useChat } from 'ai/react';
2
  import { toast } from 'react-hot-toast';
3
  import { useEffect, useRef, useState } from 'react';
4
+ import { ChatWithMessages, MessageUI, MessageUserInput } from '../types';
5
  import {
6
  dbPostCreateMessage,
7
  dbPostUpdateMessageResponse,
 
61
  }, [isLoading, messages, reload]);
62
 
63
  return {
64
+ messages: messages as MessageUI[],
65
  append: async (messageInput: MessageUserInput) => {
66
  currMediaUrl.current = messageInput.mediaUrl;
67
  append({
68
  id,
69
  role: 'user',
70
  content: messageInput.prompt,
71
+ // @ts-ignore valid when setting sendExtraMessageFields
72
+ mediaUrl: messageInput.mediaUrl,
73
  });
74
  const resp = await dbPostCreateMessage(id, messageInput);
75
  currMessageId.current = resp.id;
lib/types.ts CHANGED
@@ -8,7 +8,9 @@ export type MessageAssistantResponse = Partial<
8
  Pick<Message, 'response' | 'result'>
9
  >;
10
 
11
- export type MessageUI = Pick<MessageAI, 'role' | 'content' | 'id'>;
 
 
12
 
13
  export interface SignedPayload {
14
  id: string;
 
8
  Pick<Message, 'response' | 'result'>
9
  >;
10
 
11
+ export type MessageUI = Pick<MessageAI, 'role' | 'content' | 'id'> & {
12
+ mediaUrl?: string;
13
+ };
14
 
15
  export interface SignedPayload {
16
  id: string;
lib/utils/message.ts CHANGED
@@ -2,16 +2,6 @@ import { Message } from '@prisma/client';
2
  import { MessageAssistantResponse, MessageUI } from '../types';
3
  import { ChunkBody } from './content';
4
 
5
- const INPUT_PREFIX = 'input';
6
-
7
- const generateInputImageMarkdown = (url: string) => {
8
- if (url.toLowerCase().endsWith('.mp4')) {
9
- return `![${INPUT_PREFIX}](<${url}>)`;
10
- } else {
11
- return `![${INPUT_PREFIX}](<${url}>)`;
12
- }
13
- };
14
-
15
  /**
16
  * The Message we saved to database consists of a prompt and a response
17
  * for the UI to use, we need to break them to 2 messages, User and Assistant(if responded)
@@ -25,8 +15,8 @@ export const convertDBMessageToUIMessage = (
25
  acc.push({
26
  id: id + '-user',
27
  role: 'user',
28
- // add mediaUrl to the prompt
29
- content: prompt + '\n\n' + generateInputImageMarkdown(mediaUrl),
30
  });
31
  }
32
  if (response) {
 
2
  import { MessageAssistantResponse, MessageUI } from '../types';
3
  import { ChunkBody } from './content';
4
 
 
 
 
 
 
 
 
 
 
 
5
  /**
6
  * The Message we saved to database consists of a prompt and a response
7
  * for the UI to use, we need to break them to 2 messages, User and Assistant(if responded)
 
15
  acc.push({
16
  id: id + '-user',
17
  role: 'user',
18
+ content: prompt,
19
+ mediaUrl,
20
  });
21
  }
22
  if (response) {
package.json CHANGED
@@ -72,7 +72,7 @@
72
  "@types/react-window": "^1.8.8",
73
  "@types/uuid": "^9.0.8",
74
  "@typescript-eslint/parser": "^6.19.0",
75
- "autoprefixer": "^10.4.17",
76
  "eslint": "^8.56.0",
77
  "eslint-config-next": "14.1.0",
78
  "eslint-config-prettier": "^9.1.0",
 
72
  "@types/react-window": "^1.8.8",
73
  "@types/uuid": "^9.0.8",
74
  "@typescript-eslint/parser": "^6.19.0",
75
+ "autoprefixer": "^10.4.19",
76
  "eslint": "^8.56.0",
77
  "eslint-config-next": "14.1.0",
78
  "eslint-config-prettier": "^9.1.0",
pnpm-lock.yaml CHANGED
@@ -178,7 +178,7 @@ importers:
178
  specifier: ^6.19.0
179
  version: 6.21.0(eslint@8.57.0)(typescript@5.4.5)
180
  autoprefixer:
181
- specifier: ^10.4.17
182
  version: 10.4.19(postcss@8.4.38)
183
  eslint:
184
  specifier: ^8.56.0
@@ -1897,8 +1897,8 @@ packages:
1897
  eastasianwidth@0.2.0:
1898
  resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
1899
 
1900
- electron-to-chromium@1.4.744:
1901
- resolution: {integrity: sha512-nAGcF0yeKKfrP13LMFr5U1eghfFSvFLg302VUFzWlcjPOnUYd52yU5x6PBYrujhNbc4jYmZFrGZFK+xasaEzVA==}
1902
 
1903
  emoji-regex@8.0.0:
1904
  resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -3063,6 +3063,9 @@ packages:
3063
  picocolors@1.0.0:
3064
  resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
3065
 
 
 
 
3066
  picomatch@2.3.1:
3067
  resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
3068
  engines: {node: '>=8.6'}
@@ -3738,8 +3741,8 @@ packages:
3738
  unist-util-visit@5.0.0:
3739
  resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
3740
 
3741
- update-browserslist-db@1.0.13:
3742
- resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
3743
  hasBin: true
3744
  peerDependencies:
3745
  browserslist: '>= 4.21.0'
@@ -5920,9 +5923,9 @@ snapshots:
5920
  browserslist@4.23.0:
5921
  dependencies:
5922
  caniuse-lite: 1.0.30001611
5923
- electron-to-chromium: 1.4.744
5924
  node-releases: 2.0.14
5925
- update-browserslist-db: 1.0.13(browserslist@4.23.0)
5926
 
5927
  buffer@6.0.3:
5928
  dependencies:
@@ -6149,7 +6152,7 @@ snapshots:
6149
 
6150
  eastasianwidth@0.2.0: {}
6151
 
6152
- electron-to-chromium@1.4.744: {}
6153
 
6154
  emoji-regex@8.0.0: {}
6155
 
@@ -7650,6 +7653,8 @@ snapshots:
7650
 
7651
  picocolors@1.0.0: {}
7652
 
 
 
7653
  picomatch@2.3.1: {}
7654
 
7655
  pify@2.3.0: {}
@@ -8460,11 +8465,11 @@ snapshots:
8460
  unist-util-is: 6.0.0
8461
  unist-util-visit-parents: 6.0.1
8462
 
8463
- update-browserslist-db@1.0.13(browserslist@4.23.0):
8464
  dependencies:
8465
  browserslist: 4.23.0
8466
  escalade: 3.1.2
8467
- picocolors: 1.0.0
8468
 
8469
  uri-js@4.4.1:
8470
  dependencies:
 
178
  specifier: ^6.19.0
179
  version: 6.21.0(eslint@8.57.0)(typescript@5.4.5)
180
  autoprefixer:
181
+ specifier: ^10.4.19
182
  version: 10.4.19(postcss@8.4.38)
183
  eslint:
184
  specifier: ^8.56.0
 
1897
  eastasianwidth@0.2.0:
1898
  resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
1899
 
1900
+ electron-to-chromium@1.4.789:
1901
+ resolution: {integrity: sha512-0VbyiaXoT++Fi2vHGo2ThOeS6X3vgRCWrjPeO2FeIAWL6ItiSJ9BqlH8LfCXe3X1IdcG+S0iLoNaxQWhfZoGzQ==}
1902
 
1903
  emoji-regex@8.0.0:
1904
  resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
 
3063
  picocolors@1.0.0:
3064
  resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
3065
 
3066
+ picocolors@1.0.1:
3067
+ resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
3068
+
3069
  picomatch@2.3.1:
3070
  resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
3071
  engines: {node: '>=8.6'}
 
3741
  unist-util-visit@5.0.0:
3742
  resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
3743
 
3744
+ update-browserslist-db@1.0.16:
3745
+ resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==}
3746
  hasBin: true
3747
  peerDependencies:
3748
  browserslist: '>= 4.21.0'
 
5923
  browserslist@4.23.0:
5924
  dependencies:
5925
  caniuse-lite: 1.0.30001611
5926
+ electron-to-chromium: 1.4.789
5927
  node-releases: 2.0.14
5928
+ update-browserslist-db: 1.0.16(browserslist@4.23.0)
5929
 
5930
  buffer@6.0.3:
5931
  dependencies:
 
6152
 
6153
  eastasianwidth@0.2.0: {}
6154
 
6155
+ electron-to-chromium@1.4.789: {}
6156
 
6157
  emoji-regex@8.0.0: {}
6158
 
 
7653
 
7654
  picocolors@1.0.0: {}
7655
 
7656
+ picocolors@1.0.1: {}
7657
+
7658
  picomatch@2.3.1: {}
7659
 
7660
  pify@2.3.0: {}
 
8465
  unist-util-is: 6.0.0
8466
  unist-util-visit-parents: 6.0.1
8467
 
8468
+ update-browserslist-db@1.0.16(browserslist@4.23.0):
8469
  dependencies:
8470
  browserslist: 4.23.0
8471
  escalade: 3.1.2
8472
+ picocolors: 1.0.1
8473
 
8474
  uri-js@4.4.1:
8475
  dependencies: