Spaces:
Sleeping
Sleeping
MingruiZhang
commited on
Commit
•
c232e44
1
Parent(s):
ca7a659
feat: Assistant Chat (#68)
Browse files![image](https://github.com/landing-ai/vision-agent-ui/assets/5669963/6b8fe3d4-4b92-4869-926b-3811b0dca96e)
- app/chat/page.tsx +7 -0
- app/globals.css +0 -29
- components/chat/ChatClient.tsx +1 -1
- components/chat/ChatMessage.tsx +173 -56
- components/chat/Composer.tsx +1 -1
- components/ui/CodeBlock.tsx +1 -1
- components/ui/Dialog.tsx +4 -4
- components/ui/Icons.tsx +158 -0
- lib/hooks/useVisionAgent.ts +9 -3
- lib/messageUtils.ts +49 -0
app/chat/page.tsx
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { redirect } from 'next/navigation';
|
2 |
+
|
3 |
+
export interface PageProps {}
|
4 |
+
|
5 |
+
export default async function Page({}: PageProps) {
|
6 |
+
redirect('/');
|
7 |
+
}
|
app/globals.css
CHANGED
@@ -75,35 +75,6 @@
|
|
75 |
}
|
76 |
}
|
77 |
|
78 |
-
table {
|
79 |
-
border-spacing: 0;
|
80 |
-
border-collapse: collapse;
|
81 |
-
display: block;
|
82 |
-
margin-top: 0;
|
83 |
-
margin-bottom: 16px;
|
84 |
-
width: max-content;
|
85 |
-
max-width: 100%;
|
86 |
-
overflow: auto;
|
87 |
-
}
|
88 |
-
|
89 |
-
tr {
|
90 |
-
border-top: 1px solid #21262d;
|
91 |
-
}
|
92 |
-
|
93 |
-
td,
|
94 |
-
th {
|
95 |
-
padding: 6px 13px;
|
96 |
-
border: 1px solid #21262d;
|
97 |
-
}
|
98 |
-
|
99 |
-
th {
|
100 |
-
font-weight: 600;
|
101 |
-
}
|
102 |
-
|
103 |
-
table img {
|
104 |
-
background-color: transparent;
|
105 |
-
}
|
106 |
-
|
107 |
h1 {
|
108 |
font-size: 3.75rem; /* 48px */
|
109 |
font-family: var(--font-geist-sans);
|
|
|
75 |
}
|
76 |
}
|
77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
h1 {
|
79 |
font-size: 3.75rem; /* 48px */
|
80 |
font-family: var(--font-geist-sans);
|
components/chat/ChatClient.tsx
CHANGED
@@ -38,7 +38,7 @@ const ChatClient: React.FC<ChatClientProps> = ({ chat }) => {
|
|
38 |
className="h-full overflow-auto mx-auto w-[1024px] max-w-full border rounded-lg relative"
|
39 |
ref={scrollRef}
|
40 |
>
|
41 |
-
<div className="overflow-auto h-full pt-6 px-6" ref={messagesRef}>
|
42 |
{messages
|
43 |
// .filter(message => message.role !== 'system')
|
44 |
.map((message, index) => (
|
|
|
38 |
className="h-full overflow-auto mx-auto w-[1024px] max-w-full border rounded-lg relative"
|
39 |
ref={scrollRef}
|
40 |
>
|
41 |
+
<div className="overflow-auto h-full pt-6 px-6 z-10" ref={messagesRef}>
|
42 |
{messages
|
43 |
// .filter(message => message.role !== 'system')
|
44 |
.map((message, index) => (
|
components/chat/ChatMessage.tsx
CHANGED
@@ -9,25 +9,39 @@ 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
import Img from '../ui/Img';
|
15 |
-
import {
|
16 |
-
import Loading from '../ui/Loading';
|
17 |
import {
|
18 |
Table,
|
|
|
19 |
TableCell,
|
20 |
TableHead,
|
21 |
TableHeader,
|
22 |
TableRow,
|
23 |
} from '../ui/Table';
|
24 |
import { Button } from '../ui/Button';
|
25 |
-
import {
|
26 |
-
import {
|
27 |
-
|
|
|
|
|
|
|
|
|
|
|
28 |
|
29 |
export interface ChatMessageProps {
|
30 |
-
message:
|
31 |
isLoading: boolean;
|
32 |
}
|
33 |
|
@@ -82,9 +96,7 @@ const Markdown: React.FC<{
|
|
82 |
<p className="flex flex-wrap gap-2 items-start">{children}</p>
|
83 |
);
|
84 |
}
|
85 |
-
return
|
86 |
-
<p className="mb-2 last:mb-0 whitespace-pre-line">{children}</p>
|
87 |
-
);
|
88 |
},
|
89 |
img(props) {
|
90 |
if (props.src?.endsWith('.mp4')) {
|
@@ -104,24 +116,7 @@ const Markdown: React.FC<{
|
|
104 |
);
|
105 |
},
|
106 |
code({ node, inline, className, children, ...props }) {
|
107 |
-
// if (children.length) {
|
108 |
-
// if (children[0] == '▍') {
|
109 |
-
// return (
|
110 |
-
// <span className="mt-1 cursor-default animate-pulse">▍</span>
|
111 |
-
// );
|
112 |
-
// }
|
113 |
-
|
114 |
-
// children[0] = (children[0] as string).replace('`▍`', '▍');
|
115 |
-
// }
|
116 |
-
|
117 |
const match = /language-(\w+)/.exec(className || '');
|
118 |
-
// if (inline) {
|
119 |
-
// return (
|
120 |
-
// <code className={className} {...props}>
|
121 |
-
// {children}
|
122 |
-
// </code>
|
123 |
-
// );
|
124 |
-
// }
|
125 |
|
126 |
return (
|
127 |
<CodeBlock
|
@@ -141,39 +136,161 @@ const Markdown: React.FC<{
|
|
141 |
};
|
142 |
|
143 |
export function ChatMessage({ message, isLoading }: ChatMessageProps) {
|
144 |
-
const { content } =
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
|
|
|
|
|
|
|
|
|
|
151 |
return (
|
152 |
-
<div
|
153 |
-
className=
|
154 |
-
|
155 |
-
message.role === 'user' ? 'ml-auto mr-0 w-3/5' : 'w-4/5',
|
156 |
-
)}
|
157 |
-
>
|
158 |
-
<div
|
159 |
-
className={cn(
|
160 |
-
'flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow',
|
161 |
-
message.role === 'user'
|
162 |
-
? 'bg-background'
|
163 |
-
: 'bg-primary text-primary-foreground',
|
164 |
-
)}
|
165 |
-
>
|
166 |
-
{message.role === 'user' ? <IconUser /> : <IconLandingAI />}
|
167 |
</div>
|
168 |
<div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
|
169 |
-
{content && <Markdown content={content}
|
170 |
-
{isLoading && <Loading />}
|
171 |
</div>
|
172 |
-
|
173 |
-
|
174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
175 |
</DialogContent>
|
176 |
</Dialog>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
177 |
</div>
|
178 |
);
|
179 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
18 |
+
IconListUnordered,
|
19 |
+
IconTerminalWindow,
|
20 |
+
IconUser,
|
21 |
+
} from '@/components/ui/Icons';
|
22 |
+
import { MessageBase } from '../../lib/types';
|
23 |
import Img from '../ui/Img';
|
24 |
+
import { ChunkBody, CodeResult, formatStreamLogs } from '@/lib/messageUtils';
|
|
|
25 |
import {
|
26 |
Table,
|
27 |
+
TableBody,
|
28 |
TableCell,
|
29 |
TableHead,
|
30 |
TableHeader,
|
31 |
TableRow,
|
32 |
} from '../ui/Table';
|
33 |
import { Button } from '../ui/Button';
|
34 |
+
import { Separator } from '../ui/Separator';
|
35 |
+
import {
|
36 |
+
Tooltip,
|
37 |
+
TooltipContent,
|
38 |
+
TooltipTrigger,
|
39 |
+
} from '@/components/ui/Tooltip';
|
40 |
+
|
41 |
+
import { Dialog, DialogContent, DialogTrigger } from '../ui/Dialog';
|
42 |
|
43 |
export interface ChatMessageProps {
|
44 |
+
message: MessageBase;
|
45 |
isLoading: boolean;
|
46 |
}
|
47 |
|
|
|
96 |
<p className="flex flex-wrap gap-2 items-start">{children}</p>
|
97 |
);
|
98 |
}
|
99 |
+
return <p className="mb-2 whitespace-pre-line">{children}</p>;
|
|
|
|
|
100 |
},
|
101 |
img(props) {
|
102 |
if (props.src?.endsWith('.mp4')) {
|
|
|
116 |
);
|
117 |
},
|
118 |
code({ node, inline, className, children, ...props }) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
119 |
const match = /language-(\w+)/.exec(className || '');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
|
121 |
return (
|
122 |
<CodeBlock
|
|
|
136 |
};
|
137 |
|
138 |
export function ChatMessage({ message, isLoading }: ChatMessageProps) {
|
139 |
+
const { role, content } = message;
|
140 |
+
|
141 |
+
return role === 'user' ? (
|
142 |
+
<UserChatMessage content={content} />
|
143 |
+
) : (
|
144 |
+
<AssistantChatMessage content={content} />
|
145 |
+
);
|
146 |
+
}
|
147 |
+
|
148 |
+
const UserChatMessage: React.FC<{
|
149 |
+
content: string;
|
150 |
+
}> = ({ content }) => {
|
151 |
return (
|
152 |
+
<div className="group relative mb-6 flex rounded-md bg-muted p-4 ml-auto mr-0 w-3/5">
|
153 |
+
<div className="flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-background">
|
154 |
+
<IconUser />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
155 |
</div>
|
156 |
<div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
|
157 |
+
{content && <Markdown content={content} />}
|
|
|
158 |
</div>
|
159 |
+
</div>
|
160 |
+
);
|
161 |
+
};
|
162 |
+
|
163 |
+
const ChunkStatusToIconDict: Record<ChunkBody['status'], React.ReactElement> = {
|
164 |
+
started: <IconChevronDoubleRight className="text-cyan-500" />,
|
165 |
+
completed: <IconCheckCircle className="text-green-500" />,
|
166 |
+
running: <IconTerminalWindow className="text-teal-500" />,
|
167 |
+
failed: <IconCrossCircle className="text-red-500" />,
|
168 |
+
};
|
169 |
+
const ChunkTypeToTextDict: Record<ChunkBody['type'], string> = {
|
170 |
+
plans: 'Creating instructions',
|
171 |
+
tools: 'Retrieving tools',
|
172 |
+
code: 'Generating code',
|
173 |
+
final_code: 'Final result',
|
174 |
+
};
|
175 |
+
const ChunkPayloadAction: React.FC<{
|
176 |
+
payload: ChunkBody['payload'];
|
177 |
+
}> = ({ payload }) => {
|
178 |
+
if (Array.isArray(payload)) {
|
179 |
+
// [{title: 123, content, 345}, {title: ..., content: ...}] => ['title', 'content']
|
180 |
+
const keyArray = Array.from(
|
181 |
+
payload.reduce((acc, curr) => {
|
182 |
+
Object.keys(curr).forEach(key => acc.add(key));
|
183 |
+
return acc;
|
184 |
+
}, new Set<string>()),
|
185 |
+
);
|
186 |
+
|
187 |
+
return (
|
188 |
+
<Dialog>
|
189 |
+
<DialogTrigger asChild>
|
190 |
+
<Button variant="ghost" size="icon">
|
191 |
+
<IconListUnordered />
|
192 |
+
</Button>
|
193 |
+
</DialogTrigger>
|
194 |
+
<DialogContent className="max-w-5xl">
|
195 |
+
<Table className="border rounded-lg bg-zinc-700 overflow-hidden">
|
196 |
+
<TableHeader>
|
197 |
+
<TableRow className="border-primary/50">
|
198 |
+
{keyArray.map(header => (
|
199 |
+
<TableHead key={header}>{header}</TableHead>
|
200 |
+
))}
|
201 |
+
</TableRow>
|
202 |
+
</TableHeader>
|
203 |
+
<TableBody>
|
204 |
+
{payload.map((line, index) => (
|
205 |
+
<TableRow className="border-primary/50" key={index}>
|
206 |
+
{keyArray.map(header => (
|
207 |
+
<TableCell key={header}>{line[header]}</TableCell>
|
208 |
+
))}
|
209 |
+
</TableRow>
|
210 |
+
))}
|
211 |
+
</TableBody>
|
212 |
+
</Table>
|
213 |
</DialogContent>
|
214 |
</Dialog>
|
215 |
+
);
|
216 |
+
} else {
|
217 |
+
return (
|
218 |
+
<Dialog>
|
219 |
+
<DialogTrigger asChild>
|
220 |
+
<Button variant="ghost" size="icon">
|
221 |
+
<IconCodeWrap />
|
222 |
+
</Button>
|
223 |
+
</DialogTrigger>
|
224 |
+
<DialogContent className="max-w-5xl">
|
225 |
+
<CodeResultDisplay codeResult={payload as CodeResult} />
|
226 |
+
</DialogContent>
|
227 |
+
</Dialog>
|
228 |
+
);
|
229 |
+
}
|
230 |
+
};
|
231 |
+
|
232 |
+
const CodeResultDisplay: React.FC<{
|
233 |
+
codeResult: CodeResult;
|
234 |
+
}> = ({ codeResult }) => {
|
235 |
+
const { code, test, result } = codeResult;
|
236 |
+
return (
|
237 |
+
<div className="rounded-lg overflow-hidden relative max-w-5xl">
|
238 |
+
<CodeBlock language="python" value={code} />
|
239 |
+
<div className="rounded-lg relative">
|
240 |
+
<Separator />
|
241 |
+
<Tooltip>
|
242 |
+
<TooltipTrigger asChild>
|
243 |
+
<Button
|
244 |
+
variant="ghost"
|
245 |
+
size="icon"
|
246 |
+
className="size-8 absolute left-1/2 -translate-x-1/2 -top-4 z-10"
|
247 |
+
>
|
248 |
+
<IconTerminalWindow className="text-teal-500 size-4" />
|
249 |
+
</Button>
|
250 |
+
</TooltipTrigger>
|
251 |
+
<TooltipContent>
|
252 |
+
<CodeBlock language="python" value={test} />
|
253 |
+
</TooltipContent>
|
254 |
+
</Tooltip>
|
255 |
+
</div>
|
256 |
+
<CodeBlock language="output" value={result} />
|
257 |
</div>
|
258 |
);
|
259 |
+
};
|
260 |
+
|
261 |
+
const AssistantChatMessage: React.FC<{
|
262 |
+
content: string;
|
263 |
+
}> = ({ content }) => {
|
264 |
+
const [formattedSections, codeResult] = useMemo(
|
265 |
+
() => formatStreamLogs(content),
|
266 |
+
[content],
|
267 |
+
);
|
268 |
+
|
269 |
+
return (
|
270 |
+
<div className="group relative mb-6 flex rounded-md bg-muted p-4 w-full">
|
271 |
+
<div className="flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-primary text-primary-foreground">
|
272 |
+
<IconLandingAI />
|
273 |
+
</div>
|
274 |
+
<div className="flex-1 px-1 space-y-4 ml-4 overflow-hidden">
|
275 |
+
<Table className="border rounded-lg bg-zinc-700 overflow-hidden w-[400px]">
|
276 |
+
<TableBody>
|
277 |
+
{formattedSections.map(section => (
|
278 |
+
<TableRow className="border-primary/50" key={section.type}>
|
279 |
+
<TableCell>{ChunkStatusToIconDict[section.status]}</TableCell>
|
280 |
+
<TableCell className="font-medium">
|
281 |
+
{ChunkTypeToTextDict[section.type]}
|
282 |
+
</TableCell>
|
283 |
+
<TableCell className="text-right">
|
284 |
+
<ChunkPayloadAction payload={section.payload} />
|
285 |
+
</TableCell>
|
286 |
+
</TableRow>
|
287 |
+
))}
|
288 |
+
</TableBody>
|
289 |
+
</Table>
|
290 |
+
{codeResult && <CodeResultDisplay codeResult={codeResult} />}
|
291 |
+
</div>
|
292 |
+
</div>
|
293 |
+
);
|
294 |
+
};
|
295 |
+
|
296 |
+
export default UserChatMessage;
|
components/chat/Composer.tsx
CHANGED
@@ -75,7 +75,7 @@ const Composer = forwardRef<ComposerRef, ComposerProps>(
|
|
75 |
<div
|
76 |
{...getRootProps()}
|
77 |
className={cn(
|
78 |
-
'w-full mx-auto max-w-2xl px-6 py-4 bg-zinc-600 rounded-xl relative shadow-lg shadow-zinc-600/40',
|
79 |
isDragActive && 'bg-indigo-700/50',
|
80 |
)}
|
81 |
>
|
|
|
75 |
<div
|
76 |
{...getRootProps()}
|
77 |
className={cn(
|
78 |
+
'w-full mx-auto max-w-2xl px-6 py-4 bg-zinc-600 rounded-xl relative shadow-lg shadow-zinc-600/40 z-50',
|
79 |
isDragActive && 'bg-indigo-700/50',
|
80 |
)}
|
81 |
>
|
components/ui/CodeBlock.tsx
CHANGED
@@ -92,7 +92,7 @@ const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
|
92 |
copyToClipboard(value);
|
93 |
};
|
94 |
return (
|
95 |
-
<div className="relative w-full
|
96 |
<div className="flex items-center justify-between w-full pl-8 pr-4 pt-2 text-zinc-100">
|
97 |
<span className="text-xs lowercase">{language}</span>
|
98 |
<div className="flex items-center space-x-1">
|
|
|
92 |
copyToClipboard(value);
|
93 |
};
|
94 |
return (
|
95 |
+
<div className="relative w-full codeblock bg-zinc-900 overflow-hidden">
|
96 |
<div className="flex items-center justify-between w-full pl-8 pr-4 pt-2 text-zinc-100">
|
97 |
<span className="text-xs lowercase">{language}</span>
|
98 |
<div className="flex items-center space-x-1">
|
components/ui/Dialog.tsx
CHANGED
@@ -2,7 +2,7 @@
|
|
2 |
|
3 |
import * as React from 'react';
|
4 |
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
5 |
-
import {
|
6 |
|
7 |
import { cn } from '@/lib/utils';
|
8 |
|
@@ -38,14 +38,14 @@ const DialogContent = React.forwardRef<
|
|
38 |
<DialogPrimitive.Content
|
39 |
ref={ref}
|
40 |
className={cn(
|
41 |
-
'fixed left-
|
42 |
className,
|
43 |
)}
|
44 |
{...props}
|
45 |
>
|
46 |
{children}
|
47 |
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
48 |
-
<
|
49 |
<span className="sr-only">Close</span>
|
50 |
</DialogPrimitive.Close>
|
51 |
</DialogPrimitive.Content>
|
@@ -112,8 +112,8 @@ export {
|
|
112 |
Dialog,
|
113 |
DialogPortal,
|
114 |
DialogOverlay,
|
115 |
-
DialogClose,
|
116 |
DialogTrigger,
|
|
|
117 |
DialogContent,
|
118 |
DialogHeader,
|
119 |
DialogFooter,
|
|
|
2 |
|
3 |
import * as React from 'react';
|
4 |
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
5 |
+
import { Cross2Icon } from '@radix-ui/react-icons';
|
6 |
|
7 |
import { cn } from '@/lib/utils';
|
8 |
|
|
|
38 |
<DialogPrimitive.Content
|
39 |
ref={ref}
|
40 |
className={cn(
|
41 |
+
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
42 |
className,
|
43 |
)}
|
44 |
{...props}
|
45 |
>
|
46 |
{children}
|
47 |
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
48 |
+
<Cross2Icon className="size-4" />
|
49 |
<span className="sr-only">Close</span>
|
50 |
</DialogPrimitive.Close>
|
51 |
</DialogPrimitive.Content>
|
|
|
112 |
Dialog,
|
113 |
DialogPortal,
|
114 |
DialogOverlay,
|
|
|
115 |
DialogTrigger,
|
116 |
+
DialogClose,
|
117 |
DialogContent,
|
118 |
DialogHeader,
|
119 |
DialogFooter,
|
components/ui/Icons.tsx
CHANGED
@@ -198,6 +198,46 @@ function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
|
|
198 |
);
|
199 |
}
|
200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
201 |
function IconUser({ className, ...props }: React.ComponentProps<'svg'>) {
|
202 |
return (
|
203 |
<svg
|
@@ -577,6 +617,117 @@ function IconImage({ className, ...props }: React.ComponentProps<'svg'>) {
|
|
577 |
);
|
578 |
}
|
579 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
580 |
export {
|
581 |
IconEdit,
|
582 |
IconLandingAI,
|
@@ -610,4 +761,11 @@ export {
|
|
610 |
IconDiscord,
|
611 |
IconExclamationTriangle,
|
612 |
IconImage,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
613 |
};
|
|
|
198 |
);
|
199 |
}
|
200 |
|
201 |
+
function IconCheckCircle({ className, ...props }: React.ComponentProps<'svg'>) {
|
202 |
+
return (
|
203 |
+
<svg
|
204 |
+
height="16"
|
205 |
+
strokeLinejoin="round"
|
206 |
+
viewBox="0 0 16 16"
|
207 |
+
width="16"
|
208 |
+
className={cn('size-4', className)}
|
209 |
+
{...props}
|
210 |
+
>
|
211 |
+
<path
|
212 |
+
fillRule="evenodd"
|
213 |
+
clipRule="evenodd"
|
214 |
+
d="M14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8ZM16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM11.5303 6.53033L12.0607 6L11 4.93934L10.4697 5.46967L6.5 9.43934L5.53033 8.46967L5 7.93934L3.93934 9L4.46967 9.53033L5.96967 11.0303C6.26256 11.3232 6.73744 11.3232 7.03033 11.0303L11.5303 6.53033Z"
|
215 |
+
fill="currentColor"
|
216 |
+
></path>
|
217 |
+
</svg>
|
218 |
+
);
|
219 |
+
}
|
220 |
+
|
221 |
+
function IconCrossCircle({ className, ...props }: React.ComponentProps<'svg'>) {
|
222 |
+
return (
|
223 |
+
<svg
|
224 |
+
height="16"
|
225 |
+
stroke-linejoin="round"
|
226 |
+
viewBox="0 0 16 16"
|
227 |
+
width="16"
|
228 |
+
className={cn('size-4', className)}
|
229 |
+
{...props}
|
230 |
+
>
|
231 |
+
<path
|
232 |
+
fillRule="evenodd"
|
233 |
+
clipRule="evenodd"
|
234 |
+
d="M14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8ZM16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM5.5 11.5607L6.03033 11.0303L8 9.06066L9.96967 11.0303L10.5 11.5607L11.5607 10.5L11.0303 9.96967L9.06066 8L11.0303 6.03033L11.5607 5.5L10.5 4.43934L9.96967 4.96967L8 6.93934L6.03033 4.96967L5.5 4.43934L4.43934 5.5L4.96967 6.03033L6.93934 8L4.96967 9.96967L4.43934 10.5L5.5 11.5607Z"
|
235 |
+
fill="currentColor"
|
236 |
+
></path>
|
237 |
+
</svg>
|
238 |
+
);
|
239 |
+
}
|
240 |
+
|
241 |
function IconUser({ className, ...props }: React.ComponentProps<'svg'>) {
|
242 |
return (
|
243 |
<svg
|
|
|
617 |
);
|
618 |
}
|
619 |
|
620 |
+
function IconChevronRight({
|
621 |
+
className,
|
622 |
+
...props
|
623 |
+
}: React.ComponentProps<'svg'>) {
|
624 |
+
return (
|
625 |
+
<svg
|
626 |
+
height="16"
|
627 |
+
strokeLinejoin="round"
|
628 |
+
viewBox="0 0 16 16"
|
629 |
+
width="16"
|
630 |
+
className={cn('size-4', className)}
|
631 |
+
{...props}
|
632 |
+
>
|
633 |
+
<path
|
634 |
+
fillRule="evenodd"
|
635 |
+
clipRule="evenodd"
|
636 |
+
d="M5.50001 1.93933L6.03034 2.46966L10.8536 7.29288C11.2441 7.68341 11.2441 8.31657 10.8536 8.7071L6.03034 13.5303L5.50001 14.0607L4.43935 13L4.96968 12.4697L9.43935 7.99999L4.96968 3.53032L4.43935 2.99999L5.50001 1.93933Z"
|
637 |
+
fill="currentColor"
|
638 |
+
></path>
|
639 |
+
</svg>
|
640 |
+
);
|
641 |
+
}
|
642 |
+
|
643 |
+
function IconChevronDoubleRight({
|
644 |
+
className,
|
645 |
+
...props
|
646 |
+
}: React.ComponentProps<'svg'>) {
|
647 |
+
return (
|
648 |
+
<svg
|
649 |
+
height="16"
|
650 |
+
strokeLinejoin="round"
|
651 |
+
viewBox="0 0 16 16"
|
652 |
+
width="16"
|
653 |
+
className={cn('size-4', className)}
|
654 |
+
{...props}
|
655 |
+
>
|
656 |
+
<path
|
657 |
+
fillRule="evenodd"
|
658 |
+
clipRule="evenodd"
|
659 |
+
d="M12.8536 8.7071C13.2441 8.31657 13.2441 7.68341 12.8536 7.29288L9.03034 3.46966L8.50001 2.93933L7.43935 3.99999L7.96968 4.53032L11.4393 7.99999L7.96968 11.4697L7.43935 12L8.50001 13.0607L9.03034 12.5303L12.8536 8.7071ZM7.85356 8.7071C8.24408 8.31657 8.24408 7.68341 7.85356 7.29288L4.03034 3.46966L3.50001 2.93933L2.43935 3.99999L2.96968 4.53032L6.43935 7.99999L2.96968 11.4697L2.43935 12L3.50001 13.0607L4.03034 12.5303L7.85356 8.7071Z"
|
660 |
+
fill="currentColor"
|
661 |
+
></path>
|
662 |
+
</svg>
|
663 |
+
);
|
664 |
+
}
|
665 |
+
|
666 |
+
function IconTerminalWindow({
|
667 |
+
className,
|
668 |
+
...props
|
669 |
+
}: React.ComponentProps<'svg'>) {
|
670 |
+
return (
|
671 |
+
<svg
|
672 |
+
height="16"
|
673 |
+
strokeLinejoin="round"
|
674 |
+
viewBox="0 0 16 16"
|
675 |
+
width="16"
|
676 |
+
className={cn('size-4', className)}
|
677 |
+
{...props}
|
678 |
+
>
|
679 |
+
<path
|
680 |
+
fillRule="evenodd"
|
681 |
+
clipRule="evenodd"
|
682 |
+
d="M1.5 2.5H14.5V12.5C14.5 13.0523 14.0523 13.5 13.5 13.5H2.5C1.94772 13.5 1.5 13.0523 1.5 12.5V2.5ZM0 1H1.5H14.5H16V2.5V12.5C16 13.8807 14.8807 15 13.5 15H2.5C1.11929 15 0 13.8807 0 12.5V2.5V1ZM4 11.1339L4.44194 10.6919L6.51516 8.61872C6.85687 8.27701 6.85687 7.72299 6.51517 7.38128L4.44194 5.30806L4 4.86612L3.11612 5.75L3.55806 6.19194L5.36612 8L3.55806 9.80806L3.11612 10.25L4 11.1339ZM8 9.75494H8.6225H11.75H12.3725V10.9999H11.75H8.6225H8V9.75494Z"
|
683 |
+
fill="currentColor"
|
684 |
+
></path>
|
685 |
+
</svg>
|
686 |
+
);
|
687 |
+
}
|
688 |
+
|
689 |
+
function IconCodeWrap({ className, ...props }: React.ComponentProps<'svg'>) {
|
690 |
+
return (
|
691 |
+
<svg
|
692 |
+
height="16"
|
693 |
+
strokeLinejoin="round"
|
694 |
+
viewBox="0 0 16 16"
|
695 |
+
width="16"
|
696 |
+
className={cn('size-4', className)}
|
697 |
+
{...props}
|
698 |
+
>
|
699 |
+
<path
|
700 |
+
fillRule="evenodd"
|
701 |
+
clipRule="evenodd"
|
702 |
+
d="M7.22763 14.1819L10.2276 2.18193L10.4095 1.45432L8.95432 1.09052L8.77242 1.81812L5.77242 13.8181L5.59051 14.5457L7.04573 14.9095L7.22763 14.1819ZM3.75002 12.0607L3.21969 11.5304L0.39647 8.70713C0.00594559 8.31661 0.00594559 7.68344 0.39647 7.29292L3.21969 4.46969L3.75002 3.93936L4.81068 5.00002L4.28035 5.53035L1.81068 8.00003L4.28035 10.4697L4.81068 11L3.75002 12.0607ZM12.25 12.0607L12.7804 11.5304L15.6036 8.70713C15.9941 8.31661 15.9941 7.68344 15.6036 7.29292L12.7804 4.46969L12.25 3.93936L11.1894 5.00002L11.7197 5.53035L14.1894 8.00003L11.7197 10.4697L11.1894 11L12.25 12.0607Z"
|
703 |
+
fill="currentColor"
|
704 |
+
></path>
|
705 |
+
</svg>
|
706 |
+
);
|
707 |
+
}
|
708 |
+
|
709 |
+
function IconListUnordered({
|
710 |
+
className,
|
711 |
+
...props
|
712 |
+
}: React.ComponentProps<'svg'>) {
|
713 |
+
return (
|
714 |
+
<svg
|
715 |
+
height="16"
|
716 |
+
strokeLinejoin="round"
|
717 |
+
viewBox="0 0 16 16"
|
718 |
+
width="16"
|
719 |
+
className={cn('size-4', className)}
|
720 |
+
{...props}
|
721 |
+
>
|
722 |
+
<path
|
723 |
+
fillRule="evenodd"
|
724 |
+
clipRule="evenodd"
|
725 |
+
d="M2.5 4C3.19036 4 3.75 3.44036 3.75 2.75C3.75 2.05964 3.19036 1.5 2.5 1.5C1.80964 1.5 1.25 2.05964 1.25 2.75C1.25 3.44036 1.80964 4 2.5 4ZM2.5 9.25C3.19036 9.25 3.75 8.69036 3.75 8C3.75 7.30964 3.19036 6.75 2.5 6.75C1.80964 6.75 1.25 7.30964 1.25 8C1.25 8.69036 1.80964 9.25 2.5 9.25ZM3.75 13.25C3.75 13.9404 3.19036 14.5 2.5 14.5C1.80964 14.5 1.25 13.9404 1.25 13.25C1.25 12.5596 1.80964 12 2.5 12C3.19036 12 3.75 12.5596 3.75 13.25ZM6.75 2H6V3.5H6.75H14.25H15V2H14.25H6.75ZM6.75 7.25H6V8.75H6.75H14.25H15V7.25H14.25H6.75ZM6.75 12.5H6V14H6.75H14.25H15V12.5H14.25H6.75Z"
|
726 |
+
fill="currentColor"
|
727 |
+
></path>
|
728 |
+
</svg>
|
729 |
+
);
|
730 |
+
}
|
731 |
export {
|
732 |
IconEdit,
|
733 |
IconLandingAI,
|
|
|
761 |
IconDiscord,
|
762 |
IconExclamationTriangle,
|
763 |
IconImage,
|
764 |
+
IconCheckCircle,
|
765 |
+
IconCrossCircle,
|
766 |
+
IconChevronRight,
|
767 |
+
IconChevronDoubleRight,
|
768 |
+
IconTerminalWindow,
|
769 |
+
IconCodeWrap,
|
770 |
+
IconListUnordered,
|
771 |
};
|
lib/hooks/useVisionAgent.ts
CHANGED
@@ -1,7 +1,6 @@
|
|
1 |
import { useChat, UseChatHelpers } from 'ai/react';
|
2 |
import { toast } from 'react-hot-toast';
|
3 |
-
import { useEffect } from 'react';
|
4 |
-
import { useSearchParams } from 'next/navigation';
|
5 |
import { ChatWithMessages } from '../db/types';
|
6 |
import { dbPostCreateMessage } from '../db/functions';
|
7 |
import {
|
@@ -42,8 +41,15 @@ const useVisionAgent = (chat: ChatWithMessages) => {
|
|
42 |
/**
|
43 |
* If case this is first time user navigated with init message, we need to reload the chat for the first response
|
44 |
*/
|
|
|
45 |
useEffect(() => {
|
46 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
reload();
|
48 |
}
|
49 |
}, [isLoading, messages, reload]);
|
|
|
1 |
import { useChat, UseChatHelpers } from 'ai/react';
|
2 |
import { toast } from 'react-hot-toast';
|
3 |
+
import { useEffect, useRef } from 'react';
|
|
|
4 |
import { ChatWithMessages } from '../db/types';
|
5 |
import { dbPostCreateMessage } from '../db/functions';
|
6 |
import {
|
|
|
41 |
/**
|
42 |
* If case this is first time user navigated with init message, we need to reload the chat for the first response
|
43 |
*/
|
44 |
+
const once = useRef(true);
|
45 |
useEffect(() => {
|
46 |
+
if (
|
47 |
+
!isLoading &&
|
48 |
+
messages.length === 1 &&
|
49 |
+
messages[0].role === 'user' &&
|
50 |
+
once.current
|
51 |
+
) {
|
52 |
+
once.current = false;
|
53 |
reload();
|
54 |
}
|
55 |
}, [isLoading, messages, reload]);
|
lib/messageUtils.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
|
|
1 |
import type { ChatMessage } from '@/lib/db/types';
|
2 |
import { Message } from 'ai';
|
3 |
|
@@ -325,3 +326,51 @@ export const convertMessageToDbMessage = (
|
|
325 |
result,
|
326 |
};
|
327 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import toast from 'react-hot-toast';
|
2 |
import type { ChatMessage } from '@/lib/db/types';
|
3 |
import { Message } from 'ai';
|
4 |
|
|
|
326 |
result,
|
327 |
};
|
328 |
};
|
329 |
+
|
330 |
+
export type CodeResult = {
|
331 |
+
code: string;
|
332 |
+
test: string;
|
333 |
+
result: string;
|
334 |
+
};
|
335 |
+
|
336 |
+
export type ChunkBody = {
|
337 |
+
type: 'plans' | 'tools' | 'code' | 'final_code';
|
338 |
+
status: 'started' | 'completed' | 'failed' | 'running';
|
339 |
+
payload: Array<Record<string, string>> | CodeResult;
|
340 |
+
};
|
341 |
+
|
342 |
+
/**
|
343 |
+
* Formats the stream logs and returns an array of grouped sections.
|
344 |
+
*
|
345 |
+
* @param content - The content of the stream logs.
|
346 |
+
* @returns An array of grouped sections and an optional final code result.
|
347 |
+
*/
|
348 |
+
export const formatStreamLogs = (
|
349 |
+
content: string,
|
350 |
+
): [ChunkBody[], CodeResult?] => {
|
351 |
+
const streamLogs = content.split('\n').filter(log => !!log);
|
352 |
+
|
353 |
+
let parsedStreamLogs: ChunkBody[] = [];
|
354 |
+
try {
|
355 |
+
parsedStreamLogs = streamLogs.map(streamLog => JSON.parse(streamLog));
|
356 |
+
} catch {
|
357 |
+
toast.error('Error parsing stream logs');
|
358 |
+
return [[], undefined];
|
359 |
+
}
|
360 |
+
|
361 |
+
// Merge consecutive logs of the same type to the latest status
|
362 |
+
const groupedSections = parsedStreamLogs.reduce((acc, curr) => {
|
363 |
+
if (acc.length > 0 && acc[acc.length - 1].type === curr.type) {
|
364 |
+
acc[acc.length - 1] = curr;
|
365 |
+
} else {
|
366 |
+
acc.push(curr);
|
367 |
+
}
|
368 |
+
return acc;
|
369 |
+
}, [] as ChunkBody[]);
|
370 |
+
|
371 |
+
return [
|
372 |
+
groupedSections.filter(section => section.type !== 'final_code'),
|
373 |
+
groupedSections.find(section => section.type === 'final_code')
|
374 |
+
?.payload as CodeResult,
|
375 |
+
];
|
376 |
+
};
|