MingruiZhang commited on
Commit
db06845
β€’
1 Parent(s): 4a72a82
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
- <ChatMessage
60
- key={message.id}
61
- message={message}
62
- wipAssistantMessage={
63
- lastMessage.role === 'assistant' &&
64
- index === dbMessages.length - 1
65
- ? lastMessage
66
- : undefined
67
- }
68
- />
69
- ))}
 
 
 
70
  <div
71
  className="w-full"
72
  style={{ height: SCROLL_BOTTOM }}
73
  ref={visibilityRef}
74
  />
75
  </div>
76
- <div className="absolute bottom-4 w-full">
77
- <Composer
78
- // Use the last message mediaUrl as the initial mediaUrl
79
- initMediaUrl={dbMessages[dbMessages.length - 1]?.mediaUrl}
80
- isLoading={isLoading}
81
- onSubmit={async ({ input, mediaUrl: newMediaUrl }) => {
82
- const messageInput = {
83
- prompt: input,
84
- mediaUrl: newMediaUrl,
85
- };
86
- const resp = await dbPostCreateMessage(id, messageInput);
87
- append(resp);
88
- }}
89
- />
90
- </div>
 
 
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
- isLoading?: boolean;
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
- ({ isLoading, onSubmit, initMediaUrl, initInput }, ref) => {
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 = isLoading || isUploading || isSubmitting;
 
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={finalLoading}
159
  onChange={e => setLocalInput(e.target.value)}
160
  placeholder={
161
- finalLoading ? 'πŸ€– Agent working ✨' : 'Message Vision Agent'
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={finalLoading || input === '' || noMediaValidation}
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
- session,
123
- {
124
- params: json,
125
- },
126
- req,
127
- '_API_REQUEST',
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
  };