|
'use client' |
|
|
|
import { useState, useCallback, useEffect, useMemo } from 'react' |
|
import { useAtom, useAtomValue } from 'jotai' |
|
import { chatFamily, bingConversationStyleAtom, GreetMessages, hashAtom, voiceAtom } from '@/state' |
|
import { setConversationMessages } from './chat-history' |
|
import { ChatMessageModel, BotId, FileItem } from '@/lib/bots/bing/types' |
|
import { nanoid } from '../utils' |
|
import { TTS } from '../bots/bing/tts' |
|
|
|
export function useBing(botId: BotId = 'bing') { |
|
const chatAtom = useMemo(() => chatFamily({ botId, page: 'singleton' }), [botId]) |
|
const [enableTTS] = useAtom(voiceAtom) |
|
const speaker = useMemo(() => new TTS(), []) |
|
const [hash, setHash] = useAtom(hashAtom) |
|
const bingConversationStyle = useAtomValue(bingConversationStyleAtom) |
|
const [chatState, setChatState] = useAtom(chatAtom) |
|
const [input, setInput] = useState('') |
|
const [attachmentList, setAttachmentList] = useState<FileItem[]>([]) |
|
|
|
const updateMessage = useCallback( |
|
(messageId: string, updater: (message: ChatMessageModel) => void) => { |
|
setChatState((draft) => { |
|
const message = draft.messages.find((m) => m.id === messageId) |
|
if (message) { |
|
updater(message) |
|
} |
|
}) |
|
}, |
|
[setChatState], |
|
) |
|
|
|
const sendMessage = useCallback( |
|
async (input: string, options = {}) => { |
|
const botMessageId = nanoid() |
|
const imageUrl = attachmentList?.[0]?.status === 'loaded' ? attachmentList[0].url : undefined |
|
setChatState((draft) => { |
|
const text = imageUrl ? `${input}\n\n![image](${imageUrl})` : input |
|
draft.messages.push({ id: nanoid(), text, author: 'user' }, { id: botMessageId, text: '', author: 'bot' }) |
|
setAttachmentList([]) |
|
}) |
|
const abortController = new AbortController() |
|
setChatState((draft) => { |
|
draft.generatingMessageId = botMessageId |
|
draft.abortController = abortController |
|
}) |
|
speaker.reset() |
|
await chatState.bot.sendMessage({ |
|
prompt: input, |
|
imageUrl: /\?bcid=([^&]+)/.test(imageUrl ?? '') ? `https://www.bing.com/images/blob?bcid=${RegExp.$1}` : imageUrl, |
|
options: { |
|
...options, |
|
bingConversationStyle, |
|
}, |
|
signal: abortController.signal, |
|
onEvent(event) { |
|
if (event.type === 'UPDATE_ANSWER') { |
|
updateMessage(botMessageId, (message) => { |
|
if (event.data.text.length > message.text.length) { |
|
message.text = event.data.text |
|
} |
|
|
|
if (event.data.spokenText && enableTTS) { |
|
speaker.speak(event.data.spokenText) |
|
} |
|
|
|
message.throttling = event.data.throttling || message.throttling |
|
message.sourceAttributions = event.data.sourceAttributions || message.sourceAttributions |
|
message.suggestedResponses = event.data.suggestedResponses || message.suggestedResponses |
|
}) |
|
} else if (event.type === 'ERROR') { |
|
updateMessage(botMessageId, (message) => { |
|
message.error = event.error |
|
}) |
|
setChatState((draft) => { |
|
draft.abortController = undefined |
|
draft.generatingMessageId = '' |
|
}) |
|
} else if (event.type === 'DONE') { |
|
setChatState((draft) => { |
|
draft.abortController = undefined |
|
draft.generatingMessageId = '' |
|
}) |
|
} |
|
}, |
|
}) |
|
}, |
|
[botId, attachmentList, chatState.bot, setChatState, updateMessage], |
|
) |
|
|
|
const uploadImage = useCallback(async (imgUrl: string) => { |
|
setAttachmentList([{ url: imgUrl, status: 'loading' }]) |
|
const response = await chatState.bot.uploadImage(imgUrl, bingConversationStyle) |
|
if (response?.blobId) { |
|
setAttachmentList([{ url: `/api/blob?bcid=${response.blobId}`, status: 'loaded' }]) |
|
} else { |
|
setAttachmentList([{ url: imgUrl, status: 'error' }]) |
|
} |
|
}, [chatState.bot]) |
|
|
|
const resetConversation = useCallback(() => { |
|
chatState.bot.resetConversation() |
|
speaker.abort() |
|
setChatState((draft) => { |
|
draft.abortController = undefined |
|
draft.generatingMessageId = '' |
|
draft.messages = [{ author: 'bot', text: GreetMessages[Math.floor(GreetMessages.length * Math.random())], id: nanoid() }] |
|
draft.conversationId = nanoid() |
|
}) |
|
}, [chatState.bot, setChatState]) |
|
|
|
const stopGenerating = useCallback(() => { |
|
chatState.abortController?.abort() |
|
if (chatState.generatingMessageId) { |
|
updateMessage(chatState.generatingMessageId, (message) => { |
|
if (!message.text && !message.error) { |
|
message.text = 'Cancelled' |
|
} |
|
}) |
|
} |
|
setChatState((draft) => { |
|
draft.generatingMessageId = '' |
|
}) |
|
}, [chatState.abortController, chatState.generatingMessageId, setChatState, updateMessage]) |
|
|
|
useEffect(() => { |
|
if (chatState.messages.length) { |
|
setConversationMessages(botId, chatState.conversationId, chatState.messages) |
|
} |
|
}, [botId, chatState.conversationId, chatState.messages]) |
|
|
|
useEffect(() => { |
|
if (hash === 'reset') { |
|
resetConversation() |
|
setHash('') |
|
} |
|
}, [hash, setHash]) |
|
|
|
const chat = useMemo( |
|
() => ({ |
|
botId, |
|
bot: chatState.bot, |
|
isSpeaking: speaker.isSpeaking, |
|
messages: chatState.messages, |
|
sendMessage, |
|
setInput, |
|
input, |
|
resetConversation, |
|
generating: !!chatState.generatingMessageId, |
|
stopGenerating, |
|
uploadImage, |
|
setAttachmentList, |
|
attachmentList, |
|
}), |
|
[ |
|
botId, |
|
bingConversationStyle, |
|
chatState.bot, |
|
chatState.generatingMessageId, |
|
chatState.messages, |
|
speaker.isSpeaking, |
|
setInput, |
|
input, |
|
setAttachmentList, |
|
attachmentList, |
|
resetConversation, |
|
sendMessage, |
|
stopGenerating, |
|
], |
|
) |
|
|
|
return chat |
|
} |
|
|