Spaces:
Sleeping
Sleeping
MingruiZhang
commited on
Commit
β’
db06845
1
Parent(s):
4a72a82
save
Browse files- app/api/vision-agent/route.ts +4 -3
- app/chat/[id]/server.tsx +3 -2
- components/ChatInterface.tsx +3 -2
- components/chat/ChatList.tsx +38 -29
- components/chat/ChatMessage.tsx +11 -2
- components/chat/Composer.tsx +7 -6
- lib/logger.ts +8 -8
app/api/vision-agent/route.ts
CHANGED
@@ -90,12 +90,14 @@ export const POST = withLogging(
|
|
90 |
formData.append('image', mediaUrl);
|
91 |
|
92 |
const fetchResponse = await fetch(
|
93 |
-
`https://api.dev.landing.ai/v1/agent/chat?agent_class=vision_agent&self_reflection=false`,
|
|
|
94 |
// `http://localhost:5001/v1/agent/chat?agent_class=vision_agent&self_reflection=${enableSelfReflection}`,
|
95 |
{
|
96 |
method: 'POST',
|
97 |
headers: {
|
98 |
-
apikey: 'land_sk_DKeoYtaZZrYqJ9TMMiXe4BIQgJcZ0s3XAoB0JT3jv73FFqnr6k',
|
|
|
99 |
},
|
100 |
body: formData,
|
101 |
},
|
@@ -223,7 +225,6 @@ export const POST = withLogging(
|
|
223 |
}
|
224 |
}
|
225 |
if (done) {
|
226 |
-
console.log(done);
|
227 |
logger.info(
|
228 |
session,
|
229 |
{
|
|
|
90 |
formData.append('image', mediaUrl);
|
91 |
|
92 |
const fetchResponse = await fetch(
|
93 |
+
// `https://api.dev.landing.ai/v1/agent/chat?agent_class=vision_agent&self_reflection=false`,
|
94 |
+
`https://api.landing.ai/v1/agent/chat?agent_class=vision_agent&self_reflection=false`,
|
95 |
// `http://localhost:5001/v1/agent/chat?agent_class=vision_agent&self_reflection=${enableSelfReflection}`,
|
96 |
{
|
97 |
method: 'POST',
|
98 |
headers: {
|
99 |
+
// apikey: 'land_sk_DKeoYtaZZrYqJ9TMMiXe4BIQgJcZ0s3XAoB0JT3jv73FFqnr6k', // dev
|
100 |
+
apikey: 'land_sk_nMnUf8xiJJUjyw1l5QaIJJ4ZyrvPthzVmPAIG7TtJY7F9CW6lu', // prod
|
101 |
},
|
102 |
body: formData,
|
103 |
},
|
|
|
225 |
}
|
226 |
}
|
227 |
if (done) {
|
|
|
228 |
logger.info(
|
229 |
session,
|
230 |
{
|
app/chat/[id]/server.tsx
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
import ChatInterface from '../../../components/ChatInterface';
|
2 |
-
import { auth } from '@/auth';
|
3 |
import { dbGetChat } from '@/lib/db/functions';
|
4 |
import { redirect } from 'next/navigation';
|
5 |
import { revalidatePath } from 'next/cache';
|
@@ -10,10 +10,11 @@ interface ChatServerProps {
|
|
10 |
|
11 |
export default async function ChatServer({ id }: ChatServerProps) {
|
12 |
const chat = await dbGetChat(id);
|
|
|
13 |
|
14 |
if (!chat) {
|
15 |
revalidatePath('/');
|
16 |
redirect('/');
|
17 |
}
|
18 |
-
return <ChatInterface chat={chat} />;
|
19 |
}
|
|
|
1 |
import ChatInterface from '../../../components/ChatInterface';
|
2 |
+
import { auth, sessionUser } from '@/auth';
|
3 |
import { dbGetChat } from '@/lib/db/functions';
|
4 |
import { redirect } from 'next/navigation';
|
5 |
import { revalidatePath } from 'next/cache';
|
|
|
10 |
|
11 |
export default async function ChatServer({ id }: ChatServerProps) {
|
12 |
const chat = await dbGetChat(id);
|
13 |
+
const { id: userId } = await sessionUser();
|
14 |
|
15 |
if (!chat) {
|
16 |
revalidatePath('/');
|
17 |
redirect('/');
|
18 |
}
|
19 |
+
return <ChatInterface chat={chat} userId={userId} />;
|
20 |
}
|
components/ChatInterface.tsx
CHANGED
@@ -10,9 +10,10 @@ 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,
|
@@ -30,7 +31,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ chat }) => {
|
|
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 |
);
|
|
|
10 |
|
11 |
export interface ChatInterfaceProps {
|
12 |
chat: ChatWithMessages;
|
13 |
+
userId?: string | null;
|
14 |
}
|
15 |
|
16 |
+
const ChatInterface: React.FC<ChatInterfaceProps> = ({ chat, userId }) => {
|
17 |
const messageId = useAtomValue(selectedMessageId);
|
18 |
const messageCodeResult = chat.messages.find(
|
19 |
message => message.id === messageId,
|
|
|
31 |
)}
|
32 |
</div>
|
33 |
<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%]">
|
34 |
+
<ChatList chat={chat} userId={userId} />
|
35 |
</div>
|
36 |
</div>
|
37 |
);
|
components/chat/ChatList.tsx
CHANGED
@@ -18,14 +18,18 @@ 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);
|
@@ -55,39 +59,44 @@ const ChatList: React.FC<ChatListProps> = ({ chat }) => {
|
|
55 |
ref={scrollRef}
|
56 |
>
|
57 |
<div className="overflow-auto h-full p-4 z-10" ref={messagesRef}>
|
58 |
-
{dbMessages.map((message, index) =>
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
|
|
|
|
|
|
70 |
<div
|
71 |
className="w-full"
|
72 |
style={{ height: SCROLL_BOTTOM }}
|
73 |
ref={visibilityRef}
|
74 |
/>
|
75 |
</div>
|
76 |
-
|
77 |
-
<
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
|
|
|
|
91 |
{/* Scroll to bottom Icon */}
|
92 |
<Button
|
93 |
size="icon"
|
|
|
18 |
|
19 |
export interface ChatListProps {
|
20 |
chat: ChatWithMessages;
|
21 |
+
userId?: string | null;
|
22 |
}
|
23 |
|
24 |
export const SCROLL_BOTTOM = 120;
|
25 |
|
26 |
+
const ChatList: React.FC<ChatListProps> = ({ chat, userId }) => {
|
27 |
+
const { id, messages: dbMessages, userId: chatUserId } = chat;
|
28 |
const { messages, append, isLoading } = useVisionAgent(chat);
|
29 |
|
30 |
+
// Only login and chat owner can compose
|
31 |
+
const canCompose = !chatUserId || userId === chatUserId;
|
32 |
+
|
33 |
const lastMessage = messages[messages.length - 1];
|
34 |
const lastDbMessage = dbMessages[dbMessages.length - 1];
|
35 |
const setMessageId = useSetAtom(selectedMessageId);
|
|
|
59 |
ref={scrollRef}
|
60 |
>
|
61 |
<div className="overflow-auto h-full p-4 z-10" ref={messagesRef}>
|
62 |
+
{dbMessages.map((message, index) => {
|
63 |
+
const isLastMessage = index === dbMessages.length - 1;
|
64 |
+
return (
|
65 |
+
<ChatMessage
|
66 |
+
key={message.id}
|
67 |
+
message={message}
|
68 |
+
loading={isLastMessage && isLoading}
|
69 |
+
wipAssistantMessage={
|
70 |
+
lastMessage.role === 'assistant' && isLastMessage
|
71 |
+
? lastMessage
|
72 |
+
: undefined
|
73 |
+
}
|
74 |
+
/>
|
75 |
+
);
|
76 |
+
})}
|
77 |
<div
|
78 |
className="w-full"
|
79 |
style={{ height: SCROLL_BOTTOM }}
|
80 |
ref={visibilityRef}
|
81 |
/>
|
82 |
</div>
|
83 |
+
{canCompose && (
|
84 |
+
<div className="absolute bottom-4 w-full">
|
85 |
+
<Composer
|
86 |
+
// Use the last message mediaUrl as the initial mediaUrl
|
87 |
+
initMediaUrl={dbMessages[dbMessages.length - 1]?.mediaUrl}
|
88 |
+
disabled={isLoading}
|
89 |
+
onSubmit={async ({ input, mediaUrl: newMediaUrl }) => {
|
90 |
+
const messageInput = {
|
91 |
+
prompt: input,
|
92 |
+
mediaUrl: newMediaUrl,
|
93 |
+
};
|
94 |
+
const resp = await dbPostCreateMessage(id, messageInput);
|
95 |
+
append(resp);
|
96 |
+
}}
|
97 |
+
/>
|
98 |
+
</div>
|
99 |
+
)}
|
100 |
{/* Scroll to bottom Icon */}
|
101 |
<Button
|
102 |
size="icon"
|
components/chat/ChatMessage.tsx
CHANGED
@@ -33,12 +33,14 @@ import { usePrevious } from '@/lib/hooks/usePrevious';
|
|
33 |
|
34 |
export interface ChatMessageProps {
|
35 |
message: Message;
|
|
|
36 |
wipAssistantMessage?: MessageUI;
|
37 |
}
|
38 |
|
39 |
export const ChatMessage: React.FC<ChatMessageProps> = ({
|
40 |
message,
|
41 |
wipAssistantMessage,
|
|
|
42 |
}) => {
|
43 |
const [messageId, setMessageId] = useAtom(selectedMessageId);
|
44 |
const { id, mediaUrl, prompt, response, result } = message;
|
@@ -49,7 +51,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|
49 |
return (
|
50 |
<div
|
51 |
className={cn(
|
52 |
-
'rounded-md bg-muted border border-muted p-4 mb-4',
|
53 |
messageId === id && 'lg:border-primary/50',
|
54 |
result && 'lg:cursor-pointer',
|
55 |
)}
|
@@ -121,6 +123,14 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|
121 |
</div>
|
122 |
</>
|
123 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
</div>
|
125 |
);
|
126 |
};
|
@@ -138,7 +148,6 @@ const ChunkTypeToText: React.FC<{
|
|
138 |
|
139 |
const [seconds, setSeconds] = useState(0);
|
140 |
const isExecution = type === 'code' && status === 'running';
|
141 |
-
const timerId = useRef<NodeJS.Timeout>();
|
142 |
|
143 |
useEffect(() => {
|
144 |
if (isExecution) {
|
|
|
33 |
|
34 |
export interface ChatMessageProps {
|
35 |
message: Message;
|
36 |
+
loading?: boolean;
|
37 |
wipAssistantMessage?: MessageUI;
|
38 |
}
|
39 |
|
40 |
export const ChatMessage: React.FC<ChatMessageProps> = ({
|
41 |
message,
|
42 |
wipAssistantMessage,
|
43 |
+
loading,
|
44 |
}) => {
|
45 |
const [messageId, setMessageId] = useAtom(selectedMessageId);
|
46 |
const { id, mediaUrl, prompt, response, result } = message;
|
|
|
51 |
return (
|
52 |
<div
|
53 |
className={cn(
|
54 |
+
'rounded-md bg-muted border border-muted p-4 pb-5 mb-4 relative',
|
55 |
messageId === id && 'lg:border-primary/50',
|
56 |
result && 'lg:cursor-pointer',
|
57 |
)}
|
|
|
123 |
</div>
|
124 |
</>
|
125 |
)}
|
126 |
+
<div
|
127 |
+
className={cn(
|
128 |
+
'w-1/3 h-1 rounded-full overflow-hidden bg-zinc-700 absolute left-1/2 -translate-x-1/2 bottom-2',
|
129 |
+
loading ? 'opacity-100' : 'opacity-0',
|
130 |
+
)}
|
131 |
+
>
|
132 |
+
<div className="h-full bg-primary animate-progress origin-left-right" />
|
133 |
+
</div>
|
134 |
</div>
|
135 |
);
|
136 |
};
|
|
|
148 |
|
149 |
const [seconds, setSeconds] = useState(0);
|
150 |
const isExecution = type === 'code' && status === 'running';
|
|
|
151 |
|
152 |
useEffect(() => {
|
153 |
if (isExecution) {
|
components/chat/Composer.tsx
CHANGED
@@ -24,7 +24,7 @@ import useMediaUpload from '@/lib/hooks/useMediaUpload';
|
|
24 |
|
25 |
export interface ComposerProps {
|
26 |
onSubmit: (params: { input: string; mediaUrl: string }) => Promise<void>;
|
27 |
-
|
28 |
title?: string;
|
29 |
initMediaUrl?: string;
|
30 |
initInput?: string;
|
@@ -36,7 +36,7 @@ export interface ComposerRef {
|
|
36 |
}
|
37 |
|
38 |
const Composer = forwardRef<ComposerRef, ComposerProps>(
|
39 |
-
({
|
40 |
const { formRef, onKeyDown } = useEnterSubmit();
|
41 |
const inputRef = useRef<HTMLTextAreaElement>(null);
|
42 |
const [localMediaUrl, setLocalMediaUrl] = useState<string | undefined>(
|
@@ -53,7 +53,8 @@ const Composer = forwardRef<ComposerRef, ComposerProps>(
|
|
53 |
openUpload,
|
54 |
} = useMediaUpload(uploadUrl => setLocalMediaUrl(uploadUrl));
|
55 |
|
56 |
-
const finalLoading =
|
|
|
57 |
|
58 |
useEffect(() => {
|
59 |
if (inputRef.current) {
|
@@ -155,10 +156,10 @@ const Composer = forwardRef<ComposerRef, ComposerProps>(
|
|
155 |
onKeyDown={onKeyDown}
|
156 |
rows={1}
|
157 |
value={input}
|
158 |
-
disabled={
|
159 |
onChange={e => setLocalInput(e.target.value)}
|
160 |
placeholder={
|
161 |
-
|
162 |
}
|
163 |
spellCheck={false}
|
164 |
className="w-full grow resize-none bg-transparent focus-within:outline-none"
|
@@ -170,7 +171,7 @@ const Composer = forwardRef<ComposerRef, ComposerProps>(
|
|
170 |
type="submit"
|
171 |
size="icon"
|
172 |
className={cn('size-6 absolute bottom-3 right-3')}
|
173 |
-
disabled={
|
174 |
>
|
175 |
<IconArrowUp className="size-3" />
|
176 |
</Button>
|
|
|
24 |
|
25 |
export interface ComposerProps {
|
26 |
onSubmit: (params: { input: string; mediaUrl: string }) => Promise<void>;
|
27 |
+
disabled?: boolean;
|
28 |
title?: string;
|
29 |
initMediaUrl?: string;
|
30 |
initInput?: string;
|
|
|
36 |
}
|
37 |
|
38 |
const Composer = forwardRef<ComposerRef, ComposerProps>(
|
39 |
+
({ disabled, onSubmit, initMediaUrl, initInput }, ref) => {
|
40 |
const { formRef, onKeyDown } = useEnterSubmit();
|
41 |
const inputRef = useRef<HTMLTextAreaElement>(null);
|
42 |
const [localMediaUrl, setLocalMediaUrl] = useState<string | undefined>(
|
|
|
53 |
openUpload,
|
54 |
} = useMediaUpload(uploadUrl => setLocalMediaUrl(uploadUrl));
|
55 |
|
56 |
+
const finalLoading = isUploading || isSubmitting;
|
57 |
+
const finalDisabled = finalLoading || disabled;
|
58 |
|
59 |
useEffect(() => {
|
60 |
if (inputRef.current) {
|
|
|
156 |
onKeyDown={onKeyDown}
|
157 |
rows={1}
|
158 |
value={input}
|
159 |
+
disabled={finalDisabled}
|
160 |
onChange={e => setLocalInput(e.target.value)}
|
161 |
placeholder={
|
162 |
+
finalDisabled ? 'π€ Agent working β¨' : 'Message Vision Agent'
|
163 |
}
|
164 |
spellCheck={false}
|
165 |
className="w-full grow resize-none bg-transparent focus-within:outline-none"
|
|
|
171 |
type="submit"
|
172 |
size="icon"
|
173 |
className={cn('size-6 absolute bottom-3 right-3')}
|
174 |
+
disabled={finalDisabled || input === '' || noMediaValidation}
|
175 |
>
|
176 |
<IconArrowUp className="size-3" />
|
177 |
</Button>
|
lib/logger.ts
CHANGED
@@ -118,14 +118,14 @@ export const withLogging = (
|
|
118 |
return async (req: Request) => {
|
119 |
const session = await auth();
|
120 |
const json = await req.json();
|
121 |
-
logger.info(
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
);
|
129 |
return handler(session, json, req);
|
130 |
};
|
131 |
};
|
|
|
118 |
return async (req: Request) => {
|
119 |
const session = await auth();
|
120 |
const json = await req.json();
|
121 |
+
// logger.info(
|
122 |
+
// session,
|
123 |
+
// {
|
124 |
+
// params: json,
|
125 |
+
// },
|
126 |
+
// req,
|
127 |
+
// '_API_REQUEST',
|
128 |
+
// );
|
129 |
return handler(session, json, req);
|
130 |
};
|
131 |
};
|