Spaces:
Running
Running
MingruiZhang
commited on
Commit
•
4af6326
1
Parent(s):
f8ea050
feat: Directly us DBMessage as UIMessage + Code viewer (#74)
Browse files![image](https://github.com/landing-ai/vision-agent-ui/assets/5669963/b9bcf3d6-ef97-437a-a17c-f40dba5f8a43)
- app/api/vision-agent/route.ts +2 -4
- app/chat/[id]/page.tsx +8 -2
- components/chat/ChatServer.tsx → app/chat/[id]/server.tsx +2 -2
- app/layout.tsx +1 -1
- app/project/[projectId]/page.tsx +0 -33
- components/ChatInterface.tsx +39 -0
- components/CodeResultDisplay.tsx +123 -0
- components/chat/ChatClient.tsx +0 -85
- components/chat/ChatList.tsx +90 -50
- components/chat/ChatMessage.tsx +83 -188
- components/chat/Composer.tsx +1 -1
- components/chat/LoginPrompt.tsx +25 -0
- components/project/ClassBar.tsx +0 -22
- components/project/MediaGrid.tsx +0 -18
- components/project/MediaTile.tsx +0 -53
- components/ui/Card.tsx +83 -0
- components/ui/CodeBlock.tsx +6 -1
- components/ui/Icons.tsx +2 -0
- lib/db/functions.ts +8 -1
- lib/db/prisma.ts +12 -12
- lib/hooks/useVisionAgent.ts +6 -2
- lib/types.ts +13 -0
- lib/utils/content.ts +2 -29
- state/chat.ts +1 -1
- state/media.ts +0 -3
app/api/vision-agent/route.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
import { StreamingTextResponse, experimental_StreamData } from 'ai';
|
2 |
|
3 |
// import { auth } from '@/auth';
|
4 |
-
import { MessageUI, SignedPayload } from '@/lib/types';
|
5 |
|
6 |
import { logger, withLogging } from '@/lib/logger';
|
7 |
import { CLEANED_SEPARATOR } from '@/lib/constants';
|
@@ -169,9 +169,7 @@ export const POST = withLogging(
|
|
169 |
if (msg.type !== 'final_code') {
|
170 |
return line;
|
171 |
}
|
172 |
-
const result = JSON.parse(
|
173 |
-
msg.payload.result,
|
174 |
-
) as PrismaJson.FinalChatResult['payload']['result'];
|
175 |
for (let index = 0; index < result.results.length; index++) {
|
176 |
const png = result.results[index].png ?? '';
|
177 |
const mp4 = result.results[index].mp4 ?? '';
|
|
|
1 |
import { StreamingTextResponse, experimental_StreamData } from 'ai';
|
2 |
|
3 |
// import { auth } from '@/auth';
|
4 |
+
import { MessageUI, ResultPayload, SignedPayload } from '@/lib/types';
|
5 |
|
6 |
import { logger, withLogging } from '@/lib/logger';
|
7 |
import { CLEANED_SEPARATOR } from '@/lib/constants';
|
|
|
169 |
if (msg.type !== 'final_code') {
|
170 |
return line;
|
171 |
}
|
172 |
+
const result = JSON.parse(msg.payload.result) as ResultPayload;
|
|
|
|
|
173 |
for (let index = 0; index < result.results.length; index++) {
|
174 |
const png = result.results[index].png ?? '';
|
175 |
const mp4 = result.results[index].mp4 ?? '';
|
app/chat/[id]/page.tsx
CHANGED
@@ -1,6 +1,8 @@
|
|
1 |
import { Suspense } from 'react';
|
2 |
-
import ChatServer from '
|
3 |
import Loading from '@/components/ui/Loading';
|
|
|
|
|
4 |
|
5 |
interface PageProps {
|
6 |
params: {
|
@@ -10,6 +12,7 @@ interface PageProps {
|
|
10 |
|
11 |
export default async function Page({ params }: PageProps) {
|
12 |
const { id: chatId } = params;
|
|
|
13 |
return (
|
14 |
<Suspense
|
15 |
fallback={
|
@@ -18,7 +21,10 @@ export default async function Page({ params }: PageProps) {
|
|
18 |
</div>
|
19 |
}
|
20 |
>
|
21 |
-
<
|
|
|
|
|
|
|
22 |
</Suspense>
|
23 |
);
|
24 |
}
|
|
|
1 |
import { Suspense } from 'react';
|
2 |
+
import ChatServer from './server';
|
3 |
import Loading from '@/components/ui/Loading';
|
4 |
+
import { auth } from '@/auth';
|
5 |
+
import { LoginPrompt } from '@/components/chat/LoginPrompt';
|
6 |
|
7 |
interface PageProps {
|
8 |
params: {
|
|
|
12 |
|
13 |
export default async function Page({ params }: PageProps) {
|
14 |
const { id: chatId } = params;
|
15 |
+
const session = await auth();
|
16 |
return (
|
17 |
<Suspense
|
18 |
fallback={
|
|
|
21 |
</div>
|
22 |
}
|
23 |
>
|
24 |
+
<div className="w-[1600px] max-w-full mx-auto flex flex-col space-y-4 items-center">
|
25 |
+
{!session && <LoginPrompt />}
|
26 |
+
<ChatServer id={chatId} />
|
27 |
+
</div>
|
28 |
</Suspense>
|
29 |
);
|
30 |
}
|
components/chat/ChatServer.tsx → app/chat/[id]/server.tsx
RENAMED
@@ -1,4 +1,4 @@
|
|
1 |
-
import
|
2 |
import { auth } from '@/auth';
|
3 |
import { dbGetChat } from '@/lib/db/functions';
|
4 |
import { redirect } from 'next/navigation';
|
@@ -15,5 +15,5 @@ export default async function ChatServer({ id }: ChatServerProps) {
|
|
15 |
revalidatePath('/');
|
16 |
redirect('/');
|
17 |
}
|
18 |
-
return <
|
19 |
}
|
|
|
1 |
+
import ChatInterface from '../../../components/ChatInterface';
|
2 |
import { auth } from '@/auth';
|
3 |
import { dbGetChat } from '@/lib/db/functions';
|
4 |
import { redirect } from 'next/navigation';
|
|
|
15 |
revalidatePath('/');
|
16 |
redirect('/');
|
17 |
}
|
18 |
+
return <ChatInterface chat={chat} />;
|
19 |
}
|
app/layout.tsx
CHANGED
@@ -53,7 +53,7 @@ export default function RootLayout(props: RootLayoutProps) {
|
|
53 |
>
|
54 |
<div className="flex flex-col min-h-screen">
|
55 |
<Header />
|
56 |
-
<main className="flex
|
57 |
{children}
|
58 |
</main>
|
59 |
</div>
|
|
|
53 |
>
|
54 |
<div className="flex flex-col min-h-screen">
|
55 |
<Header />
|
56 |
+
<main className="flex p-4 h-[calc(100vh-64px)] bg-background overflow-hidden relative w-screen">
|
57 |
{children}
|
58 |
</main>
|
59 |
</div>
|
app/project/[projectId]/page.tsx
DELETED
@@ -1,33 +0,0 @@
|
|
1 |
-
import MediaGrid from '@/components/project/MediaGrid';
|
2 |
-
import { fetchProjectClass, fetchProjectMedia } from '@/lib/fetch';
|
3 |
-
import ProjectChat from '@/components/project/ProjectChat';
|
4 |
-
import ClassBar from '@/components/project/ClassBar';
|
5 |
-
|
6 |
-
interface PageProps {
|
7 |
-
params: {
|
8 |
-
projectId: string;
|
9 |
-
};
|
10 |
-
}
|
11 |
-
|
12 |
-
export default async function Page({ params }: PageProps) {
|
13 |
-
const { projectId } = params;
|
14 |
-
|
15 |
-
const [mediaList, classList] = await Promise.all([
|
16 |
-
fetchProjectMedia({ projectId: Number(projectId) }),
|
17 |
-
fetchProjectClass({ projectId: Number(projectId) }),
|
18 |
-
]);
|
19 |
-
|
20 |
-
return (
|
21 |
-
<div className="pt-4 md:pt-10 h-full">
|
22 |
-
<div className="flex h-full">
|
23 |
-
<div className="w-1/2 relative border-r border-gray-300 overflow-auto">
|
24 |
-
<ClassBar classList={classList} />
|
25 |
-
<MediaGrid mediaList={mediaList} />
|
26 |
-
</div>
|
27 |
-
<div className="w-1/2 relative overflow-auto">
|
28 |
-
<ProjectChat mediaList={mediaList} />
|
29 |
-
</div>
|
30 |
-
</div>
|
31 |
-
</div>
|
32 |
-
);
|
33 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/ChatInterface.tsx
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { ChatWithMessages } from '@/lib/types';
|
4 |
+
import React from 'react';
|
5 |
+
import ChatList from './chat/ChatList';
|
6 |
+
import { Card } from './ui/Card';
|
7 |
+
import { useAtom, useAtomValue } from 'jotai';
|
8 |
+
import { selectedMessageId } from '@/state/chat';
|
9 |
+
import CodeResultDisplay from './CodeResultDisplay';
|
10 |
+
|
11 |
+
export interface ChatInterfaceProps {
|
12 |
+
chat: ChatWithMessages;
|
13 |
+
}
|
14 |
+
|
15 |
+
const ChatInterface: React.FC<ChatInterfaceProps> = ({ chat }) => {
|
16 |
+
const messageId = useAtomValue(selectedMessageId);
|
17 |
+
const messageCodeResult = chat.messages.find(
|
18 |
+
message => message.id === messageId,
|
19 |
+
)?.result;
|
20 |
+
return (
|
21 |
+
<div className="relative flex overflow-hidden space-x-4 size-full">
|
22 |
+
<div
|
23 |
+
data-state={messageCodeResult?.payload ? 'open' : 'closed'}
|
24 |
+
className="pl-4 peer absolute right-0 inset-y-0 hidden translate-x-full data-[state=open]:translate-x-0 z-30 duration-300 ease-in-out xl:flex flex-col items-start xl:w-1/2 h-full dark:bg-zinc-950 overflow-auto"
|
25 |
+
>
|
26 |
+
{messageCodeResult?.payload && (
|
27 |
+
<Card className="w-full">
|
28 |
+
<CodeResultDisplay codeResult={messageCodeResult.payload} />
|
29 |
+
</Card>
|
30 |
+
)}
|
31 |
+
</div>
|
32 |
+
<div className="w-full flex justify-center overflow-auto pr-0 animate-in duration-300 ease-in-out peer-[[data-state=open]]:xl:pr-[50%]">
|
33 |
+
<ChatList chat={chat} />
|
34 |
+
</div>
|
35 |
+
</div>
|
36 |
+
);
|
37 |
+
};
|
38 |
+
|
39 |
+
export default ChatInterface;
|
components/CodeResultDisplay.tsx
ADDED
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
|
3 |
+
import { ChunkBody, CodeResult, formatStreamLogs } from '@/lib/utils/content';
|
4 |
+
import { CodeBlock } from './ui/CodeBlock';
|
5 |
+
import {
|
6 |
+
Dialog,
|
7 |
+
DialogTrigger,
|
8 |
+
DialogContent,
|
9 |
+
DialogHeader,
|
10 |
+
DialogTitle,
|
11 |
+
} from './ui/Dialog';
|
12 |
+
import { Button } from './ui/Button';
|
13 |
+
import { IconLog, IconTerminalWindow } from './ui/Icons';
|
14 |
+
import { Separator } from './ui/Separator';
|
15 |
+
import { ResultPayload } from '@/lib/types';
|
16 |
+
import Img from './ui/Img';
|
17 |
+
|
18 |
+
export interface CodeResultDisplayProps {}
|
19 |
+
|
20 |
+
const CodeResultDisplay: React.FC<{
|
21 |
+
codeResult: CodeResult;
|
22 |
+
}> = ({ codeResult }) => {
|
23 |
+
const { code, test, result } = codeResult;
|
24 |
+
const getDetail = () => {
|
25 |
+
if (!result) return {};
|
26 |
+
try {
|
27 |
+
const detail = JSON.parse(result) as ResultPayload;
|
28 |
+
return {
|
29 |
+
results: detail.results,
|
30 |
+
stderr: detail.logs.stderr,
|
31 |
+
stdout: detail.logs.stdout,
|
32 |
+
};
|
33 |
+
} catch {
|
34 |
+
return {};
|
35 |
+
}
|
36 |
+
};
|
37 |
+
|
38 |
+
const { results, stderr, stdout } = getDetail();
|
39 |
+
|
40 |
+
return (
|
41 |
+
<div className="rounded-lg overflow-hidden relative max-w-5xl">
|
42 |
+
<CodeBlock language="python" value={code} />
|
43 |
+
<div className="rounded-lg relative">
|
44 |
+
<div className="absolute left-1/2 -translate-x-1/2 -top-4 z-10">
|
45 |
+
<Dialog>
|
46 |
+
<DialogTrigger asChild>
|
47 |
+
<Button variant="ghost" size="icon" className="size-8">
|
48 |
+
<IconTerminalWindow className="text-teal-500 size-4" />
|
49 |
+
</Button>
|
50 |
+
</DialogTrigger>
|
51 |
+
<DialogContent className="max-w-5xl">
|
52 |
+
<DialogHeader>
|
53 |
+
<DialogTitle>Test Code</DialogTitle>
|
54 |
+
</DialogHeader>
|
55 |
+
<CodeBlock language="python" value={test} />
|
56 |
+
</DialogContent>
|
57 |
+
</Dialog>
|
58 |
+
{Array.isArray(stderr) && !!stderr.join('').trim() && (
|
59 |
+
<Dialog>
|
60 |
+
<DialogTrigger asChild>
|
61 |
+
<Button variant="ghost" size="icon" className="size-8">
|
62 |
+
<IconLog className="text-gray-500 size-4" />
|
63 |
+
</Button>
|
64 |
+
</DialogTrigger>
|
65 |
+
<DialogContent className="max-w-5xl">
|
66 |
+
<CodeBlock language="vim" value={stderr.join('').trim()} />
|
67 |
+
</DialogContent>
|
68 |
+
</Dialog>
|
69 |
+
)}
|
70 |
+
</div>
|
71 |
+
</div>
|
72 |
+
{Array.isArray(stdout) && !!stdout.join('').trim() && (
|
73 |
+
<>
|
74 |
+
<Separator />
|
75 |
+
<CodeBlock language="print" value={stdout.join('').trim()} />
|
76 |
+
</>
|
77 |
+
)}
|
78 |
+
{Array.isArray(results) && !!results.length && (
|
79 |
+
<>
|
80 |
+
<Separator />
|
81 |
+
{results.map((result, index) => {
|
82 |
+
if (result.png) {
|
83 |
+
return (
|
84 |
+
<Img
|
85 |
+
key={'png' + index}
|
86 |
+
src={result.png}
|
87 |
+
alt={'answer-image'}
|
88 |
+
quality={100}
|
89 |
+
sizes="(min-width: 66em) 15vw,
|
90 |
+
(min-width: 44em) 20vw,
|
91 |
+
100vw"
|
92 |
+
/>
|
93 |
+
);
|
94 |
+
} else if (result.mp4) {
|
95 |
+
return (
|
96 |
+
<video
|
97 |
+
key={'mp4' + index}
|
98 |
+
src={result.mp4}
|
99 |
+
controls
|
100 |
+
width={500}
|
101 |
+
height={500}
|
102 |
+
/>
|
103 |
+
);
|
104 |
+
} else if (result.text) {
|
105 |
+
return (
|
106 |
+
<CodeBlock
|
107 |
+
key={'text' + index}
|
108 |
+
language="output"
|
109 |
+
value={result.text}
|
110 |
+
/>
|
111 |
+
);
|
112 |
+
} else {
|
113 |
+
return null;
|
114 |
+
}
|
115 |
+
})}
|
116 |
+
</>
|
117 |
+
)}
|
118 |
+
<Separator />
|
119 |
+
</div>
|
120 |
+
);
|
121 |
+
};
|
122 |
+
|
123 |
+
export default CodeResultDisplay;
|
components/chat/ChatClient.tsx
DELETED
@@ -1,85 +0,0 @@
|
|
1 |
-
'use client';
|
2 |
-
|
3 |
-
// import { ChatList } from '@/components/chat/ChatList';
|
4 |
-
import Composer from '@/components/chat/Composer';
|
5 |
-
import useVisionAgent from '@/lib/hooks/useVisionAgent';
|
6 |
-
import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
|
7 |
-
import { Session } from 'next-auth';
|
8 |
-
import { useEffect } from 'react';
|
9 |
-
import { ChatWithMessages, MessageUserInput } from '@/lib/types';
|
10 |
-
import { ChatMessage } from './ChatMessage';
|
11 |
-
import { Button } from '../ui/Button';
|
12 |
-
import { cn } from '@/lib/utils';
|
13 |
-
import { IconArrowDown } from '../ui/Icons';
|
14 |
-
import { dbPostCreateMessage } from '@/lib/db/functions';
|
15 |
-
|
16 |
-
export interface ChatClientProps {
|
17 |
-
chat: ChatWithMessages;
|
18 |
-
}
|
19 |
-
|
20 |
-
export const SCROLL_BOTTOM = 120;
|
21 |
-
|
22 |
-
const ChatClient: React.FC<ChatClientProps> = ({ chat }) => {
|
23 |
-
const { id, messages: dbMessages } = chat;
|
24 |
-
const { messages, append, isLoading } = useVisionAgent(chat);
|
25 |
-
|
26 |
-
const { messagesRef, scrollRef, visibilityRef, isVisible, scrollToBottom } =
|
27 |
-
useScrollAnchor(SCROLL_BOTTOM);
|
28 |
-
|
29 |
-
// Scroll to bottom when messages are loading
|
30 |
-
useEffect(() => {
|
31 |
-
if (isLoading && messages.length) {
|
32 |
-
scrollToBottom();
|
33 |
-
}
|
34 |
-
}, [isLoading, scrollToBottom, messages]);
|
35 |
-
|
36 |
-
return (
|
37 |
-
<div
|
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) => (
|
45 |
-
<ChatMessage
|
46 |
-
key={index}
|
47 |
-
message={message}
|
48 |
-
isLoading={isLoading && index === messages.length - 1}
|
49 |
-
/>
|
50 |
-
))}
|
51 |
-
<div
|
52 |
-
className="w-full"
|
53 |
-
style={{ height: SCROLL_BOTTOM }}
|
54 |
-
ref={visibilityRef}
|
55 |
-
/>
|
56 |
-
</div>
|
57 |
-
<div className="absolute bottom-4 w-full">
|
58 |
-
<Composer
|
59 |
-
// Use the last message mediaUrl as the initial mediaUrl
|
60 |
-
initMediaUrl={dbMessages[dbMessages.length - 1]?.mediaUrl}
|
61 |
-
isLoading={isLoading}
|
62 |
-
onSubmit={async ({ input, mediaUrl: newMediaUrl }) => {
|
63 |
-
append({
|
64 |
-
prompt: input,
|
65 |
-
mediaUrl: newMediaUrl,
|
66 |
-
});
|
67 |
-
}}
|
68 |
-
/>
|
69 |
-
</div>
|
70 |
-
{/* Scroll to bottom Icon */}
|
71 |
-
<Button
|
72 |
-
size="icon"
|
73 |
-
className={cn(
|
74 |
-
'absolute bottom-3 right-3 transition-opacity duration-300 size-6',
|
75 |
-
isVisible ? 'opacity-0' : 'opacity-100',
|
76 |
-
)}
|
77 |
-
onClick={() => scrollToBottom()}
|
78 |
-
>
|
79 |
-
<IconArrowDown className="size-3" />
|
80 |
-
</Button>
|
81 |
-
</div>
|
82 |
-
);
|
83 |
-
};
|
84 |
-
|
85 |
-
export default ChatClient;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/chat/ChatList.tsx
CHANGED
@@ -1,63 +1,103 @@
|
|
1 |
'use client';
|
2 |
|
3 |
-
import {
|
4 |
-
import
|
5 |
-
import
|
|
|
6 |
import { Session } from 'next-auth';
|
7 |
-
import {
|
8 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
-
export interface
|
11 |
-
|
12 |
-
session: Session | null;
|
13 |
-
isLoading: boolean;
|
14 |
}
|
15 |
|
16 |
-
export
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
return (
|
18 |
-
<
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
</div>
|
25 |
-
<div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
|
26 |
-
{process.env.NEXT_PUBLIC_IS_HUGGING_FACE ? (
|
27 |
-
<p className="text-muted-foreground leading-normal">
|
28 |
-
Please visit and login into{' '}
|
29 |
-
<Link
|
30 |
-
href="https://va.landing.ai/"
|
31 |
-
target="_blank"
|
32 |
-
className="underline"
|
33 |
-
>
|
34 |
-
our landing website
|
35 |
-
</Link>{' '}
|
36 |
-
to save and revisit your chat history!
|
37 |
-
</p>
|
38 |
-
) : (
|
39 |
-
<p className="text-muted-foreground leading-normal">
|
40 |
-
Please{' '}
|
41 |
-
<Link href="/sign-in" className="underline">
|
42 |
-
log in
|
43 |
-
</Link>{' '}
|
44 |
-
to save and revisit your chat history!
|
45 |
-
</p>
|
46 |
-
)}
|
47 |
-
</div>
|
48 |
-
</div>
|
49 |
-
<Separator className="my-4" />
|
50 |
-
</>
|
51 |
-
)}
|
52 |
-
{messages
|
53 |
-
// .filter(message => message.role !== 'system')
|
54 |
-
.map((message, index) => (
|
55 |
<ChatMessage
|
56 |
key={index}
|
57 |
message={message}
|
58 |
-
|
|
|
|
|
|
|
|
|
59 |
/>
|
60 |
))}
|
61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
);
|
63 |
-
}
|
|
|
|
|
|
1 |
'use client';
|
2 |
|
3 |
+
// import { ChatList } from '@/components/chat/ChatList';
|
4 |
+
import Composer from '@/components/chat/Composer';
|
5 |
+
import useVisionAgent from '@/lib/hooks/useVisionAgent';
|
6 |
+
import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
|
7 |
import { Session } from 'next-auth';
|
8 |
+
import { useEffect } from 'react';
|
9 |
+
import { ChatWithMessages, MessageUserInput } from '@/lib/types';
|
10 |
+
import { ChatMessage } from './ChatMessage';
|
11 |
+
import { Button } from '../ui/Button';
|
12 |
+
import { cn } from '@/lib/utils';
|
13 |
+
import { IconArrowDown } from '../ui/Icons';
|
14 |
+
import { dbPostCreateMessage } from '@/lib/db/functions';
|
15 |
+
import { Card } from '../ui/Card';
|
16 |
+
import { useSetAtom } from 'jotai';
|
17 |
+
import { selectedMessageId } from '@/state/chat';
|
18 |
|
19 |
+
export interface ChatListProps {
|
20 |
+
chat: ChatWithMessages;
|
|
|
|
|
21 |
}
|
22 |
|
23 |
+
export const SCROLL_BOTTOM = 120;
|
24 |
+
|
25 |
+
const ChatList: React.FC<ChatListProps> = ({ chat }) => {
|
26 |
+
const { id, messages: dbMessages } = chat;
|
27 |
+
const { messages, append, isLoading } = useVisionAgent(chat);
|
28 |
+
|
29 |
+
const lastMessage = messages[messages.length - 1];
|
30 |
+
const lastDbMessage = dbMessages[dbMessages.length - 1];
|
31 |
+
const setMessageId = useSetAtom(selectedMessageId);
|
32 |
+
|
33 |
+
const { messagesRef, scrollRef, visibilityRef, isVisible, scrollToBottom } =
|
34 |
+
useScrollAnchor(SCROLL_BOTTOM);
|
35 |
+
|
36 |
+
// Scroll to bottom on init and highlight last message
|
37 |
+
useEffect(() => {
|
38 |
+
scrollToBottom();
|
39 |
+
if (lastDbMessage.result) {
|
40 |
+
setMessageId(lastDbMessage.id);
|
41 |
+
}
|
42 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
43 |
+
}, []);
|
44 |
+
|
45 |
+
// Scroll to bottom when messages are loading
|
46 |
+
useEffect(() => {
|
47 |
+
if (isLoading && messages.length) {
|
48 |
+
scrollToBottom();
|
49 |
+
}
|
50 |
+
}, [isLoading, scrollToBottom, messages]);
|
51 |
+
|
52 |
return (
|
53 |
+
<Card
|
54 |
+
className="size-full max-w-5xl overflow-auto relative"
|
55 |
+
ref={scrollRef}
|
56 |
+
>
|
57 |
+
<div className="overflow-auto h-full p-4 z-10" ref={messagesRef}>
|
58 |
+
{dbMessages.map((message, index) => (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
<ChatMessage
|
60 |
key={index}
|
61 |
message={message}
|
62 |
+
wipAssistantMessage={
|
63 |
+
isLoading && lastMessage.role === 'assistant'
|
64 |
+
? lastMessage
|
65 |
+
: undefined
|
66 |
+
}
|
67 |
/>
|
68 |
))}
|
69 |
+
<div
|
70 |
+
className="w-full"
|
71 |
+
style={{ height: SCROLL_BOTTOM }}
|
72 |
+
ref={visibilityRef}
|
73 |
+
/>
|
74 |
+
</div>
|
75 |
+
<div className="absolute bottom-4 w-full">
|
76 |
+
<Composer
|
77 |
+
// Use the last message mediaUrl as the initial mediaUrl
|
78 |
+
initMediaUrl={dbMessages[dbMessages.length - 1]?.mediaUrl}
|
79 |
+
isLoading={isLoading}
|
80 |
+
onSubmit={async ({ input, mediaUrl: newMediaUrl }) => {
|
81 |
+
append({
|
82 |
+
prompt: input,
|
83 |
+
mediaUrl: newMediaUrl,
|
84 |
+
});
|
85 |
+
}}
|
86 |
+
/>
|
87 |
+
</div>
|
88 |
+
{/* Scroll to bottom Icon */}
|
89 |
+
<Button
|
90 |
+
size="icon"
|
91 |
+
className={cn(
|
92 |
+
'absolute bottom-3 right-3 transition-opacity duration-300 size-6',
|
93 |
+
isVisible ? 'opacity-0' : 'opacity-100',
|
94 |
+
)}
|
95 |
+
onClick={() => scrollToBottom()}
|
96 |
+
>
|
97 |
+
<IconArrowDown className="size-3" />
|
98 |
+
</Button>
|
99 |
+
</Card>
|
100 |
);
|
101 |
+
};
|
102 |
+
|
103 |
+
export default ChatList;
|
components/chat/ChatMessage.tsx
CHANGED
@@ -8,8 +8,6 @@ import {
|
|
8 |
IconListUnordered,
|
9 |
IconTerminalWindow,
|
10 |
IconUser,
|
11 |
-
IconOutput,
|
12 |
-
IconLog,
|
13 |
IconGlowingDot,
|
14 |
} from '@/components/ui/Icons';
|
15 |
import { MessageUI } from '@/lib/types';
|
@@ -23,59 +21,93 @@ import {
|
|
23 |
TableRow,
|
24 |
} from '../ui/Table';
|
25 |
import { Button } from '../ui/Button';
|
26 |
-
import { Separator } from '../ui/Separator';
|
27 |
-
import {
|
28 |
-
Tooltip,
|
29 |
-
TooltipContent,
|
30 |
-
TooltipTrigger,
|
31 |
-
} from '@/components/ui/Tooltip';
|
32 |
-
|
33 |
import { Dialog, DialogContent, DialogTrigger } from '../ui/Dialog';
|
34 |
-
import { Markdown } from './MemoizedReactMarkdown';
|
35 |
import Img from '../ui/Img';
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
|
37 |
export interface ChatMessageProps {
|
38 |
-
message:
|
39 |
-
|
40 |
}
|
41 |
|
42 |
-
export
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
|
|
|
|
49 |
);
|
50 |
-
}
|
51 |
-
|
52 |
-
const UserChatMessage: React.FC<{
|
53 |
-
content: string;
|
54 |
-
mediaUrl?: string;
|
55 |
-
}> = ({ content, mediaUrl }) => {
|
56 |
return (
|
57 |
-
<div
|
58 |
-
|
59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
</div>
|
61 |
-
<
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
</div>
|
80 |
</div>
|
81 |
);
|
@@ -113,7 +145,10 @@ const ChunkPayloadAction: React.FC<{
|
|
113 |
<IconListUnordered />
|
114 |
</Button>
|
115 |
</DialogTrigger>
|
116 |
-
<DialogContent
|
|
|
|
|
|
|
117 |
<Table>
|
118 |
<TableHeader>
|
119 |
<TableRow className="border-primary/50">
|
@@ -170,144 +205,4 @@ const ChunkPayloadAction: React.FC<{
|
|
170 |
}
|
171 |
};
|
172 |
|
173 |
-
|
174 |
-
codeResult: CodeResult;
|
175 |
-
}> = ({ codeResult }) => {
|
176 |
-
const { code, test, result } = codeResult;
|
177 |
-
const getDetail = () => {
|
178 |
-
if (!result) return {};
|
179 |
-
try {
|
180 |
-
const detail = JSON.parse(result);
|
181 |
-
return {
|
182 |
-
results: detail.results,
|
183 |
-
stderr: detail.logs.stderr,
|
184 |
-
stdout: detail.logs.stdout,
|
185 |
-
};
|
186 |
-
} catch {
|
187 |
-
return {};
|
188 |
-
}
|
189 |
-
};
|
190 |
-
|
191 |
-
const { results, stderr, stdout } = getDetail();
|
192 |
-
|
193 |
-
return (
|
194 |
-
<div className="rounded-lg overflow-hidden relative max-w-5xl">
|
195 |
-
<CodeBlock language="python" value={code} />
|
196 |
-
<div className="rounded-lg relative">
|
197 |
-
<div className="absolute left-1/2 -translate-x-1/2 -top-4 z-10">
|
198 |
-
<Dialog>
|
199 |
-
<DialogTrigger asChild>
|
200 |
-
<Button variant="ghost" size="icon" className="size-8">
|
201 |
-
<IconTerminalWindow className="text-teal-500 size-4" />
|
202 |
-
</Button>
|
203 |
-
</DialogTrigger>
|
204 |
-
<DialogContent className="max-w-5xl">
|
205 |
-
<CodeBlock language="python" value={test} />
|
206 |
-
</DialogContent>
|
207 |
-
</Dialog>
|
208 |
-
{Array.isArray(stderr) && (
|
209 |
-
<Dialog>
|
210 |
-
<DialogTrigger asChild>
|
211 |
-
<Button variant="ghost" size="icon" className="size-8">
|
212 |
-
<IconLog className="text-gray-500 size-4" />
|
213 |
-
</Button>
|
214 |
-
</DialogTrigger>
|
215 |
-
<DialogContent className="max-w-5xl">
|
216 |
-
<CodeBlock language="vim" value={stderr.join('').trim()} />
|
217 |
-
</DialogContent>
|
218 |
-
</Dialog>
|
219 |
-
)}
|
220 |
-
</div>
|
221 |
-
</div>
|
222 |
-
{Array.isArray(stdout) && !!stdout.join('').trim() && (
|
223 |
-
<>
|
224 |
-
<Separator />
|
225 |
-
<CodeBlock language="print" value={stdout.join('').trim()} />
|
226 |
-
</>
|
227 |
-
)}
|
228 |
-
{Array.isArray(results) && !!results.length && (
|
229 |
-
<>
|
230 |
-
<Separator />
|
231 |
-
{results.map((result, index) => {
|
232 |
-
if (result.png) {
|
233 |
-
return (
|
234 |
-
<Img
|
235 |
-
key={'png' + index}
|
236 |
-
src={result.png}
|
237 |
-
alt={'answer-image'}
|
238 |
-
quality={100}
|
239 |
-
sizes="(min-width: 66em) 15vw,
|
240 |
-
(min-width: 44em) 20vw,
|
241 |
-
100vw"
|
242 |
-
/>
|
243 |
-
);
|
244 |
-
} else if (result.mp4) {
|
245 |
-
return (
|
246 |
-
<video
|
247 |
-
key={'mp4' + index}
|
248 |
-
src={result.mp4}
|
249 |
-
controls
|
250 |
-
width={500}
|
251 |
-
height={500}
|
252 |
-
/>
|
253 |
-
);
|
254 |
-
} else if (result.text) {
|
255 |
-
return (
|
256 |
-
<CodeBlock
|
257 |
-
key={'text' + index}
|
258 |
-
language="output"
|
259 |
-
value={result.text}
|
260 |
-
/>
|
261 |
-
);
|
262 |
-
} else {
|
263 |
-
return null;
|
264 |
-
}
|
265 |
-
})}
|
266 |
-
</>
|
267 |
-
)}
|
268 |
-
<Separator />
|
269 |
-
</div>
|
270 |
-
);
|
271 |
-
};
|
272 |
-
|
273 |
-
const AssistantChatMessage: React.FC<{
|
274 |
-
content: string;
|
275 |
-
}> = ({ content }) => {
|
276 |
-
const [formattedSections, codeResult] = useMemo(
|
277 |
-
() => formatStreamLogs(content),
|
278 |
-
[content],
|
279 |
-
);
|
280 |
-
|
281 |
-
return (
|
282 |
-
<div className="group relative mb-6 flex rounded-md bg-muted p-6 w-full">
|
283 |
-
<div className="flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-primary text-primary-foreground">
|
284 |
-
<IconLandingAI />
|
285 |
-
</div>
|
286 |
-
<div className="flex-1 px-1 space-y-4 ml-4 overflow-hidden">
|
287 |
-
<Table className="w-[400px]">
|
288 |
-
<TableBody>
|
289 |
-
{formattedSections.map(section => (
|
290 |
-
<TableRow
|
291 |
-
className="border-primary/50 h-[56px]"
|
292 |
-
key={section.type}
|
293 |
-
>
|
294 |
-
<TableCell className="text-center text-webkit-center">
|
295 |
-
{ChunkStatusToIconDict[section.status]}
|
296 |
-
</TableCell>
|
297 |
-
<TableCell className="font-medium">
|
298 |
-
{ChunkTypeToTextDict[section.type]}
|
299 |
-
</TableCell>
|
300 |
-
<TableCell className="text-right">
|
301 |
-
<ChunkPayloadAction payload={section.payload} />
|
302 |
-
</TableCell>
|
303 |
-
</TableRow>
|
304 |
-
))}
|
305 |
-
</TableBody>
|
306 |
-
</Table>
|
307 |
-
{codeResult && <CodeResultDisplay codeResult={codeResult} />}
|
308 |
-
</div>
|
309 |
-
</div>
|
310 |
-
);
|
311 |
-
};
|
312 |
-
|
313 |
-
export default UserChatMessage;
|
|
|
8 |
IconListUnordered,
|
9 |
IconTerminalWindow,
|
10 |
IconUser,
|
|
|
|
|
11 |
IconGlowingDot,
|
12 |
} from '@/components/ui/Icons';
|
13 |
import { MessageUI } from '@/lib/types';
|
|
|
21 |
TableRow,
|
22 |
} from '../ui/Table';
|
23 |
import { Button } from '../ui/Button';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
import { Dialog, DialogContent, DialogTrigger } from '../ui/Dialog';
|
|
|
25 |
import Img from '../ui/Img';
|
26 |
+
import CodeResultDisplay from '../CodeResultDisplay';
|
27 |
+
import { useAtom, useSetAtom } from 'jotai';
|
28 |
+
import { selectedMessageId } from '@/state/chat';
|
29 |
+
import { Message } from '@prisma/client';
|
30 |
+
import { Separator } from '../ui/Separator';
|
31 |
+
import { cn } from '@/lib/utils';
|
32 |
|
33 |
export interface ChatMessageProps {
|
34 |
+
message: Message;
|
35 |
+
wipAssistantMessage?: MessageUI;
|
36 |
}
|
37 |
|
38 |
+
export const ChatMessage: React.FC<ChatMessageProps> = ({
|
39 |
+
message,
|
40 |
+
wipAssistantMessage,
|
41 |
+
}) => {
|
42 |
+
const [messageId, setMessageId] = useAtom(selectedMessageId);
|
43 |
+
const { id, mediaUrl, prompt, response, result } = message;
|
44 |
+
const [formattedSections, codeResult] = useMemo(
|
45 |
+
() => formatStreamLogs(response ?? wipAssistantMessage?.content),
|
46 |
+
[response, wipAssistantMessage?.content],
|
47 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
return (
|
49 |
+
<div
|
50 |
+
className={cn(
|
51 |
+
'rounded-md bg-muted border border-muted p-4 mb-4',
|
52 |
+
messageId === id && 'lg:border-primary/50',
|
53 |
+
result && 'lg:cursor-pointer',
|
54 |
+
)}
|
55 |
+
onClick={() => {
|
56 |
+
if (result) {
|
57 |
+
setMessageId(id);
|
58 |
+
}
|
59 |
+
}}
|
60 |
+
>
|
61 |
+
<div className="flex">
|
62 |
+
<div className="flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-background">
|
63 |
+
<IconUser />
|
64 |
+
</div>
|
65 |
+
<div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
|
66 |
+
<p>{prompt}</p>
|
67 |
+
{mediaUrl && (
|
68 |
+
<>
|
69 |
+
{mediaUrl?.endsWith('.mp4') ? (
|
70 |
+
<video src={mediaUrl} controls width={500} height={500} />
|
71 |
+
) : (
|
72 |
+
<Img src={mediaUrl} alt={mediaUrl} quality={100} width={300} />
|
73 |
+
)}
|
74 |
+
</>
|
75 |
+
)}
|
76 |
+
</div>
|
77 |
</div>
|
78 |
+
<Separator className="bg-primary/30 my-4" />
|
79 |
+
<div className="flex">
|
80 |
+
<div className="flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-primary text-primary-foreground">
|
81 |
+
<IconLandingAI />
|
82 |
+
</div>
|
83 |
+
<div className="flex-1 px-1 space-y-4 ml-4 overflow-hidden">
|
84 |
+
<Table className="w-[400px]">
|
85 |
+
<TableBody>
|
86 |
+
{formattedSections.map(section => (
|
87 |
+
<TableRow
|
88 |
+
className="border-primary/50 h-[56px]"
|
89 |
+
key={section.type}
|
90 |
+
>
|
91 |
+
<TableCell className="text-center text-webkit-center">
|
92 |
+
{ChunkStatusToIconDict[section.status]}
|
93 |
+
</TableCell>
|
94 |
+
<TableCell className="font-medium">
|
95 |
+
{ChunkTypeToTextDict[section.type]}
|
96 |
+
</TableCell>
|
97 |
+
<TableCell className="text-right">
|
98 |
+
<ChunkPayloadAction payload={section.payload} />
|
99 |
+
</TableCell>
|
100 |
+
</TableRow>
|
101 |
+
))}
|
102 |
+
</TableBody>
|
103 |
+
</Table>
|
104 |
+
{codeResult && (
|
105 |
+
<div className="xl:hidden">
|
106 |
+
<CodeResultDisplay codeResult={codeResult} />
|
107 |
+
</div>
|
108 |
+
)}
|
109 |
+
{codeResult && <p>✨ Coding complete</p>}
|
110 |
+
</div>
|
111 |
</div>
|
112 |
</div>
|
113 |
);
|
|
|
145 |
<IconListUnordered />
|
146 |
</Button>
|
147 |
</DialogTrigger>
|
148 |
+
<DialogContent
|
149 |
+
className="max-w-5xl"
|
150 |
+
onOpenAutoFocus={e => e.preventDefault()}
|
151 |
+
>
|
152 |
<Table>
|
153 |
<TableHeader>
|
154 |
<TableRow className="border-primary/50">
|
|
|
205 |
}
|
206 |
};
|
207 |
|
208 |
+
export default ChatMessage;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/chat/Composer.tsx
CHANGED
@@ -75,7 +75,7 @@ const Composer = forwardRef<ComposerRef, ComposerProps>(
|
|
75 |
<div
|
76 |
{...getRootProps()}
|
77 |
className={cn(
|
78 |
-
'
|
79 |
isDragActive && 'bg-indigo-700/50',
|
80 |
)}
|
81 |
>
|
|
|
75 |
<div
|
76 |
{...getRootProps()}
|
77 |
className={cn(
|
78 |
+
'mx-auto w-[42rem] max-w-full 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/chat/LoginPrompt.tsx
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { Card } from '../ui/Card';
|
4 |
+
import { IconExclamationTriangle } from '../ui/Icons';
|
5 |
+
import Link from 'next/link';
|
6 |
+
|
7 |
+
export interface LoginPrompt {}
|
8 |
+
|
9 |
+
export function LoginPrompt() {
|
10 |
+
return (
|
11 |
+
<Card className="group py-2 px-4 flex items-center">
|
12 |
+
<div className="bg-background flex size-8 shrink-0 select-none items-center justify-center rounded-md">
|
13 |
+
<IconExclamationTriangle className="font-medium" />
|
14 |
+
</div>
|
15 |
+
<div className="flex-1 px-1 ml-2 overflow-hidden">
|
16 |
+
<p className="leading-normal font-medium">
|
17 |
+
<Link href="/sign-in" className="underline">
|
18 |
+
Sign in
|
19 |
+
</Link>{' '}
|
20 |
+
to save and revisit your chat history!
|
21 |
+
</p>
|
22 |
+
</div>
|
23 |
+
</Card>
|
24 |
+
);
|
25 |
+
}
|
components/project/ClassBar.tsx
DELETED
@@ -1,22 +0,0 @@
|
|
1 |
-
import { ClassDetails } from '@/lib/fetch';
|
2 |
-
import Chip from '../ui/Chip';
|
3 |
-
|
4 |
-
export interface ClassBarProps {
|
5 |
-
classList: ClassDetails[];
|
6 |
-
}
|
7 |
-
|
8 |
-
export default async function ClassBar({ classList }: ClassBarProps) {
|
9 |
-
return (
|
10 |
-
<div className="border-b border-gray-300 px-3 pb-3 max-w-3xl mx-auto">
|
11 |
-
{classList.map(classItem => {
|
12 |
-
return (
|
13 |
-
<Chip
|
14 |
-
key={classItem.id}
|
15 |
-
value={classItem.name}
|
16 |
-
className="px-3 py-1 my-1"
|
17 |
-
/>
|
18 |
-
);
|
19 |
-
})}
|
20 |
-
</div>
|
21 |
-
);
|
22 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/project/MediaGrid.tsx
DELETED
@@ -1,18 +0,0 @@
|
|
1 |
-
import { MediaDetails } from '@/lib/fetch';
|
2 |
-
import MediaTile from './MediaTile';
|
3 |
-
|
4 |
-
export default function MediaGrid({
|
5 |
-
mediaList,
|
6 |
-
}: {
|
7 |
-
mediaList: MediaDetails[];
|
8 |
-
}) {
|
9 |
-
return (
|
10 |
-
<div className="relative size-full p-3 max-w-3xl mx-auto">
|
11 |
-
<div className="columns-1 sm:columns-1 md:columns-2 lg:columns-2 xl:columns:3 gap-3 [&>img:not(:first-child)]:mt-3">
|
12 |
-
{mediaList.map(media => (
|
13 |
-
<MediaTile key={media.id} media={media} />
|
14 |
-
))}
|
15 |
-
</div>
|
16 |
-
</div>
|
17 |
-
);
|
18 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/project/MediaTile.tsx
DELETED
@@ -1,53 +0,0 @@
|
|
1 |
-
'use client';
|
2 |
-
|
3 |
-
import React from 'react';
|
4 |
-
import Image from 'next/image';
|
5 |
-
import {
|
6 |
-
Tooltip,
|
7 |
-
TooltipContent,
|
8 |
-
TooltipTrigger,
|
9 |
-
} from '@/components/ui/Tooltip';
|
10 |
-
import { MediaDetails } from '@/lib/fetch';
|
11 |
-
import { useAtom } from 'jotai';
|
12 |
-
import { selectedMediaIdAtom } from '@/state/media';
|
13 |
-
import { cn } from '@/lib/utils';
|
14 |
-
|
15 |
-
export interface MediaTileProps {
|
16 |
-
media: MediaDetails;
|
17 |
-
}
|
18 |
-
|
19 |
-
export default function MediaTile({ media }: MediaTileProps) {
|
20 |
-
const {
|
21 |
-
url,
|
22 |
-
thumbnails,
|
23 |
-
id,
|
24 |
-
name,
|
25 |
-
properties: { width, height },
|
26 |
-
} = media;
|
27 |
-
const [selectedMediaId, setSelectedMediaId] = useAtom(selectedMediaIdAtom);
|
28 |
-
const selected = selectedMediaId === id;
|
29 |
-
// const imageSrc = thumbnails.length ? thumbnails[thumbnails.length - 1] : url;
|
30 |
-
const imageSrc = url;
|
31 |
-
return (
|
32 |
-
<Tooltip>
|
33 |
-
<TooltipTrigger asChild>
|
34 |
-
<Image
|
35 |
-
src={imageSrc}
|
36 |
-
draggable={false}
|
37 |
-
alt="dataset images"
|
38 |
-
width={width}
|
39 |
-
height={height}
|
40 |
-
onClick={() => setSelectedMediaId(id)}
|
41 |
-
className={cn(
|
42 |
-
'w-full h-auto relative rounded-xl overflow-hidden shadow-md cursor-pointer transition-transform hover:scale-105 box-content',
|
43 |
-
selected && 'border-2 border-primary',
|
44 |
-
)}
|
45 |
-
/>
|
46 |
-
</TooltipTrigger>
|
47 |
-
<TooltipContent>
|
48 |
-
<p>{name}</p>
|
49 |
-
<p className="font-light text-xs">{`${width} x ${height}`}</p>
|
50 |
-
</TooltipContent>
|
51 |
-
</Tooltip>
|
52 |
-
);
|
53 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/ui/Card.tsx
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from 'react';
|
2 |
+
|
3 |
+
import { cn } from '@/lib/utils';
|
4 |
+
|
5 |
+
const Card = React.forwardRef<
|
6 |
+
HTMLDivElement,
|
7 |
+
React.HTMLAttributes<HTMLDivElement>
|
8 |
+
>(({ className, ...props }, ref) => (
|
9 |
+
<div
|
10 |
+
ref={ref}
|
11 |
+
className={cn(
|
12 |
+
'rounded-xl border bg-card text-card-foreground shadow',
|
13 |
+
className,
|
14 |
+
)}
|
15 |
+
{...props}
|
16 |
+
/>
|
17 |
+
));
|
18 |
+
Card.displayName = 'Card';
|
19 |
+
|
20 |
+
const CardHeader = React.forwardRef<
|
21 |
+
HTMLDivElement,
|
22 |
+
React.HTMLAttributes<HTMLDivElement>
|
23 |
+
>(({ className, ...props }, ref) => (
|
24 |
+
<div
|
25 |
+
ref={ref}
|
26 |
+
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
27 |
+
{...props}
|
28 |
+
/>
|
29 |
+
));
|
30 |
+
CardHeader.displayName = 'CardHeader';
|
31 |
+
|
32 |
+
const CardTitle = React.forwardRef<
|
33 |
+
HTMLParagraphElement,
|
34 |
+
React.HTMLAttributes<HTMLHeadingElement>
|
35 |
+
>(({ className, ...props }, ref) => (
|
36 |
+
<h3
|
37 |
+
ref={ref}
|
38 |
+
className={cn('font-semibold leading-none tracking-tight', className)}
|
39 |
+
{...props}
|
40 |
+
/>
|
41 |
+
));
|
42 |
+
CardTitle.displayName = 'CardTitle';
|
43 |
+
|
44 |
+
const CardDescription = React.forwardRef<
|
45 |
+
HTMLParagraphElement,
|
46 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
47 |
+
>(({ className, ...props }, ref) => (
|
48 |
+
<p
|
49 |
+
ref={ref}
|
50 |
+
className={cn('text-sm text-muted-foreground', className)}
|
51 |
+
{...props}
|
52 |
+
/>
|
53 |
+
));
|
54 |
+
CardDescription.displayName = 'CardDescription';
|
55 |
+
|
56 |
+
const CardContent = React.forwardRef<
|
57 |
+
HTMLDivElement,
|
58 |
+
React.HTMLAttributes<HTMLDivElement>
|
59 |
+
>(({ className, ...props }, ref) => (
|
60 |
+
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
61 |
+
));
|
62 |
+
CardContent.displayName = 'CardContent';
|
63 |
+
|
64 |
+
const CardFooter = React.forwardRef<
|
65 |
+
HTMLDivElement,
|
66 |
+
React.HTMLAttributes<HTMLDivElement>
|
67 |
+
>(({ className, ...props }, ref) => (
|
68 |
+
<div
|
69 |
+
ref={ref}
|
70 |
+
className={cn('flex items-center p-6 pt-0', className)}
|
71 |
+
{...props}
|
72 |
+
/>
|
73 |
+
));
|
74 |
+
CardFooter.displayName = 'CardFooter';
|
75 |
+
|
76 |
+
export {
|
77 |
+
Card,
|
78 |
+
CardHeader,
|
79 |
+
CardFooter,
|
80 |
+
CardTitle,
|
81 |
+
CardDescription,
|
82 |
+
CardContent,
|
83 |
+
};
|
components/ui/CodeBlock.tsx
CHANGED
@@ -45,9 +45,14 @@ export const programmingLanguages: languageMap = {
|
|
45 |
sql: '.sql',
|
46 |
html: '.html',
|
47 |
css: '.css',
|
|
|
48 |
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
|
49 |
};
|
50 |
|
|
|
|
|
|
|
|
|
51 |
export const generateRandomString = (length: number, lowercase = false) => {
|
52 |
const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789'; // excluding similar looking characters like Z, 2, I, 1, O, 0
|
53 |
let result = '';
|
@@ -118,7 +123,7 @@ const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
|
118 |
</div>
|
119 |
</div>
|
120 |
<SyntaxHighlighter
|
121 |
-
language={language}
|
122 |
style={coldarkDark}
|
123 |
PreTag="div"
|
124 |
showLineNumbers
|
|
|
45 |
sql: '.sql',
|
46 |
html: '.html',
|
47 |
css: '.css',
|
48 |
+
print: '.txt',
|
49 |
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
|
50 |
};
|
51 |
|
52 |
+
const customSyntax: languageMap = {
|
53 |
+
print: 'vim',
|
54 |
+
};
|
55 |
+
|
56 |
export const generateRandomString = (length: number, lowercase = false) => {
|
57 |
const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789'; // excluding similar looking characters like Z, 2, I, 1, O, 0
|
58 |
let result = '';
|
|
|
123 |
</div>
|
124 |
</div>
|
125 |
<SyntaxHighlighter
|
126 |
+
language={customSyntax[language] ?? language}
|
127 |
style={coldarkDark}
|
128 |
PreTag="div"
|
129 |
showLineNumbers
|
components/ui/Icons.tsx
CHANGED
@@ -585,6 +585,8 @@ function IconExclamationTriangle({
|
|
585 |
viewBox="0 0 15 15"
|
586 |
fill="none"
|
587 |
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
588 |
>
|
589 |
<path
|
590 |
d="M8.4449 0.608765C8.0183 -0.107015 6.9817 -0.107015 6.55509 0.608766L0.161178 11.3368C-0.275824 12.07 0.252503 13 1.10608 13H13.8939C14.7475 13 15.2758 12.07 14.8388 11.3368L8.4449 0.608765ZM7.4141 1.12073C7.45288 1.05566 7.54712 1.05566 7.5859 1.12073L13.9798 11.8488C14.0196 11.9154 13.9715 12 13.8939 12H1.10608C1.02849 12 0.980454 11.9154 1.02018 11.8488L7.4141 1.12073ZM6.8269 4.48611C6.81221 4.10423 7.11783 3.78663 7.5 3.78663C7.88217 3.78663 8.18778 4.10423 8.1731 4.48612L8.01921 8.48701C8.00848 8.766 7.7792 8.98664 7.5 8.98664C7.2208 8.98664 6.99151 8.766 6.98078 8.48701L6.8269 4.48611ZM8.24989 10.476C8.24989 10.8902 7.9141 11.226 7.49989 11.226C7.08567 11.226 6.74989 10.8902 6.74989 10.476C6.74989 10.0618 7.08567 9.72599 7.49989 9.72599C7.9141 9.72599 8.24989 10.0618 8.24989 10.476Z"
|
|
|
585 |
viewBox="0 0 15 15"
|
586 |
fill="none"
|
587 |
xmlns="http://www.w3.org/2000/svg"
|
588 |
+
className={cn('size-4', className)}
|
589 |
+
{...props}
|
590 |
>
|
591 |
<path
|
592 |
d="M8.4449 0.608765C8.0183 -0.107015 6.9817 -0.107015 6.55509 0.608766L0.161178 11.3368C-0.275824 12.07 0.252503 13 1.10608 13H13.8939C14.7475 13 15.2758 12.07 14.8388 11.3368L8.4449 0.608765ZM7.4141 1.12073C7.45288 1.05566 7.54712 1.05566 7.5859 1.12073L13.9798 11.8488C14.0196 11.9154 13.9715 12 13.8939 12H1.10608C1.02849 12 0.980454 11.9154 1.02018 11.8488L7.4141 1.12073ZM6.8269 4.48611C6.81221 4.10423 7.11783 3.78663 7.5 3.78663C7.88217 3.78663 8.18778 4.10423 8.1731 4.48612L8.01921 8.48701C8.00848 8.766 7.7792 8.98664 7.5 8.98664C7.2208 8.98664 6.99151 8.766 6.98078 8.48701L6.8269 4.48611ZM8.24989 10.476C8.24989 10.8902 7.9141 11.226 7.49989 11.226C7.08567 11.226 6.74989 10.8902 6.74989 10.476C6.74989 10.0618 7.08567 9.72599 7.49989 9.72599C7.9141 9.72599 8.24989 10.0618 8.24989 10.476Z"
|
lib/db/functions.ts
CHANGED
@@ -60,6 +60,9 @@ export async function dbGetMyChatList(): Promise<Chat[]> {
|
|
60 |
|
61 |
return prisma.chat.findMany({
|
62 |
where: { userId },
|
|
|
|
|
|
|
63 |
});
|
64 |
}
|
65 |
|
@@ -72,7 +75,11 @@ export async function dbGetChat(id: string): Promise<ChatWithMessages | null> {
|
|
72 |
return prisma.chat.findUnique({
|
73 |
where: { id },
|
74 |
include: {
|
75 |
-
messages:
|
|
|
|
|
|
|
|
|
76 |
},
|
77 |
});
|
78 |
}
|
|
|
60 |
|
61 |
return prisma.chat.findMany({
|
62 |
where: { userId },
|
63 |
+
orderBy: {
|
64 |
+
createdAt: 'desc',
|
65 |
+
},
|
66 |
});
|
67 |
}
|
68 |
|
|
|
75 |
return prisma.chat.findUnique({
|
76 |
where: { id },
|
77 |
include: {
|
78 |
+
messages: {
|
79 |
+
orderBy: {
|
80 |
+
createdAt: 'asc',
|
81 |
+
},
|
82 |
+
},
|
83 |
},
|
84 |
});
|
85 |
}
|
lib/db/prisma.ts
CHANGED
@@ -10,18 +10,18 @@ declare global {
|
|
10 |
payload: {
|
11 |
code: string;
|
12 |
test: string;
|
13 |
-
result:
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
};
|
25 |
};
|
26 |
};
|
27 |
}
|
|
|
10 |
payload: {
|
11 |
code: string;
|
12 |
test: string;
|
13 |
+
result: string; // TODO To be fixed to JSON below
|
14 |
+
// result: {
|
15 |
+
// logs: {
|
16 |
+
// stderr: string[];
|
17 |
+
// stdout: string[];
|
18 |
+
// };
|
19 |
+
// results: Array<{
|
20 |
+
// png?: string;
|
21 |
+
// text: string;
|
22 |
+
// is_main_result: boolean;
|
23 |
+
// }>;
|
24 |
+
// };
|
25 |
};
|
26 |
};
|
27 |
}
|
lib/hooks/useVisionAgent.ts
CHANGED
@@ -10,10 +10,13 @@ import {
|
|
10 |
convertAssistantUIMessageToDBMessageResponse,
|
11 |
convertDBMessageToUIMessage,
|
12 |
} from '../utils/message';
|
|
|
|
|
13 |
|
14 |
const useVisionAgent = (chat: ChatWithMessages) => {
|
15 |
const { messages: dbMessages, id, mediaUrl } = chat;
|
16 |
const latestDbMessage = dbMessages[dbMessages.length - 1];
|
|
|
17 |
|
18 |
// Temporary solution for now while single we have to pass mediaUrl separately outside of the messages
|
19 |
const currMediaUrl = useRef<string>(mediaUrl);
|
@@ -32,6 +35,7 @@ const useVisionAgent = (chat: ChatWithMessages) => {
|
|
32 |
currMessageId.current,
|
33 |
convertAssistantUIMessageToDBMessageResponse(message),
|
34 |
);
|
|
|
35 |
},
|
36 |
sendExtraMessageFields: true,
|
37 |
initialMessages: convertDBMessageToUIMessage(dbMessages),
|
@@ -63,7 +67,9 @@ const useVisionAgent = (chat: ChatWithMessages) => {
|
|
63 |
return {
|
64 |
messages: messages as MessageUI[],
|
65 |
append: async (messageInput: MessageUserInput) => {
|
|
|
66 |
currMediaUrl.current = messageInput.mediaUrl;
|
|
|
67 |
append({
|
68 |
id,
|
69 |
role: 'user',
|
@@ -71,8 +77,6 @@ const useVisionAgent = (chat: ChatWithMessages) => {
|
|
71 |
// @ts-ignore valid when setting sendExtraMessageFields
|
72 |
mediaUrl: messageInput.mediaUrl,
|
73 |
});
|
74 |
-
const resp = await dbPostCreateMessage(id, messageInput);
|
75 |
-
currMessageId.current = resp.id;
|
76 |
},
|
77 |
reload,
|
78 |
isLoading,
|
|
|
10 |
convertAssistantUIMessageToDBMessageResponse,
|
11 |
convertDBMessageToUIMessage,
|
12 |
} from '../utils/message';
|
13 |
+
import { useSetAtom } from 'jotai';
|
14 |
+
import { selectedMessageId } from '@/state/chat';
|
15 |
|
16 |
const useVisionAgent = (chat: ChatWithMessages) => {
|
17 |
const { messages: dbMessages, id, mediaUrl } = chat;
|
18 |
const latestDbMessage = dbMessages[dbMessages.length - 1];
|
19 |
+
const setMessageId = useSetAtom(selectedMessageId);
|
20 |
|
21 |
// Temporary solution for now while single we have to pass mediaUrl separately outside of the messages
|
22 |
const currMediaUrl = useRef<string>(mediaUrl);
|
|
|
35 |
currMessageId.current,
|
36 |
convertAssistantUIMessageToDBMessageResponse(message),
|
37 |
);
|
38 |
+
setMessageId(currMessageId.current);
|
39 |
},
|
40 |
sendExtraMessageFields: true,
|
41 |
initialMessages: convertDBMessageToUIMessage(dbMessages),
|
|
|
67 |
return {
|
68 |
messages: messages as MessageUI[],
|
69 |
append: async (messageInput: MessageUserInput) => {
|
70 |
+
const resp = await dbPostCreateMessage(id, messageInput);
|
71 |
currMediaUrl.current = messageInput.mediaUrl;
|
72 |
+
currMessageId.current = resp.id;
|
73 |
append({
|
74 |
id,
|
75 |
role: 'user',
|
|
|
77 |
// @ts-ignore valid when setting sendExtraMessageFields
|
78 |
mediaUrl: messageInput.mediaUrl,
|
79 |
});
|
|
|
|
|
80 |
},
|
81 |
reload,
|
82 |
isLoading,
|
lib/types.ts
CHANGED
@@ -18,3 +18,16 @@ export interface SignedPayload {
|
|
18 |
signedUrl: string;
|
19 |
fields: Record<string, string>;
|
20 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
signedUrl: string;
|
19 |
fields: Record<string, string>;
|
20 |
}
|
21 |
+
|
22 |
+
export type ResultPayload = {
|
23 |
+
logs: {
|
24 |
+
stderr: string[];
|
25 |
+
stdout: string[];
|
26 |
+
};
|
27 |
+
results: Array<{
|
28 |
+
png?: string;
|
29 |
+
mp4?: string;
|
30 |
+
text: string;
|
31 |
+
is_main_result: boolean;
|
32 |
+
}>;
|
33 |
+
};
|
lib/utils/content.ts
CHANGED
@@ -77,34 +77,6 @@ const generateCodeExecutionMarkdown = (
|
|
77 |
return message;
|
78 |
};
|
79 |
|
80 |
-
const generateFinalCodeMarkdown = (
|
81 |
-
code: string,
|
82 |
-
test: string,
|
83 |
-
result: PrismaJson.FinalChatResult['payload']['result'],
|
84 |
-
) => {
|
85 |
-
let message = 'Final Code: \n';
|
86 |
-
message += `\`\`\`python\n${code}\n\`\`\`\n`;
|
87 |
-
message += 'Final test: \n';
|
88 |
-
message += `\`\`\`python\n${test}\n\`\`\`\n`;
|
89 |
-
message += `Final result: \n`;
|
90 |
-
const images = result.results.map(result => result.png).filter(png => !!png);
|
91 |
-
if (images.length > 0) {
|
92 |
-
message += `Visualization output:\n`;
|
93 |
-
images.forEach((image, index) => {
|
94 |
-
message += generateAnswersImageMarkdown(index, image!);
|
95 |
-
});
|
96 |
-
}
|
97 |
-
if (result.logs.stderr.length > 0) {
|
98 |
-
message += `Error output:\n`;
|
99 |
-
message += `\`\`\`\n${result.logs.stderr.join('\n')}\n\`\`\`\n`;
|
100 |
-
}
|
101 |
-
if (result.logs.stdout.length > 0) {
|
102 |
-
message += `Output:\n`;
|
103 |
-
message += `\`\`\`\n${result.logs.stdout.join('\n')}\n\`\`\`\n`;
|
104 |
-
}
|
105 |
-
return message;
|
106 |
-
};
|
107 |
-
|
108 |
type PlansBody =
|
109 |
| {
|
110 |
type: 'plans';
|
@@ -255,8 +227,9 @@ export type ChunkBody =
|
|
255 |
* @returns An array of grouped sections and an optional final code result.
|
256 |
*/
|
257 |
export const formatStreamLogs = (
|
258 |
-
content: string,
|
259 |
): [ChunkBody[], CodeResult?] => {
|
|
|
260 |
const streamLogs = content.split('\n').filter(log => !!log);
|
261 |
|
262 |
const buffer = streamLogs.pop();
|
|
|
77 |
return message;
|
78 |
};
|
79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
80 |
type PlansBody =
|
81 |
| {
|
82 |
type: 'plans';
|
|
|
227 |
* @returns An array of grouped sections and an optional final code result.
|
228 |
*/
|
229 |
export const formatStreamLogs = (
|
230 |
+
content: string | null | undefined,
|
231 |
): [ChunkBody[], CodeResult?] => {
|
232 |
+
if (!content) return [[], undefined];
|
233 |
const streamLogs = content.split('\n').filter(log => !!log);
|
234 |
|
235 |
const buffer = streamLogs.pop();
|
state/chat.ts
CHANGED
@@ -1,3 +1,3 @@
|
|
1 |
import { atom } from 'jotai';
|
2 |
|
3 |
-
export const
|
|
|
1 |
import { atom } from 'jotai';
|
2 |
|
3 |
+
export const selectedMessageId = atom<string | undefined>(undefined);
|
state/media.ts
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
import { atom } from 'jotai';
|
2 |
-
|
3 |
-
export const selectedMediaIdAtom = atom<number | null>(null);
|
|
|
|
|
|
|
|