| '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` : 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 |
| } |
|
|