Spaces:
Sleeping
Sleeping
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 |
};
|