Spaces:
Sleeping
Sleeping
MingruiZhang
commited on
feat: UI Improvements (#72)
Browse files1. 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 +21 -0
- components/chat/ChatMessage.tsx +68 -143
- components/chat/MemoizedReactMarkdown.tsx +65 -0
- components/ui/Dialog.tsx +4 -4
- components/ui/Icons.tsx +9 -4
- components/ui/Table.tsx +4 -1
- lib/hooks/useVisionAgent.ts +4 -2
- lib/types.ts +3 -1
- lib/utils/message.ts +2 -12
- package.json +1 -1
- pnpm-lock.yaml +15 -10
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 |
-
|
|
|
153 |
return (
|
154 |
-
<div className="group relative mb-6 flex rounded-md bg-muted p-
|
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-
|
159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
</div>
|
161 |
</div>
|
162 |
);
|
163 |
};
|
164 |
|
165 |
const ChunkStatusToIconDict: Record<ChunkBody['status'], React.ReactElement> = {
|
166 |
-
started: <
|
167 |
completed: <IconCheckCircle className="text-green-500" />,
|
168 |
-
running: <
|
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 |
-
<
|
212 |
-
<
|
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 |
-
</
|
221 |
-
<
|
222 |
<CodeBlock language="md" value={line[header]} />
|
223 |
-
</
|
224 |
-
</
|
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 |
-
<
|
280 |
-
<
|
281 |
<Button variant="ghost" size="icon" className="size-8">
|
282 |
<IconTerminalWindow className="text-teal-500 size-4" />
|
283 |
</Button>
|
284 |
-
</
|
285 |
-
<
|
286 |
<CodeBlock language="python" value={test} />
|
287 |
-
</
|
288 |
-
</
|
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 |
-
<
|
303 |
-
<
|
304 |
<Button variant="ghost" size="icon" className="size-8">
|
305 |
<IconLog className="text-gray-500 size-4" />
|
306 |
</Button>
|
307 |
-
</
|
308 |
-
<
|
309 |
<CodeBlock language="vim" value={stderr.join('').trim()} />
|
310 |
-
</
|
311 |
-
</
|
312 |
)}
|
313 |
</div>
|
314 |
</div>
|
315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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-
|
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
|
338 |
-
|
|
|
|
|
|
|
|
|
|
|
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
|
33 |
-
React.
|
34 |
-
|
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 |
-
|
744 |
-
|
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 |
-
|
764 |
-
|
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(
|
|
|
|
|
|
|
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 |
-
|
29 |
-
|
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.
|
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.
|
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.
|
1901 |
-
resolution: {integrity: sha512-
|
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.
|
3742 |
-
resolution: {integrity: sha512-
|
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.
|
5924 |
node-releases: 2.0.14
|
5925 |
-
update-browserslist-db: 1.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.
|
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.
|
8464 |
dependencies:
|
8465 |
browserslist: 4.23.0
|
8466 |
escalade: 3.1.2
|
8467 |
-
picocolors: 1.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:
|