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 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 '@/components/chat/ChatServer';
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
- <ChatServer id={chatId} />
 
 
 
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 ChatClient from './ChatClient';
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 <ChatClient chat={chat} />;
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 py-8 h-[calc(100vh-64px)] bg-background overflow-hidden relative">
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 { Separator } from '@/components/ui/Separator';
4
- import { ChatMessage } from '@/components/chat/ChatMessage';
5
- import { MessageUI } from '@/lib/types';
 
6
  import { Session } from 'next-auth';
7
- import { IconExclamationTriangle } from '../ui/Icons';
8
- import Link from 'next/link';
 
 
 
 
 
 
 
 
9
 
10
- export interface ChatList {
11
- messages: MessageUI[];
12
- session: Session | null;
13
- isLoading: boolean;
14
  }
15
 
16
- export function ChatList({ messages, session, isLoading }: ChatList) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  return (
18
- <div className="relative mx-auto max-w-5xl px-8 pt-6 border rounded-lg">
19
- {!session && (
20
- <>
21
- <div className="group relative mb-6 flex items-center">
22
- <div className="bg-background flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow">
23
- <IconExclamationTriangle />
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
- isLoading={isLoading && index === messages.length - 1}
 
 
 
 
59
  />
60
  ))}
61
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: MessageUI;
39
- isLoading: boolean;
40
  }
41
 
42
- export function ChatMessage({ message, isLoading }: ChatMessageProps) {
43
- const { role, content, mediaUrl } = message;
44
-
45
- return role === 'user' ? (
46
- <UserChatMessage content={content} mediaUrl={mediaUrl} />
47
- ) : (
48
- <AssistantChatMessage content={content} />
 
 
49
  );
50
- }
51
-
52
- const UserChatMessage: React.FC<{
53
- content: string;
54
- mediaUrl?: string;
55
- }> = ({ content, mediaUrl }) => {
56
  return (
57
- <div className="group relative mb-6 flex rounded-md bg-muted p-6 ml-auto mr-0 w-3/5">
58
- <div className="flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-background">
59
- <IconUser />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  </div>
61
- <div className="flex-1 px-1 ml-4 space-y-3 overflow-hidden">
62
- <p>{content}</p>
63
- {mediaUrl && (
64
- <>
65
- {mediaUrl?.endsWith('.mp4') ? (
66
- <video src={mediaUrl} controls width={500} height={500} />
67
- ) : (
68
- <Img
69
- src={mediaUrl}
70
- alt={mediaUrl}
71
- quality={100}
72
- sizes="(min-width: 66em) 15vw,
73
- (min-width: 44em) 20vw,
74
- 100vw"
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 className="max-w-5xl" onOpenAutoFocus={e => e.preventDefault()}>
 
 
 
117
  <Table>
118
  <TableHeader>
119
  <TableRow className="border-primary/50">
@@ -170,144 +205,4 @@ const ChunkPayloadAction: React.FC<{
170
  }
171
  };
172
 
173
- const CodeResultDisplay: React.FC<{
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
- '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
  >
 
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: true,
 
 
 
 
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
- logs: {
15
- stderr: string[];
16
- stdout: string[];
17
- };
18
- results: Array<{
19
- png?: string;
20
- mp4?: string;
21
- text: string;
22
- is_main_result: boolean;
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 chatViewMode = atom<'chat' | 'chat-all'>('chat');
 
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);