vision-agent / lib /hooks /useVisionAgent.tsx
wuyiqunLu
feat: support image rendering on answer (#19)
5d7d435 unverified
raw
history blame
4.02 kB
import { useChat, type Message, UseChatHelpers } from 'ai/react';
import { toast } from 'react-hot-toast';
import { useEffect, useState } from 'react';
import { ChatEntity, MessageBase, SignedPayload } from '../types';
import { saveKVChatMessage } from '../kv/chat';
import { fetcher } from '../utils';
import {
getCleanedUpMessages,
CLEANED_SEPARATOR,
} from './useCleanedUpMessages';
const uploadBase64 = async (
base64: string,
messageId: string,
chatId: string,
index: number,
) => {
const res = await fetch('data:image/png;base64,' + base64);
const blob = await res.blob();
const { signedUrl, publicUrl, fields } = await fetcher<SignedPayload>(
'/api/sign',
{
method: 'POST',
body: JSON.stringify({
id: `${chatId}/${messageId}`,
fileType: blob.type,
fileName: `answer-${index}.${blob.type.split('/')[1]}`,
}),
},
);
const formData = new FormData();
Object.entries(fields).forEach(([key, value]) => {
formData.append(key, value as string);
});
formData.append('file', blob);
const uploadResponse = await fetch(signedUrl, {
method: 'POST',
body: formData,
});
if (uploadResponse.ok) {
return publicUrl;
} else {
throw new Error('Upload failed');
}
};
const useVisionAgent = (chat: ChatEntity) => {
const { messages: initialMessages, id, url } = chat;
const {
messages,
append: appendRaw,
reload,
stop,
isLoading,
input,
setInput,
setMessages,
} = useChat({
sendExtraMessageFields: true,
api: '/api/vision-agent',
onResponse(response) {
if (response.status !== 200) {
toast.error(response.statusText);
}
},
onFinish: async message => {
const { logs = '', content, images } = getCleanedUpMessages(message);
if (images?.length) {
const publicUrls = await Promise.all(
images.map((image, index) =>
uploadBase64(image, message.id, id, index),
),
);
const newMessage = {
...message,
content:
logs +
CLEANED_SEPARATOR +
content +
'\n' +
publicUrls
.map((url, index) => `![image-${index}](${url})`)
.join('\n'),
};
saveKVChatMessage(id, newMessage);
} else {
saveKVChatMessage(id, {
...message,
content: logs + CLEANED_SEPARATOR + content,
});
}
},
initialMessages: initialMessages,
body: {
url,
id,
},
});
const [loadingDots, setLoadingDots] = useState('');
useEffect(() => {
let loadingInterval: NodeJS.Timeout;
if (isLoading) {
loadingInterval = setInterval(() => {
setLoadingDots(prevMessage => {
switch (prevMessage) {
case '':
return '.';
case '.':
return '..';
case '..':
return '...';
case '...':
return '';
default:
return '';
}
});
}, 500);
}
return () => {
clearInterval(loadingInterval);
};
}, [isLoading]);
useEffect(() => {
if (
!isLoading &&
messages.length &&
messages[messages.length - 1].role === 'user'
) {
reload();
}
}, [isLoading, messages, reload]);
const assistantLoadingMessage = {
id: 'loading',
content: loadingDots,
role: 'assistant',
};
const messageWithLoading =
isLoading &&
messages.length &&
messages[messages.length - 1].role !== 'assistant'
? [...messages, assistantLoadingMessage]
: messages;
const append: UseChatHelpers['append'] = async message => {
await saveKVChatMessage(id, message as MessageBase);
return appendRaw(message);
};
return {
messages: messageWithLoading as MessageBase[],
append,
reload,
stop,
isLoading,
input,
setInput,
};
};
export default useVisionAgent;