Spaces:
Running
Running
feat: project analysis (#27)
Browse files<img width="1904" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/5669963/b8e9e7bb-7248-4c8f-8648-8087c7d99b7c">
- app/chat/[id]/page.tsx +4 -9
- app/project/[projectId]/page.tsx +3 -5
- components/Header.tsx +11 -8
- components/chat-sidebar/ChatAdminToggle.tsx +1 -1
- components/chat-sidebar/ChatCard.tsx +2 -1
- components/chat/ChatDataLoad.tsx +0 -11
- components/chat/ChatList.tsx +1 -1
- components/chat/ChatMessage.tsx +27 -8
- components/chat/Composer.tsx +109 -108
- components/chat/ImageList.tsx +0 -76
- components/chat/index.tsx +16 -16
- components/project-sidebar/ProjectCard.tsx +2 -2
- components/project/Chat.tsx +0 -32
- components/project/MediaTile.tsx +12 -1
- components/project/ProjectChat.tsx +59 -0
- components/ui/Img.tsx +3 -2
- lib/fetch/index.ts +4 -3
- lib/hooks/useVisionAgent.tsx +26 -42
- lib/types.ts +1 -1
- state/{index.ts → chat.ts} +0 -5
- state/media.ts +3 -0
app/chat/[id]/page.tsx
CHANGED
@@ -1,6 +1,5 @@
|
|
1 |
-
import
|
2 |
-
import {
|
3 |
-
import Loading from '@/components/ui/Loading';
|
4 |
|
5 |
interface PageProps {
|
6 |
params: {
|
@@ -10,10 +9,6 @@ interface PageProps {
|
|
10 |
|
11 |
export default async function Page({ params }: PageProps) {
|
12 |
const { id: chatId } = params;
|
13 |
-
|
14 |
-
return
|
15 |
-
<Suspense fallback={<Loading />}>
|
16 |
-
<ChatDataLoad chatId={chatId} />
|
17 |
-
</Suspense>
|
18 |
-
);
|
19 |
}
|
|
|
1 |
+
import { getKVChat } from '@/lib/kv/chat';
|
2 |
+
import { Chat } from '@/components/chat';
|
|
|
3 |
|
4 |
interface PageProps {
|
5 |
params: {
|
|
|
9 |
|
10 |
export default async function Page({ params }: PageProps) {
|
11 |
const { id: chatId } = params;
|
12 |
+
const chat = await getKVChat(chatId);
|
13 |
+
return <Chat chat={chat} />;
|
|
|
|
|
|
|
|
|
14 |
}
|
app/project/[projectId]/page.tsx
CHANGED
@@ -1,8 +1,6 @@
|
|
1 |
import MediaGrid from '@/components/project/MediaGrid';
|
2 |
import { fetchProjectMedia } from '@/lib/fetch';
|
3 |
-
import
|
4 |
-
import Loading from '../../../components/ui/Loading';
|
5 |
-
import Chat from '@/components/project/Chat';
|
6 |
|
7 |
interface PageProps {
|
8 |
params: {
|
@@ -16,13 +14,13 @@ export default async function Page({ params }: PageProps) {
|
|
16 |
const mediaList = await fetchProjectMedia({ projectId: Number(projectId) });
|
17 |
|
18 |
return (
|
19 |
-
<div className="
|
20 |
<div className="flex h-full">
|
21 |
<div className="w-1/2 relative border-r border-gray-300 overflow-auto">
|
22 |
<MediaGrid mediaList={mediaList} />
|
23 |
</div>
|
24 |
<div className="w-1/2 relative overflow-auto">
|
25 |
-
<
|
26 |
</div>
|
27 |
</div>
|
28 |
</div>
|
|
|
1 |
import MediaGrid from '@/components/project/MediaGrid';
|
2 |
import { fetchProjectMedia } from '@/lib/fetch';
|
3 |
+
import ProjectChat from '@/components/project/ProjectChat';
|
|
|
|
|
4 |
|
5 |
interface PageProps {
|
6 |
params: {
|
|
|
14 |
const mediaList = await fetchProjectMedia({ projectId: Number(projectId) });
|
15 |
|
16 |
return (
|
17 |
+
<div className="pt-4 md:pt-10 h-full">
|
18 |
<div className="flex h-full">
|
19 |
<div className="w-1/2 relative border-r border-gray-300 overflow-auto">
|
20 |
<MediaGrid mediaList={mediaList} />
|
21 |
</div>
|
22 |
<div className="w-1/2 relative overflow-auto">
|
23 |
+
<ProjectChat mediaList={mediaList} />
|
24 |
</div>
|
25 |
</div>
|
26 |
</div>
|
components/Header.tsx
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
import * as React from 'react';
|
2 |
import Link from 'next/link';
|
3 |
|
4 |
-
import { auth } from '@/auth';
|
5 |
import { Button } from '@/components/ui/Button';
|
6 |
import { UserMenu } from '@/components/UserMenu';
|
7 |
import {
|
@@ -15,12 +15,10 @@ import { redirect } from 'next/navigation';
|
|
15 |
|
16 |
export async function Header() {
|
17 |
const session = await auth();
|
|
|
18 |
return (
|
19 |
<header className="sticky top-0 z-50 flex items-center justify-end w-full h-16 px-8 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
|
20 |
-
{/* <
|
21 |
-
<Link href="/project">Projects</Link>
|
22 |
-
</Button> */}
|
23 |
-
<Tooltip>
|
24 |
<TooltipTrigger asChild>
|
25 |
<Button variant="link" asChild className="mr-2">
|
26 |
<Link href="/chat">
|
@@ -29,10 +27,15 @@ export async function Header() {
|
|
29 |
</Button>
|
30 |
</TooltipTrigger>
|
31 |
<TooltipContent>New chat</TooltipContent>
|
32 |
-
</Tooltip>
|
33 |
-
{
|
|
|
|
|
|
|
|
|
|
|
34 |
<Link href="/chat">Chat</Link>
|
35 |
-
</Button>
|
36 |
<IconSeparator className="size-6 text-muted-foreground/50" />
|
37 |
<div className="flex items-center">
|
38 |
{session?.user ? <UserMenu user={session!.user} /> : <LoginMenu />}
|
|
|
1 |
import * as React from 'react';
|
2 |
import Link from 'next/link';
|
3 |
|
4 |
+
import { auth, authEmail } from '@/auth';
|
5 |
import { Button } from '@/components/ui/Button';
|
6 |
import { UserMenu } from '@/components/UserMenu';
|
7 |
import {
|
|
|
15 |
|
16 |
export async function Header() {
|
17 |
const session = await auth();
|
18 |
+
const { isAdmin } = await authEmail();
|
19 |
return (
|
20 |
<header className="sticky top-0 z-50 flex items-center justify-end w-full h-16 px-8 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
|
21 |
+
{/* <Tooltip>
|
|
|
|
|
|
|
22 |
<TooltipTrigger asChild>
|
23 |
<Button variant="link" asChild className="mr-2">
|
24 |
<Link href="/chat">
|
|
|
27 |
</Button>
|
28 |
</TooltipTrigger>
|
29 |
<TooltipContent>New chat</TooltipContent>
|
30 |
+
</Tooltip> */}
|
31 |
+
{isAdmin && (
|
32 |
+
<Button variant="link" asChild className="mr-2">
|
33 |
+
<Link href="/project">Projects (Internal)</Link>
|
34 |
+
</Button>
|
35 |
+
)}
|
36 |
+
<Button variant="link" asChild className="mr-2">
|
37 |
<Link href="/chat">Chat</Link>
|
38 |
+
</Button>
|
39 |
<IconSeparator className="size-6 text-muted-foreground/50" />
|
40 |
<div className="flex items-center">
|
41 |
{session?.user ? <UserMenu user={session!.user} /> : <LoginMenu />}
|
components/chat-sidebar/ChatAdminToggle.tsx
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
'use client';
|
2 |
|
3 |
-
import { chatViewMode } from '@/state';
|
4 |
import { useAtom } from 'jotai';
|
5 |
import React from 'react';
|
6 |
|
|
|
1 |
'use client';
|
2 |
|
3 |
+
import { chatViewMode } from '@/state/chat';
|
4 |
import { useAtom } from 'jotai';
|
5 |
import React from 'react';
|
6 |
|
components/chat-sidebar/ChatCard.tsx
CHANGED
@@ -8,6 +8,7 @@ import { ChatEntity } from '@/lib/types';
|
|
8 |
import Image from 'next/image';
|
9 |
import clsx from 'clsx';
|
10 |
import Img from '../ui/Img';
|
|
|
11 |
// import { format } from 'date-fns';
|
12 |
|
13 |
type ChatCardProps = PropsWithChildren<{
|
@@ -50,7 +51,7 @@ const ChatCard: React.FC<ChatCardProps> = ({ chat }) => {
|
|
50 |
<div className="flex items-start flex-col h-full ml-3 w-3/4">
|
51 |
<p className="text-sm mb-1">{title}</p>
|
52 |
<p className="text-xs text-gray-500">
|
53 |
-
{updatedAt ?
|
54 |
</p>
|
55 |
</div>
|
56 |
</div>
|
|
|
8 |
import Image from 'next/image';
|
9 |
import clsx from 'clsx';
|
10 |
import Img from '../ui/Img';
|
11 |
+
import { format } from 'date-fns';
|
12 |
// import { format } from 'date-fns';
|
13 |
|
14 |
type ChatCardProps = PropsWithChildren<{
|
|
|
51 |
<div className="flex items-start flex-col h-full ml-3 w-3/4">
|
52 |
<p className="text-sm mb-1">{title}</p>
|
53 |
<p className="text-xs text-gray-500">
|
54 |
+
{updatedAt ? format(Number(updatedAt), 'yyyy-MM-dd') : '-'}
|
55 |
</p>
|
56 |
</div>
|
57 |
</div>
|
components/chat/ChatDataLoad.tsx
DELETED
@@ -1,11 +0,0 @@
|
|
1 |
-
import { getKVChat } from '@/lib/kv/chat';
|
2 |
-
import { Chat } from '.';
|
3 |
-
|
4 |
-
export interface ChatDataLoadProps {
|
5 |
-
chatId: string;
|
6 |
-
}
|
7 |
-
|
8 |
-
export default async function ChatDataLoad({ chatId }: ChatDataLoadProps) {
|
9 |
-
const chat = await getKVChat(chatId);
|
10 |
-
return <Chat chat={chat} />;
|
11 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/chat/ChatList.tsx
CHANGED
@@ -10,7 +10,7 @@ export interface ChatList {
|
|
10 |
|
11 |
export function ChatList({ messages }: ChatList) {
|
12 |
return (
|
13 |
-
<div className="relative mx-auto max-w-5xl px-8 pr-12
|
14 |
{messages
|
15 |
// .filter(message => message.role !== 'system')
|
16 |
.map((message, index) => (
|
|
|
10 |
|
11 |
export function ChatList({ messages }: ChatList) {
|
12 |
return (
|
13 |
+
<div className="relative mx-auto max-w-5xl px-8 pr-12">
|
14 |
{messages
|
15 |
// .filter(message => message.role !== 'system')
|
16 |
.map((message, index) => (
|
components/chat/ChatMessage.tsx
CHANGED
@@ -11,6 +11,11 @@ import { IconOpenAI, IconUser } from '@/components/ui/Icons';
|
|
11 |
import { ChatMessageActions } from '@/components/chat/ChatMessageActions';
|
12 |
import { MessageBase } from '../../lib/types';
|
13 |
import { useCleanedUpMessages } from '@/lib/hooks/useCleanedUpMessages';
|
|
|
|
|
|
|
|
|
|
|
14 |
import Img from '../ui/Img';
|
15 |
|
16 |
export interface ChatMessageProps {
|
@@ -77,14 +82,28 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
|
77 |
},
|
78 |
img(props) {
|
79 |
return (
|
80 |
-
<
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
|
|
|
|
|
|
86 |
100vw"
|
87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
);
|
89 |
},
|
90 |
code({ node, inline, className, children, ...props }) {
|
@@ -120,7 +139,7 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
|
120 |
>
|
121 |
{content}
|
122 |
</MemoizedReactMarkdown>
|
123 |
-
<ChatMessageActions message={message} />
|
124 |
</div>
|
125 |
</div>
|
126 |
);
|
|
|
11 |
import { ChatMessageActions } from '@/components/chat/ChatMessageActions';
|
12 |
import { MessageBase } from '../../lib/types';
|
13 |
import { useCleanedUpMessages } from '@/lib/hooks/useCleanedUpMessages';
|
14 |
+
import {
|
15 |
+
Tooltip,
|
16 |
+
TooltipContent,
|
17 |
+
TooltipTrigger,
|
18 |
+
} from '@/components/ui/Tooltip';
|
19 |
import Img from '../ui/Img';
|
20 |
|
21 |
export interface ChatMessageProps {
|
|
|
82 |
},
|
83 |
img(props) {
|
84 |
return (
|
85 |
+
<Tooltip>
|
86 |
+
<TooltipTrigger asChild>
|
87 |
+
<Img
|
88 |
+
src={props.src ?? '/landing.png'}
|
89 |
+
alt={props.alt ?? 'answer-image'}
|
90 |
+
quality={100}
|
91 |
+
className="cursor-zoom-in"
|
92 |
+
sizes="(min-width: 66em) 25vw,
|
93 |
+
(min-width: 44em) 40vw,
|
94 |
100vw"
|
95 |
+
/>
|
96 |
+
</TooltipTrigger>
|
97 |
+
<TooltipContent>
|
98 |
+
<Img
|
99 |
+
className="m-2"
|
100 |
+
src={props.src ?? '/landing.png'}
|
101 |
+
alt={props.alt ?? 'answer-image'}
|
102 |
+
quality={100}
|
103 |
+
width={500}
|
104 |
+
/>
|
105 |
+
</TooltipContent>
|
106 |
+
</Tooltip>
|
107 |
);
|
108 |
},
|
109 |
code({ node, inline, className, children, ...props }) {
|
|
|
139 |
>
|
140 |
{content}
|
141 |
</MemoizedReactMarkdown>
|
142 |
+
{/* <ChatMessageActions message={message} /> */}
|
143 |
</div>
|
144 |
</div>
|
145 |
);
|
components/chat/Composer.tsx
CHANGED
@@ -57,138 +57,139 @@ export function Composer({
|
|
57 |
}, []);
|
58 |
|
59 |
return (
|
60 |
-
<div className="fixed inset-x-0 bottom-0 w-full animate-in duration-300 ease-in-out peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px] h-[178px]">
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
}
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
ref={formRef}
|
78 |
-
className="h-full"
|
79 |
-
>
|
80 |
-
<div className="relative flex px-8 pl-2 overflow-hidden size-full bg-background sm:rounded-md sm:border sm:px-12 sm:pl-2 items-start">
|
81 |
-
{url && (
|
82 |
-
<div className="w-1/5 p-2 h-full flex items-center justify-center relative">
|
83 |
-
<Tooltip>
|
84 |
-
<TooltipTrigger asChild>
|
85 |
-
<Img
|
86 |
-
src={url}
|
87 |
-
className="cursor-zoom-in"
|
88 |
-
alt="preview-image"
|
89 |
-
/>
|
90 |
-
</TooltipTrigger>
|
91 |
-
<TooltipContent>
|
92 |
-
<Img
|
93 |
-
src={url}
|
94 |
-
className="m-2"
|
95 |
-
quality={100}
|
96 |
-
alt="zoomed-in-image"
|
97 |
-
/>
|
98 |
-
</TooltipContent>
|
99 |
-
</Tooltip>
|
100 |
-
</div>
|
101 |
)}
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
className={cn(
|
121 |
-
'absolute top-3 right-4 transition-opacity duration-300',
|
122 |
-
isAtBottom ? 'opacity-0' : 'opacity-100',
|
123 |
-
)}
|
124 |
-
>
|
125 |
<Tooltip>
|
126 |
<TooltipTrigger asChild>
|
127 |
<Button
|
128 |
variant="outline"
|
129 |
size="icon"
|
130 |
className="bg-background"
|
131 |
-
onClick={() =>
|
132 |
>
|
133 |
-
<
|
134 |
</Button>
|
135 |
</TooltipTrigger>
|
136 |
-
<TooltipContent>
|
137 |
</Tooltip>
|
138 |
-
|
139 |
-
|
140 |
-
<div className="absolute bottom-14 right-4">
|
141 |
-
{isLoading ? (
|
142 |
<Tooltip>
|
143 |
<TooltipTrigger asChild>
|
144 |
<Button
|
145 |
variant="outline"
|
146 |
size="icon"
|
147 |
className="bg-background"
|
148 |
-
onClick={() =>
|
149 |
>
|
150 |
-
<
|
151 |
</Button>
|
152 |
</TooltipTrigger>
|
153 |
-
<TooltipContent>
|
154 |
</Tooltip>
|
155 |
-
)
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
</div>
|
173 |
-
{/* Submit Icon */}
|
174 |
-
<div className="absolute bottom-3 right-4">
|
175 |
-
<Tooltip>
|
176 |
-
<TooltipTrigger asChild>
|
177 |
-
<Button
|
178 |
-
type="submit"
|
179 |
-
size="icon"
|
180 |
-
disabled={isLoading || input === ''}
|
181 |
-
>
|
182 |
-
<IconArrowElbow />
|
183 |
-
</Button>
|
184 |
-
</TooltipTrigger>
|
185 |
-
<TooltipContent>Send message</TooltipContent>
|
186 |
-
</Tooltip>
|
187 |
-
</div>
|
188 |
</div>
|
189 |
-
</
|
190 |
-
</
|
191 |
</div>
|
192 |
</div>
|
|
|
193 |
);
|
194 |
}
|
|
|
57 |
}, []);
|
58 |
|
59 |
return (
|
60 |
+
// <div className="fixed inset-x-0 bottom-0 w-full animate-in duration-300 ease-in-out peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px] h-[178px]">
|
61 |
+
<div className="mx-auto sm:max-w-3xl sm:px-4 h-full">
|
62 |
+
<div className="px-4 py-2 space-y-4 border-t shadow-lg bg-background sm:rounded-t-xl sm:border md:py-4 h-full">
|
63 |
+
<form
|
64 |
+
onSubmit={async e => {
|
65 |
+
e.preventDefault();
|
66 |
+
if (!input?.trim()) {
|
67 |
+
return;
|
68 |
+
}
|
69 |
+
setInput('');
|
70 |
+
await append({
|
71 |
+
id,
|
72 |
+
content: input,
|
73 |
+
role: 'user',
|
74 |
+
});
|
75 |
+
scrollToBottom();
|
76 |
+
}}
|
77 |
+
ref={formRef}
|
78 |
+
className="h-full"
|
79 |
+
>
|
80 |
+
<div className="relative flex px-8 pl-2 overflow-hidden size-full bg-background sm:rounded-md sm:border sm:px-12 sm:pl-2 items-start">
|
81 |
+
{url && (
|
82 |
+
<div className="w-1/5 p-2 h-full flex items-center justify-center relative">
|
83 |
+
<Tooltip>
|
84 |
+
<TooltipTrigger asChild>
|
85 |
+
<Img
|
86 |
+
src={url}
|
87 |
+
className="cursor-zoom-in"
|
88 |
+
alt="preview-image"
|
89 |
+
/>
|
90 |
+
</TooltipTrigger>
|
91 |
+
<TooltipContent>
|
92 |
+
<Img
|
93 |
+
src={url}
|
94 |
+
className="m-2"
|
95 |
+
quality={100}
|
96 |
+
width={500}
|
97 |
+
alt="zoomed-in-image"
|
98 |
+
/>
|
99 |
+
</TooltipContent>
|
100 |
+
</Tooltip>
|
101 |
+
</div>
|
102 |
+
)}
|
103 |
+
<Textarea
|
104 |
+
ref={inputRef}
|
105 |
+
tabIndex={0}
|
106 |
+
onKeyDown={onKeyDown}
|
107 |
+
rows={1}
|
108 |
+
value={input}
|
109 |
+
disabled={isLoading}
|
110 |
+
onChange={e => setInput(e.target.value)}
|
111 |
+
placeholder={
|
112 |
+
isLoading
|
113 |
+
? 'Vision Agent is thinking...'
|
114 |
+
: 'Ask question about the image.'
|
115 |
}
|
116 |
+
spellCheck={false}
|
117 |
+
className="min-h-[60px] w-4/5 resize-none bg-transparent px-4 py-[1.3em] focus-within:outline-none sm:text-sm"
|
118 |
+
/>
|
119 |
+
{/* Scroll to bottom Icon */}
|
120 |
+
<div
|
121 |
+
className={cn(
|
122 |
+
'absolute top-3 right-4 transition-opacity duration-300',
|
123 |
+
isAtBottom ? 'opacity-0' : 'opacity-100',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
)}
|
125 |
+
>
|
126 |
+
<Tooltip>
|
127 |
+
<TooltipTrigger asChild>
|
128 |
+
<Button
|
129 |
+
variant="outline"
|
130 |
+
size="icon"
|
131 |
+
className="bg-background"
|
132 |
+
onClick={() => scrollToBottom()}
|
133 |
+
>
|
134 |
+
<IconArrowDown />
|
135 |
+
</Button>
|
136 |
+
</TooltipTrigger>
|
137 |
+
<TooltipContent>Scroll to bottom</TooltipContent>
|
138 |
+
</Tooltip>
|
139 |
+
</div>
|
140 |
+
{/* Stop / Regenerate Icon */}
|
141 |
+
<div className="absolute bottom-14 right-4">
|
142 |
+
{isLoading ? (
|
|
|
|
|
|
|
|
|
|
|
143 |
<Tooltip>
|
144 |
<TooltipTrigger asChild>
|
145 |
<Button
|
146 |
variant="outline"
|
147 |
size="icon"
|
148 |
className="bg-background"
|
149 |
+
onClick={() => stop()}
|
150 |
>
|
151 |
+
<IconStop />
|
152 |
</Button>
|
153 |
</TooltipTrigger>
|
154 |
+
<TooltipContent>Stop generating</TooltipContent>
|
155 |
</Tooltip>
|
156 |
+
) : (
|
157 |
+
messages?.length >= 2 && (
|
|
|
|
|
158 |
<Tooltip>
|
159 |
<TooltipTrigger asChild>
|
160 |
<Button
|
161 |
variant="outline"
|
162 |
size="icon"
|
163 |
className="bg-background"
|
164 |
+
onClick={() => reload()}
|
165 |
>
|
166 |
+
<IconRefresh />
|
167 |
</Button>
|
168 |
</TooltipTrigger>
|
169 |
+
<TooltipContent>Regenerate response</TooltipContent>
|
170 |
</Tooltip>
|
171 |
+
)
|
172 |
+
)}
|
173 |
+
</div>
|
174 |
+
{/* Submit Icon */}
|
175 |
+
<div className="absolute bottom-3 right-4">
|
176 |
+
<Tooltip>
|
177 |
+
<TooltipTrigger asChild>
|
178 |
+
<Button
|
179 |
+
type="submit"
|
180 |
+
size="icon"
|
181 |
+
disabled={isLoading || input === ''}
|
182 |
+
>
|
183 |
+
<IconArrowElbow />
|
184 |
+
</Button>
|
185 |
+
</TooltipTrigger>
|
186 |
+
<TooltipContent>Send message</TooltipContent>
|
187 |
+
</Tooltip>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
188 |
</div>
|
189 |
+
</div>
|
190 |
+
</form>
|
191 |
</div>
|
192 |
</div>
|
193 |
+
// </div>
|
194 |
);
|
195 |
}
|
components/chat/ImageList.tsx
DELETED
@@ -1,76 +0,0 @@
|
|
1 |
-
import React, { useCallback } from 'react';
|
2 |
-
import useImageUpload from '../../lib/hooks/useImageUpload';
|
3 |
-
import { useAtom, useAtomValue } from 'jotai';
|
4 |
-
import { datasetAtom } from '../../state';
|
5 |
-
import Image from 'next/image';
|
6 |
-
import { produce } from 'immer';
|
7 |
-
|
8 |
-
export interface ImageListProps {}
|
9 |
-
|
10 |
-
const ImageList: React.FC<ImageListProps> = () => {
|
11 |
-
const { getRootProps, getInputProps, isDragActive } = useImageUpload({
|
12 |
-
noClick: true,
|
13 |
-
});
|
14 |
-
|
15 |
-
const [dataset, setDataset] = useAtom(datasetAtom);
|
16 |
-
return (
|
17 |
-
<div className="relative size-full px-12 max-w-3xl mx-auto">
|
18 |
-
{/* {dataset.length < 10 ? (
|
19 |
-
<div className="col-span-full px-8 py-4 rounded-xl bg-blue-100 text-blue-400 mb-8">
|
20 |
-
You can upload up to 10 images max by dragging image.
|
21 |
-
</div>
|
22 |
-
) : (
|
23 |
-
<div className="col-span-full px-8 py-4 rounded-xl bg-red-100 text-red-400 mb-8">
|
24 |
-
You have reached the maximum limit of 10 images.
|
25 |
-
</div>
|
26 |
-
)} */}
|
27 |
-
<div
|
28 |
-
{...getRootProps()}
|
29 |
-
className="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-4"
|
30 |
-
>
|
31 |
-
{dataset.map(entity => {
|
32 |
-
const { url: imageSrc, name, selected } = entity;
|
33 |
-
return (
|
34 |
-
<div
|
35 |
-
key={name}
|
36 |
-
onClick={() =>
|
37 |
-
setDataset(prev =>
|
38 |
-
produce(prev, draft => {
|
39 |
-
const index = draft.findIndex(d => d.name === name);
|
40 |
-
draft[index].selected = !selected;
|
41 |
-
}),
|
42 |
-
)
|
43 |
-
}
|
44 |
-
className={`relative rounded-xl overflow-hidden shadow-md cursor-pointer transition-transform hover:scale-105 box-content ${selected ? 'border-4 border-blue-500' : ''}`}
|
45 |
-
>
|
46 |
-
<Image
|
47 |
-
src={imageSrc}
|
48 |
-
draggable={false}
|
49 |
-
alt="dataset images"
|
50 |
-
width={500}
|
51 |
-
height={500}
|
52 |
-
objectFit="cover"
|
53 |
-
className="rounded-xl"
|
54 |
-
/>
|
55 |
-
<div className="absolute bottom-0 left-0 bg-gray-800/50 text-white px-3 py-1 rounded-tr-lg">
|
56 |
-
<p className="text-xs font-bold">{name}</p>
|
57 |
-
</div>
|
58 |
-
</div>
|
59 |
-
);
|
60 |
-
})}
|
61 |
-
</div>
|
62 |
-
|
63 |
-
{isDragActive && (
|
64 |
-
<div
|
65 |
-
{...getRootProps()}
|
66 |
-
className="dropzone border-2 border-dashed border-gray-400 size-full absolute top-0 left-0 flex items-center justify-center rounded-lg cursor-pointer bg-gray-500/50"
|
67 |
-
>
|
68 |
-
<input {...getInputProps()} />
|
69 |
-
<p className="text-white">Drop the files here ...</p>
|
70 |
-
</div>
|
71 |
-
)}
|
72 |
-
</div>
|
73 |
-
);
|
74 |
-
};
|
75 |
-
|
76 |
-
export default ImageList;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/chat/index.tsx
CHANGED
@@ -1,10 +1,8 @@
|
|
1 |
'use client';
|
2 |
|
3 |
-
import { cn } from '@/lib/utils';
|
4 |
import { ChatList } from '@/components/chat/ChatList';
|
5 |
import { Composer } from '@/components/chat/Composer';
|
6 |
import { ChatEntity } from '@/lib/types';
|
7 |
-
import Image from 'next/image';
|
8 |
import useVisionAgent from '@/lib/hooks/useVisionAgent';
|
9 |
import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
|
10 |
|
@@ -22,25 +20,27 @@ export function Chat({ chat }: ChatProps) {
|
|
22 |
|
23 |
return (
|
24 |
<>
|
25 |
-
<div className="h-full overflow-auto" ref={scrollRef}>
|
26 |
<div className="pb-[200px] pt-4 md:pt-10" ref={messagesRef}>
|
27 |
<ChatList messages={messages} />
|
28 |
<div className="h-px w-full" ref={visibilityRef} />
|
29 |
</div>
|
30 |
</div>
|
31 |
-
<
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
|
|
|
|
44 |
</>
|
45 |
);
|
46 |
}
|
|
|
1 |
'use client';
|
2 |
|
|
|
3 |
import { ChatList } from '@/components/chat/ChatList';
|
4 |
import { Composer } from '@/components/chat/Composer';
|
5 |
import { ChatEntity } from '@/lib/types';
|
|
|
6 |
import useVisionAgent from '@/lib/hooks/useVisionAgent';
|
7 |
import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
|
8 |
|
|
|
20 |
|
21 |
return (
|
22 |
<>
|
23 |
+
<div className="h-full overflow-auto relative" ref={scrollRef}>
|
24 |
<div className="pb-[200px] pt-4 md:pt-10" ref={messagesRef}>
|
25 |
<ChatList messages={messages} />
|
26 |
<div className="h-px w-full" ref={visibilityRef} />
|
27 |
</div>
|
28 |
</div>
|
29 |
+
<div className="fixed inset-x-0 bottom-0 w-full animate-in duration-300 ease-in-out peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px] h-[178px]">
|
30 |
+
<Composer
|
31 |
+
id={id}
|
32 |
+
url={url}
|
33 |
+
isLoading={isLoading}
|
34 |
+
stop={stop}
|
35 |
+
append={append}
|
36 |
+
reload={reload}
|
37 |
+
messages={messages}
|
38 |
+
input={input}
|
39 |
+
setInput={setInput}
|
40 |
+
isAtBottom={isAtBottom}
|
41 |
+
scrollToBottom={scrollToBottom}
|
42 |
+
/>
|
43 |
+
</div>
|
44 |
</>
|
45 |
);
|
46 |
}
|
components/project-sidebar/ProjectCard.tsx
CHANGED
@@ -26,14 +26,14 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ projectInfo }) => {
|
|
26 |
return (
|
27 |
<Link
|
28 |
className={cn(
|
29 |
-
'p-4 m-2 bg-
|
30 |
Number(projectIdFromParam) === id && 'border-gray-500',
|
31 |
)}
|
32 |
href={`/project/${id}`}
|
33 |
>
|
34 |
<div className="overflow-hidden">
|
35 |
<p className="text-xs text-gray-500">{orgName}</p>
|
36 |
-
<p className="text-sm font-medium text-
|
37 |
<p className="text-xs text-gray-500">{formattedDate}</p>
|
38 |
</div>
|
39 |
</Link>
|
|
|
26 |
return (
|
27 |
<Link
|
28 |
className={cn(
|
29 |
+
'p-4 m-2 bg-background l:h-[250px] rounded-xl shadow-md flex items-center border border-transparent hover:border-gray-500 transition-all cursor-pointer',
|
30 |
Number(projectIdFromParam) === id && 'border-gray-500',
|
31 |
)}
|
32 |
href={`/project/${id}`}
|
33 |
>
|
34 |
<div className="overflow-hidden">
|
35 |
<p className="text-xs text-gray-500">{orgName}</p>
|
36 |
+
<p className="text-sm font-medium text-gray mb-1">{name}</p>
|
37 |
<p className="text-xs text-gray-500">{formattedDate}</p>
|
38 |
</div>
|
39 |
</Link>
|
components/project/Chat.tsx
DELETED
@@ -1,32 +0,0 @@
|
|
1 |
-
'use client';
|
2 |
-
|
3 |
-
import { MediaDetails } from '@/lib/fetch';
|
4 |
-
import useChatWithMedia from '@/lib/hooks/useChatWithMedia';
|
5 |
-
import React from 'react';
|
6 |
-
import { ChatList } from '../chat/ChatList';
|
7 |
-
// import { ChatPanel } from '../chat/ChatPanel';
|
8 |
-
|
9 |
-
export interface ChatProps {
|
10 |
-
mediaList: MediaDetails[];
|
11 |
-
}
|
12 |
-
|
13 |
-
const Chat: React.FC<ChatProps> = ({ mediaList }) => {
|
14 |
-
const { messages, append, reload, stop, isLoading, input, setInput } =
|
15 |
-
useChatWithMedia(mediaList);
|
16 |
-
return (
|
17 |
-
<>
|
18 |
-
<ChatList messages={messages} />
|
19 |
-
{/* <ChatPanel
|
20 |
-
isLoading={isLoading}
|
21 |
-
stop={stop}
|
22 |
-
append={append}
|
23 |
-
reload={reload}
|
24 |
-
messages={messages}
|
25 |
-
input={input}
|
26 |
-
setInput={setInput}
|
27 |
-
/> */}
|
28 |
-
</>
|
29 |
-
);
|
30 |
-
};
|
31 |
-
|
32 |
-
export default Chat;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/project/MediaTile.tsx
CHANGED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
import React from 'react';
|
2 |
import Image from 'next/image';
|
3 |
import {
|
@@ -6,6 +8,9 @@ import {
|
|
6 |
TooltipTrigger,
|
7 |
} from '@/components/ui/Tooltip';
|
8 |
import { MediaDetails } from '@/lib/fetch';
|
|
|
|
|
|
|
9 |
|
10 |
export interface MediaTileProps {
|
11 |
media: MediaDetails;
|
@@ -19,6 +24,8 @@ export default function MediaTile({ media }: MediaTileProps) {
|
|
19 |
name,
|
20 |
properties: { width, height },
|
21 |
} = media;
|
|
|
|
|
22 |
// const imageSrc = thumbnails.length ? thumbnails[thumbnails.length - 1] : url;
|
23 |
const imageSrc = url;
|
24 |
return (
|
@@ -30,7 +37,11 @@ export default function MediaTile({ media }: MediaTileProps) {
|
|
30 |
alt="dataset images"
|
31 |
width={width}
|
32 |
height={height}
|
33 |
-
|
|
|
|
|
|
|
|
|
34 |
/>
|
35 |
</TooltipTrigger>
|
36 |
<TooltipContent>
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
import React from 'react';
|
4 |
import Image from 'next/image';
|
5 |
import {
|
|
|
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;
|
|
|
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 (
|
|
|
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>
|
components/project/ProjectChat.tsx
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { MediaDetails } from '@/lib/fetch';
|
4 |
+
import React from 'react';
|
5 |
+
import { ChatList } from '../chat/ChatList';
|
6 |
+
import useVisionAgent from '@/lib/hooks/useVisionAgent';
|
7 |
+
import { nanoid } from '@/lib/utils';
|
8 |
+
import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
|
9 |
+
import { Composer } from '../chat/Composer';
|
10 |
+
import { useAtomValue } from 'jotai';
|
11 |
+
import { selectedMediaIdAtom } from '@/state/media';
|
12 |
+
|
13 |
+
export interface ChatProps {
|
14 |
+
mediaList: MediaDetails[];
|
15 |
+
}
|
16 |
+
|
17 |
+
const ProjectChat: React.FC<ChatProps> = ({ mediaList }) => {
|
18 |
+
const selectedMediaId = useAtomValue(selectedMediaIdAtom);
|
19 |
+
// fallback to the first media
|
20 |
+
const selectedMedia =
|
21 |
+
mediaList.find(media => media.id === selectedMediaId) ?? mediaList[0];
|
22 |
+
const { messages, append, reload, stop, isLoading, input, setInput } =
|
23 |
+
useVisionAgent({
|
24 |
+
url: selectedMedia.url,
|
25 |
+
messages: [],
|
26 |
+
user: 'does-not-matter@landing.ai',
|
27 |
+
updatedAt: Date.now(),
|
28 |
+
});
|
29 |
+
|
30 |
+
const { messagesRef, scrollRef, visibilityRef, isAtBottom, scrollToBottom } =
|
31 |
+
useScrollAnchor();
|
32 |
+
|
33 |
+
return (
|
34 |
+
<>
|
35 |
+
<div className="h-full overflow-auto" ref={scrollRef}>
|
36 |
+
<div className="pb-[200px] pt-4 md:pt-10" ref={messagesRef}>
|
37 |
+
<ChatList messages={messages} />
|
38 |
+
<div className="h-px w-full" ref={visibilityRef} />
|
39 |
+
</div>
|
40 |
+
</div>
|
41 |
+
<div className="absolute inset-x-0 bottom-0 w-full h-[178px]">
|
42 |
+
<Composer
|
43 |
+
url={selectedMedia.url}
|
44 |
+
isLoading={isLoading}
|
45 |
+
stop={stop}
|
46 |
+
append={append}
|
47 |
+
reload={reload}
|
48 |
+
messages={messages}
|
49 |
+
input={input}
|
50 |
+
setInput={setInput}
|
51 |
+
isAtBottom={isAtBottom}
|
52 |
+
scrollToBottom={scrollToBottom}
|
53 |
+
/>
|
54 |
+
</div>
|
55 |
+
</>
|
56 |
+
);
|
57 |
+
};
|
58 |
+
|
59 |
+
export default ProjectChat;
|
components/ui/Img.tsx
CHANGED
@@ -29,10 +29,11 @@ const Img = React.forwardRef<
|
|
29 |
className={cn('rounded-md', className)}
|
30 |
onLoad={e => {
|
31 |
const img = e.target as HTMLImageElement;
|
|
|
32 |
startTransition(() => {
|
33 |
setDimensions({
|
34 |
-
width: img.naturalWidth,
|
35 |
-
height: img.naturalHeight,
|
36 |
});
|
37 |
});
|
38 |
return onLoad?.(e);
|
|
|
29 |
className={cn('rounded-md', className)}
|
30 |
onLoad={e => {
|
31 |
const img = e.target as HTMLImageElement;
|
32 |
+
const aspectRatio = img.naturalWidth / img.naturalHeight;
|
33 |
startTransition(() => {
|
34 |
setDimensions({
|
35 |
+
width: width ?? img.naturalWidth,
|
36 |
+
height: width ? Number(width) / aspectRatio : img.naturalHeight,
|
37 |
});
|
38 |
});
|
39 |
return onLoad?.(e);
|
lib/fetch/index.ts
CHANGED
@@ -51,7 +51,8 @@ const clefApiBuilder = <Params extends object | void, Resp>(
|
|
51 |
method: options?.method ?? 'GET',
|
52 |
headers: {
|
53 |
sessionuser: JSON.stringify(sessionUser), // faked production user
|
54 |
-
apikey: 'land_sk_DKeoYtaZZrYqJ9TMMiXe4BIQgJcZ0s3XAoB0JT3jv73FFqnr6k', // dev key
|
|
|
55 |
'X-ROUTE': 'mingruizhang-landing',
|
56 |
},
|
57 |
};
|
@@ -95,7 +96,7 @@ export type ProjectBaseInfo = {
|
|
95 |
* @author https://github.com/landing-ai/landing-platform/blob/mingrui-04-08-meaningful-project/packages/server-clef/src/main_app/controllers/admin/get_admin_meaningful_project_controller.ts
|
96 |
*/
|
97 |
export const fetchRecentProjectList = clefApiBuilder<void, ProjectBaseInfo[]>(
|
98 |
-
'api/admin/projects/recent',
|
99 |
);
|
100 |
|
101 |
export type MediaDetails = {
|
@@ -120,4 +121,4 @@ export type MediaDetails = {
|
|
120 |
export const fetchProjectMedia = clefApiBuilder<
|
121 |
{ projectId: number },
|
122 |
MediaDetails[]
|
123 |
-
>('api/admin/project/media');
|
|
|
51 |
method: options?.method ?? 'GET',
|
52 |
headers: {
|
53 |
sessionuser: JSON.stringify(sessionUser), // faked production user
|
54 |
+
// apikey: 'land_sk_DKeoYtaZZrYqJ9TMMiXe4BIQgJcZ0s3XAoB0JT3jv73FFqnr6k', // dev key
|
55 |
+
apikey: 'land_sk_Z0AWhSGURwC8UVTjyZDH7VMsYod6jfTK79fC2RTGD85NkV6hwO', // prod key
|
56 |
'X-ROUTE': 'mingruizhang-landing',
|
57 |
},
|
58 |
};
|
|
|
96 |
* @author https://github.com/landing-ai/landing-platform/blob/mingrui-04-08-meaningful-project/packages/server-clef/src/main_app/controllers/admin/get_admin_meaningful_project_controller.ts
|
97 |
*/
|
98 |
export const fetchRecentProjectList = clefApiBuilder<void, ProjectBaseInfo[]>(
|
99 |
+
'api/admin/vision-agent/projects/recent',
|
100 |
);
|
101 |
|
102 |
export type MediaDetails = {
|
|
|
121 |
export const fetchProjectMedia = clefApiBuilder<
|
122 |
{ projectId: number },
|
123 |
MediaDetails[]
|
124 |
+
>('api/admin/vision-agent/project/media');
|
lib/hooks/useVisionAgent.tsx
CHANGED
@@ -3,7 +3,7 @@ import { toast } from 'react-hot-toast';
|
|
3 |
import { useEffect, useState } from 'react';
|
4 |
import { ChatEntity, MessageBase, SignedPayload } from '../types';
|
5 |
import { saveKVChatMessage } from '../kv/chat';
|
6 |
-
import { fetcher } from '../utils';
|
7 |
import { getCleanedUpMessages } from './useCleanedUpMessages';
|
8 |
import { CLEANED_SEPARATOR } from '../constants';
|
9 |
|
@@ -58,7 +58,6 @@ const useVisionAgent = (chat: ChatEntity) => {
|
|
58 |
setMessages,
|
59 |
error,
|
60 |
} = useChat({
|
61 |
-
sendExtraMessageFields: true,
|
62 |
api: '/api/vision-agent',
|
63 |
onResponse(response) {
|
64 |
if (response.status !== 200) {
|
@@ -70,7 +69,7 @@ const useVisionAgent = (chat: ChatEntity) => {
|
|
70 |
if (images?.length) {
|
71 |
const publicUrls = await Promise.all(
|
72 |
images.map((image, index) =>
|
73 |
-
uploadBase64(image, message.id, id, index),
|
74 |
),
|
75 |
);
|
76 |
const newContent = publicUrls.reduce((accum, url, index) => {
|
@@ -83,13 +82,25 @@ const useVisionAgent = (chat: ChatEntity) => {
|
|
83 |
...message,
|
84 |
content: logs + CLEANED_SEPARATOR + newContent,
|
85 |
};
|
86 |
-
|
87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
} else {
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
|
|
|
|
93 |
}
|
94 |
},
|
95 |
initialMessages: initialMessages,
|
@@ -99,35 +110,6 @@ const useVisionAgent = (chat: ChatEntity) => {
|
|
99 |
},
|
100 |
});
|
101 |
|
102 |
-
const [loadingDots, setLoadingDots] = useState('');
|
103 |
-
|
104 |
-
useEffect(() => {
|
105 |
-
let loadingInterval: NodeJS.Timeout;
|
106 |
-
|
107 |
-
if (isLoading) {
|
108 |
-
loadingInterval = setInterval(() => {
|
109 |
-
setLoadingDots(prevMessage => {
|
110 |
-
switch (prevMessage) {
|
111 |
-
case '':
|
112 |
-
return '.';
|
113 |
-
case '.':
|
114 |
-
return '..';
|
115 |
-
case '..':
|
116 |
-
return '...';
|
117 |
-
case '...':
|
118 |
-
return '';
|
119 |
-
default:
|
120 |
-
return '';
|
121 |
-
}
|
122 |
-
});
|
123 |
-
}, 500);
|
124 |
-
}
|
125 |
-
|
126 |
-
return () => {
|
127 |
-
clearInterval(loadingInterval);
|
128 |
-
};
|
129 |
-
}, [isLoading]);
|
130 |
-
|
131 |
useEffect(() => {
|
132 |
if (
|
133 |
!isLoading &&
|
@@ -140,19 +122,21 @@ const useVisionAgent = (chat: ChatEntity) => {
|
|
140 |
|
141 |
const assistantLoadingMessage = {
|
142 |
id: 'loading',
|
143 |
-
content:
|
144 |
role: 'assistant',
|
145 |
};
|
146 |
|
147 |
const messageWithLoading =
|
148 |
isLoading &&
|
149 |
-
|
150 |
-
|
151 |
? [...messages, assistantLoadingMessage]
|
152 |
: messages;
|
153 |
|
154 |
const append: UseChatHelpers['append'] = async message => {
|
155 |
-
|
|
|
|
|
156 |
return appendRaw(message);
|
157 |
};
|
158 |
|
|
|
3 |
import { useEffect, useState } from 'react';
|
4 |
import { ChatEntity, MessageBase, SignedPayload } from '../types';
|
5 |
import { saveKVChatMessage } from '../kv/chat';
|
6 |
+
import { fetcher, nanoid } from '../utils';
|
7 |
import { getCleanedUpMessages } from './useCleanedUpMessages';
|
8 |
import { CLEANED_SEPARATOR } from '../constants';
|
9 |
|
|
|
58 |
setMessages,
|
59 |
error,
|
60 |
} = useChat({
|
|
|
61 |
api: '/api/vision-agent',
|
62 |
onResponse(response) {
|
63 |
if (response.status !== 200) {
|
|
|
69 |
if (images?.length) {
|
70 |
const publicUrls = await Promise.all(
|
71 |
images.map((image, index) =>
|
72 |
+
uploadBase64(image, message.id, id ?? 'no-id', index),
|
73 |
),
|
74 |
);
|
75 |
const newContent = publicUrls.reduce((accum, url, index) => {
|
|
|
82 |
...message,
|
83 |
content: logs + CLEANED_SEPARATOR + newContent,
|
84 |
};
|
85 |
+
/**
|
86 |
+
* A workaround to fix the issue of the message not being appended to the chat
|
87 |
+
* https://github.com/vercel/ai/issues/550#issuecomment-1712693371
|
88 |
+
*/
|
89 |
+
setMessages([
|
90 |
+
...messages,
|
91 |
+
{ id: nanoid(), role: 'user', content: input, createdAt: new Date() },
|
92 |
+
newMessage,
|
93 |
+
]);
|
94 |
+
if (id) {
|
95 |
+
saveKVChatMessage(id, newMessage);
|
96 |
+
}
|
97 |
} else {
|
98 |
+
if (id) {
|
99 |
+
saveKVChatMessage(id, {
|
100 |
+
...message,
|
101 |
+
content: logs + CLEANED_SEPARATOR + content,
|
102 |
+
});
|
103 |
+
}
|
104 |
}
|
105 |
},
|
106 |
initialMessages: initialMessages,
|
|
|
110 |
},
|
111 |
});
|
112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
useEffect(() => {
|
114 |
if (
|
115 |
!isLoading &&
|
|
|
122 |
|
123 |
const assistantLoadingMessage = {
|
124 |
id: 'loading',
|
125 |
+
content: '...',
|
126 |
role: 'assistant',
|
127 |
};
|
128 |
|
129 |
const messageWithLoading =
|
130 |
isLoading &&
|
131 |
+
messages.length &&
|
132 |
+
messages[messages.length - 1].role !== 'assistant'
|
133 |
? [...messages, assistantLoadingMessage]
|
134 |
: messages;
|
135 |
|
136 |
const append: UseChatHelpers['append'] = async message => {
|
137 |
+
if (id) {
|
138 |
+
await saveKVChatMessage(id, message as MessageBase);
|
139 |
+
}
|
140 |
return appendRaw(message);
|
141 |
};
|
142 |
|
lib/types.ts
CHANGED
@@ -31,7 +31,7 @@ export type MessageBase = {
|
|
31 |
|
32 |
export type ChatEntity = {
|
33 |
url: string;
|
34 |
-
id
|
35 |
user: string; // email
|
36 |
messages: MessageBase[];
|
37 |
updatedAt: number;
|
|
|
31 |
|
32 |
export type ChatEntity = {
|
33 |
url: string;
|
34 |
+
id?: string; // a chat without id is not to be saved
|
35 |
user: string; // email
|
36 |
messages: MessageBase[];
|
37 |
updatedAt: number;
|
state/{index.ts → chat.ts}
RENAMED
@@ -1,8 +1,3 @@
|
|
1 |
import { atom } from 'jotai';
|
2 |
-
import { DatasetImageEntity } from '../lib/types';
|
3 |
-
|
4 |
-
// list of image urls or base64 strings
|
5 |
-
export const datasetAtom = atom<DatasetImageEntity[]>([]);
|
6 |
-
// export const selectedImagesAtom = atom<number[]>([]);
|
7 |
|
8 |
export const chatViewMode = atom<'chat' | 'chat-all'>('chat');
|
|
|
1 |
import { atom } from 'jotai';
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
export const chatViewMode = atom<'chat' | 'chat-all'>('chat');
|
state/media.ts
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import { atom } from 'jotai';
|
2 |
+
|
3 |
+
export const selectedMediaIdAtom = atom<number | null>(null);
|