235 lines
6.6 KiB
TypeScript
235 lines
6.6 KiB
TypeScript
import Loading from "@/app/[locale]/loading"
|
|
import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
|
|
import { ChatbotUIContext } from "@/context/context"
|
|
import { getAssistantToolsByAssistantId } from "@/db/assistant-tools"
|
|
import { getChatFilesByChatId } from "@/db/chat-files"
|
|
import { getChatById } from "@/db/chats"
|
|
import { getMessageFileItemsByMessageId } from "@/db/message-file-items"
|
|
import { getMessagesByChatId } from "@/db/messages"
|
|
import { getMessageImageFromStorage } from "@/db/storage/message-images"
|
|
import { convertBlobToBase64 } from "@/lib/blob-to-b64"
|
|
import useHotkey from "@/lib/hooks/use-hotkey"
|
|
import { LLMID, MessageImage } from "@/types"
|
|
import { useParams } from "next/navigation"
|
|
import { FC, useContext, useEffect, useState } from "react"
|
|
import { ChatHelp } from "./chat-help"
|
|
import { useScroll } from "./chat-hooks/use-scroll"
|
|
import { ChatInput } from "./chat-input"
|
|
import { ChatMessages } from "./chat-messages"
|
|
import { ChatScrollButtons } from "./chat-scroll-buttons"
|
|
import { ChatSecondaryButtons } from "./chat-secondary-buttons"
|
|
|
|
import { useTranslation } from "react-i18next" // 引入 useTranslation 进行国际化处理
|
|
|
|
interface ChatUIProps {}
|
|
|
|
export const ChatUI: FC<ChatUIProps> = ({}) => {
|
|
useHotkey("o", () => handleNewChat())
|
|
|
|
const { t } = useTranslation() // 使用 t 函数进行国际化
|
|
|
|
const params = useParams()
|
|
|
|
const {
|
|
setChatMessages,
|
|
selectedChat,
|
|
setSelectedChat,
|
|
setChatSettings,
|
|
setChatImages,
|
|
assistants,
|
|
setSelectedAssistant,
|
|
setChatFileItems,
|
|
setChatFiles,
|
|
setShowFilesDisplay,
|
|
setUseRetrieval,
|
|
setSelectedTools
|
|
} = useContext(ChatbotUIContext)
|
|
|
|
const { handleNewChat, handleFocusChatInput } = useChatHandler()
|
|
|
|
const {
|
|
messagesStartRef,
|
|
messagesEndRef,
|
|
handleScroll,
|
|
scrollToBottom,
|
|
setIsAtBottom,
|
|
isAtTop,
|
|
isAtBottom,
|
|
isOverflowing,
|
|
scrollToTop
|
|
} = useScroll()
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
await fetchMessages()
|
|
await fetchChat()
|
|
|
|
scrollToBottom()
|
|
setIsAtBottom(true)
|
|
}
|
|
|
|
if (params.chatid) {
|
|
fetchData().then(() => {
|
|
handleFocusChatInput()
|
|
setLoading(false)
|
|
})
|
|
} else {
|
|
setLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
const fetchMessages = async () => {
|
|
const fetchedMessages = await getMessagesByChatId(params.chatid as string)
|
|
|
|
const imagePromises: Promise<MessageImage>[] = fetchedMessages.flatMap(
|
|
message =>
|
|
message.image_paths
|
|
? message.image_paths.map(async imagePath => {
|
|
const url = await getMessageImageFromStorage(imagePath)
|
|
|
|
if (url) {
|
|
const response = await fetch(url)
|
|
const blob = await response.blob()
|
|
const base64 = await convertBlobToBase64(blob)
|
|
|
|
return {
|
|
messageId: message.id,
|
|
path: imagePath,
|
|
base64,
|
|
url,
|
|
file: null
|
|
}
|
|
}
|
|
|
|
return {
|
|
messageId: message.id,
|
|
path: imagePath,
|
|
base64: "",
|
|
url,
|
|
file: null
|
|
}
|
|
})
|
|
: []
|
|
)
|
|
|
|
const images: MessageImage[] = await Promise.all(imagePromises.flat())
|
|
setChatImages(images)
|
|
|
|
const messageFileItemPromises = fetchedMessages.map(
|
|
async message => await getMessageFileItemsByMessageId(message.id)
|
|
)
|
|
|
|
const messageFileItems = await Promise.all(messageFileItemPromises)
|
|
|
|
const uniqueFileItems = messageFileItems.flatMap(item => item.file_items)
|
|
setChatFileItems(uniqueFileItems)
|
|
|
|
const chatFiles = await getChatFilesByChatId(params.chatid as string)
|
|
|
|
setChatFiles(
|
|
chatFiles.files.map(file => ({
|
|
id: file.id,
|
|
name: file.name,
|
|
type: file.type,
|
|
file: null
|
|
}))
|
|
)
|
|
|
|
setUseRetrieval(true)
|
|
setShowFilesDisplay(true)
|
|
|
|
const fetchedChatMessages = fetchedMessages.map(message => {
|
|
return {
|
|
message,
|
|
fileItems: messageFileItems
|
|
.filter(messageFileItem => messageFileItem.id === message.id)
|
|
.flatMap(messageFileItem =>
|
|
messageFileItem.file_items.map(fileItem => fileItem.id)
|
|
)
|
|
}
|
|
})
|
|
|
|
setChatMessages(fetchedChatMessages)
|
|
}
|
|
|
|
const fetchChat = async () => {
|
|
const chat = await getChatById(params.chatid as string)
|
|
if (!chat) return
|
|
|
|
if (chat.assistant_id) {
|
|
const assistant = assistants.find(
|
|
assistant => assistant.id === chat.assistant_id
|
|
)
|
|
|
|
if (assistant) {
|
|
setSelectedAssistant(assistant)
|
|
|
|
const assistantTools = (
|
|
await getAssistantToolsByAssistantId(assistant.id)
|
|
).tools
|
|
setSelectedTools(assistantTools)
|
|
}
|
|
}
|
|
|
|
setSelectedChat(chat)
|
|
setChatSettings({
|
|
model: chat.model as LLMID,
|
|
prompt: chat.prompt,
|
|
temperature: chat.temperature,
|
|
contextLength: chat.context_length,
|
|
includeProfileContext: chat.include_profile_context,
|
|
includeWorkspaceInstructions: chat.include_workspace_instructions,
|
|
embeddingsProvider: chat.embeddings_provider as "openai" | "local"
|
|
})
|
|
}
|
|
|
|
if (loading) {
|
|
return <Loading />
|
|
}
|
|
|
|
return (
|
|
<div className="relative flex h-full flex-col items-center">
|
|
<div className="absolute left-4 top-2.5 flex justify-center">
|
|
<ChatScrollButtons
|
|
isAtTop={isAtTop}
|
|
isAtBottom={isAtBottom}
|
|
isOverflowing={isOverflowing}
|
|
scrollToTop={scrollToTop}
|
|
scrollToBottom={scrollToBottom}
|
|
/>
|
|
</div>
|
|
|
|
<div className="absolute right-4 top-1 flex h-[40px] items-center space-x-2">
|
|
<ChatSecondaryButtons />
|
|
</div>
|
|
|
|
<div className="bg-secondary flex max-h-[50px] min-h-[50px] w-full items-center justify-center border-b-2 font-bold">
|
|
<div className="max-w-[200px] truncate sm:max-w-[400px] md:max-w-[500px] lg:max-w-[600px] xl:max-w-[700px]">
|
|
{selectedChat?.name || t("chat.defaultChatTitle")}
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className="flex size-full flex-col overflow-auto border-b"
|
|
onScroll={handleScroll}
|
|
>
|
|
<div ref={messagesStartRef} />
|
|
|
|
<ChatMessages />
|
|
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
|
|
<div className="relative w-full min-w-[300px] items-end px-2 pb-3 pt-0 sm:w-[600px] sm:pb-8 sm:pt-5 md:w-[700px] lg:w-[700px] xl:w-[800px]">
|
|
<ChatInput />
|
|
</div>
|
|
|
|
<div className="absolute bottom-2 right-2 hidden md:block lg:bottom-4 lg:right-4">
|
|
<ChatHelp />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|