import { useDebouncedCallback } from "use-debounce"; import React, { useState, useRef, useEffect, useMemo, useCallback, Fragment, RefObject, } from "react"; import { HomeOutlined, MenuOutlined } from '@ant-design/icons'; import SendWhiteIcon from "../icons/send-white.svg"; import BrainIcon from "../icons/brain.svg"; import RenameIcon from "../icons/rename.svg"; import ExportIcon from "../icons/share.svg"; import ReturnIcon from "../icons/return.svg"; import CopyIcon from "../icons/copy.svg"; import faviconSrc from "../icons/favicon.png"; import LeftIcon from "../icons/left.svg"; import Favicon from "../icons/favicon.svg"; import LoadingIcon from "../icons/three-dots.svg"; import LoadingButtonIcon from "../icons/loading.svg"; import PromptIcon from "../icons/prompt.svg"; import MaskIcon from "../icons/mask.svg"; import MaxIcon from "../icons/max.svg"; import MinIcon from "../icons/min.svg"; import ResetIcon from "../icons/reload.svg"; import BreakIcon from "../icons/break.svg"; import SettingsIcon from "../icons/chat-settings.svg"; import DeleteIcon from "../icons/clear.svg"; import PinIcon from "../icons/pin.svg"; import EditIcon from "../icons/rename.svg"; import ConfirmIcon from "../icons/confirm.svg"; import CancelIcon from "../icons/cancel.svg"; import ImageIcon from "../icons/image.svg"; import LightIcon from "../icons/light.svg"; import DarkIcon from "../icons/dark.svg"; import AutoIcon from "../icons/auto.svg"; import BottomIcon from "../icons/bottom.svg"; import StopIcon from "../icons/pause.svg"; import RobotIcon from "../icons/robot.svg"; import AddIcon from "../icons/add.svg"; import SizeIcon from "../icons/size.svg"; import PluginIcon from "../icons/plugin.svg"; import avatarSrc from "../icons/avatar.png"; import { ChatMessage, SubmitKey, useChatStore, BOT_HELLO, createMessage, useAccessStore, Theme, useAppConfig, DEFAULT_TOPIC, ModelType, } from "../store"; import { copyToClipboard, selectOrCopy, autoGrowTextArea, useMobileScreen, getMessageTextContent, getMessageImages, isVisionModel, isDalle3, } from "../utils"; import { uploadImage as uploadImageRemote } from "@/app/utils/chat"; import dynamic from "next/dynamic"; import { ChatControllerPool } from "../client/controller"; import { DalleSize } from "../typing"; import { Prompt, usePromptStore } from "../store/prompt"; import { useGlobalStore } from "../store"; import Locale from "../locales"; import { IconButton } from "./button"; import styles from "./chat.module.scss"; import { List, ListItem, Modal, Selector, showConfirm, showPrompt, showToast, } from "./ui-lib"; import { useNavigate, useLocation } from "react-router-dom"; import { CHAT_PAGE_SIZE, LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS, UNFINISHED_INPUT, ServiceProvider, Plugin, } from "../constant"; import { Avatar } from "./emoji"; import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask"; import { useMaskStore } from "../store/mask"; import { ChatCommandPrefix, useChatCommand, useCommand } from "../command"; import { prettyObject } from "../utils/format"; import { ExportMessageModal } from "./exporter"; import { getClientConfig } from "../config/client"; import { useAllModels } from "../utils/hooks"; import { Button, Collapse, Drawer, message, Popover, Select, Skeleton, Space } from 'antd'; import { FileOutlined, FilePdfOutlined, FileTextOutlined, FileWordOutlined } from '@ant-design/icons'; import { RightOutlined, CheckCircleOutlined, CaretRightOutlined, StarTwoTone } from '@ant-design/icons'; import api from "@/app/api/api"; const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { loading: () => , }); export function SessionConfigModel(props: { onClose: () => void }) { const chatStore = useChatStore(); const session = chatStore.currentSession(); const maskStore = useMaskStore(); const navigate = useNavigate(); return (
props.onClose()} actions={[ } bordered text={Locale.Chat.Config.Reset} onClick={async () => { if (await showConfirm(Locale.Memory.ResetConfirm)) { chatStore.updateCurrentSession( (session) => (session.memoryPrompt = ""), ); } }} />, } bordered text={Locale.Chat.Config.SaveAs} onClick={() => { navigate(Path.Masks); setTimeout(() => { maskStore.create(session.mask); }, 500); }} />, ]} > { const mask = { ...session.mask }; updater(mask); chatStore.updateCurrentSession((session) => (session.mask = mask)); }} shouldSyncFromGlobal extraListItems={ session.mask.modelConfig.sendMemory ? ( ) : ( <> ) } >
); } // 提示词 const CallWord = (props: { setUserInput: (value: string) => void, doSubmit: (userInput: string) => void, }) => { const { setUserInput, doSubmit } = props const list = [ { title: '信息公布', // text: '在哪里查看招聘信息?', text: '今年上海建科工程咨询的校园招聘什么时候开始?如何查阅相关招聘信息?', }, { title: '招聘岗位', // text: '今年招聘的岗位有哪些?', text: '今年招聘的岗位有哪些?', }, { title: '专业要求', // text: '招聘的岗位有什么专业要求?', text: '招聘的岗位有什么专业要求?', }, { title: '工作地点', // text: '全国都有工作地点吗?', text: '工作地点是如何确定的?', }, { title: '薪资待遇', // text: '企业可提供的薪资与福利待遇如何?', text: '企业可提供的薪资与福利待遇如何?', }, { title: '职业发展', // text: '我应聘贵单位,你们能提供怎样的职业发展规划?', text: '公司有哪些职业发展通道?', }, { title: '落户政策', // text: '公司是否能协助我落户?', text: '关于落户支持?', } ] return ( <> { list.map((item, index) => { return { const plan: string = '2'; if (plan === '1') { // 方案1.点击后出现在输入框内,用户自己点击发送 setUserInput(item.text); } else { // 方案2.点击后直接发送 doSubmit(item.text) } }} > {item.title} }) } ) } function PromptToast(props: { showToast?: boolean; showModal?: boolean; setShowModal: (_: boolean) => void; }) { const chatStore = useChatStore(); const session = chatStore.currentSession(); const context = session.mask.context; return (
{props.showToast && (
props.setShowModal(true)} > {Locale.Context.Toast(context.length)}
)} {props.showModal && ( props.setShowModal(false)} /> )}
); } function useSubmitHandler() { const config = useAppConfig(); const submitKey = config.submitKey; const isComposing = useRef(false); useEffect(() => { const onCompositionStart = () => { isComposing.current = true; }; const onCompositionEnd = () => { isComposing.current = false; }; window.addEventListener("compositionstart", onCompositionStart); window.addEventListener("compositionend", onCompositionEnd); return () => { window.removeEventListener("compositionstart", onCompositionStart); window.removeEventListener("compositionend", onCompositionEnd); }; }, []); const shouldSubmit = (e: React.KeyboardEvent) => { // Fix Chinese input method "Enter" on Safari if (e.keyCode == 229) return false; if (e.key !== "Enter") return false; if (e.key === "Enter" && (e.nativeEvent.isComposing || isComposing.current)) return false; return ( (config.submitKey === SubmitKey.AltEnter && e.altKey) || (config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) || (config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) || (config.submitKey === SubmitKey.MetaEnter && e.metaKey) || (config.submitKey === SubmitKey.Enter && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey) ); }; return { submitKey, shouldSubmit, }; } export type RenderPrompt = Pick; export function PromptHints(props: { prompts: RenderPrompt[]; onPromptSelect: (prompt: RenderPrompt) => void; }) { const noPrompts = props.prompts.length === 0; const [selectIndex, setSelectIndex] = useState(0); const selectedRef = useRef(null); useEffect(() => { setSelectIndex(0); }, [props.prompts.length]); useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (noPrompts || e.metaKey || e.altKey || e.ctrlKey) { return; } // arrow up / down to select prompt const changeIndex = (delta: number) => { e.stopPropagation(); e.preventDefault(); const nextIndex = Math.max( 0, Math.min(props.prompts.length - 1, selectIndex + delta), ); setSelectIndex(nextIndex); selectedRef.current?.scrollIntoView({ block: "center", }); }; if (e.key === "ArrowUp") { changeIndex(1); } else if (e.key === "ArrowDown") { changeIndex(-1); } else if (e.key === "Enter") { const selectedPrompt = props.prompts.at(selectIndex); if (selectedPrompt) { props.onPromptSelect(selectedPrompt); } } }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.prompts.length, selectIndex]); if (noPrompts) return null; return (
{props.prompts.map((prompt, i) => (
props.onPromptSelect(prompt)} onMouseEnter={() => setSelectIndex(i)} >
{prompt.title}
{prompt.content}
))}
); } function ClearContextDivider() { const chatStore = useChatStore(); return (
chatStore.updateCurrentSession( (session) => (session.clearContextIndex = undefined), ) } >
{Locale.Context.Clear}
{Locale.Context.Revert}
); } export function ChatAction(props: { text: string; icon: JSX.Element; onClick: () => void; }) { const iconRef = useRef(null); const textRef = useRef(null); const [width, setWidth] = useState({ full: 16, icon: 16, }); function updateWidth() { if (!iconRef.current || !textRef.current) return; const getWidth = (dom: HTMLDivElement) => dom.getBoundingClientRect().width; const textWidth = getWidth(textRef.current); const iconWidth = getWidth(iconRef.current); setWidth({ full: textWidth + iconWidth, icon: iconWidth, }); } return (
{ props.onClick(); setTimeout(updateWidth, 1); }} onMouseEnter={updateWidth} onTouchStart={updateWidth} style={ { "--icon-width": `${width.icon}px`, "--full-width": `${width.full}px`, } as React.CSSProperties } >
{props.icon}
{props.text}
); } function useScrollToBottom( scrollRef: RefObject, detach: boolean = false, ) { // for auto-scroll const [autoScroll, setAutoScroll] = useState(true); function scrollDomToBottom() { const dom = scrollRef.current; if (dom) { requestAnimationFrame(() => { setAutoScroll(true); dom.scrollTo(0, dom.scrollHeight); }); } } // auto scroll useEffect(() => { if (autoScroll && !detach) { scrollDomToBottom(); } }); return { scrollRef, autoScroll, setAutoScroll, scrollDomToBottom, }; } export function ChatActions(props: { isClickStop: boolean, sendStatus: boolean, setSendStatus: (status: boolean) => void; setUserInput: (value: string) => void; doSubmit: (userInput: string) => void; uploadImage: () => void; setAttachImages: (images: string[]) => void; setUploading: (uploading: boolean) => void; showPromptModal: () => void; scrollToBottom: () => void; showPromptHints: () => void; hitBottom: boolean; uploading: boolean; }) { const config = useAppConfig(); const navigate = useNavigate(); const chatStore = useChatStore(); // switch themes const theme = config.theme; function nextTheme() { const themes = [Theme.Auto, Theme.Light, Theme.Dark]; const themeIndex = themes.indexOf(theme); const nextIndex = (themeIndex + 1) % themes.length; const nextTheme = themes[nextIndex]; config.update((config) => (config.theme = nextTheme)); } // stop all responses const couldStop = ChatControllerPool.hasPending(); const stopAll = () => ChatControllerPool.stopAll(); // switch model const currentModel = chatStore.currentSession().mask.modelConfig.model; const currentProviderName = chatStore.currentSession().mask.modelConfig?.providerName || ServiceProvider.OpenAI; const allModels = useAllModels(); const models = useMemo(() => { const filteredModels = allModels.filter((m) => m.available); const defaultModel = filteredModels.find((m) => m.isDefault); if (defaultModel) { const arr = [ defaultModel, ...filteredModels.filter((m) => m !== defaultModel), ]; return arr; } else { return filteredModels; } }, [allModels]); const currentModelName = useMemo(() => { const model = models.find( (m) => m.name == currentModel && m?.provider?.providerName == currentProviderName, ); return model?.displayName ?? ""; }, [models, currentModel, currentProviderName]); const [showModelSelector, setShowModelSelector] = useState(false); const [showPluginSelector, setShowPluginSelector] = useState(false); const [showUploadImage, setShowUploadImage] = useState(false); type GuessList = string[] const [guessList, setGuessList] = useState([]); const [showSizeSelector, setShowSizeSelector] = useState(false); const dalle3Sizes: DalleSize[] = ["1024x1024", "1792x1024", "1024x1792"]; const currentSize = chatStore.currentSession().mask.modelConfig?.size ?? "1024x1024"; const session = chatStore.currentSession(); useEffect(() => { const show = isVisionModel(currentModel); setShowUploadImage(show); if (!show) { props.setAttachImages([]); props.setUploading(false); } // if current model is not available // switch to first available model const isUnavaliableModel = !models.some((m) => m.name === currentModel); if (isUnavaliableModel && models.length > 0) { // show next model to default model if exist let nextModel = models.find((model) => model.isDefault) || models[0]; chatStore.updateCurrentSession((session) => { session.mask.modelConfig.model = nextModel.name; session.mask.modelConfig.providerName = nextModel?.provider?.providerName as ServiceProvider; }); showToast( nextModel?.provider?.providerName == "ByteDance" ? nextModel.displayName : nextModel.name, ); } }, [chatStore, currentModel, models]); const fetchGuessList = async (record: { content: string; role: string }) => { try { const data = { appId: session.appId, messages: [ { content: record.content, role: record.role, } ], chat_id: session.chat_id, } let url = ''; if (chatStore.chatMode === 'LOCAL') { url = '/deepseek/api/async/completions'; } else { url = '/bigmodel/api/async/completions'; } const res = await api.post(url, data); setGuessList(res.data); } catch (error) { console.error(error); } } useEffect(() => { setGuessList([]); if (chatStore.message.content) { fetchGuessList(chatStore.message); } }, [chatStore.message]); useEffect(() => { if (props.isClickStop) { props.setSendStatus(false); setGuessList([]); } }, [props.isClickStop]) const [activeKey, setActiveKey]= useState('0'); // 控制手风琴是否展示 return (
{ props.sendStatus && {setActiveKey(key[0])}} bordered={false} style={{ width: '100%',backgroundColor:'#fff' }} expandIconPosition="end" items={[ { key: '1', label:你还可以尝试提问:, children:
{/*
你还可以尝试提问:
*/} { guessList.length === 0 ? :
{ guessList.map((item, index) => { return (
{ props.setUserInput(item); props.doSubmit(item) setActiveKey('') }} key={index} > {item}
) }) }
}
} ]} /> } {/* { props.sendStatus &&
你还可以尝试提问:
{ guessList.length === 0 ? :
{ guessList.map((item, index) => { return (
{ props.setUserInput(item); props.doSubmit(item) }} key={index} > {item}
) }) }
}
} */} {/* {couldStop && ( } /> )} */} {/* {!props.hitBottom && ( } /> )} */} {/* {props.hitBottom && ( } /> )} */} {/* {showUploadImage && ( : } /> )} */} {/* {theme === Theme.Auto ? ( ) : theme === Theme.Light ? ( ) : theme === Theme.Dark ? ( ) : null} } /> */} {/* */} {/* } /> */} {/* { navigate(Path.Masks); }} text={Locale.Chat.InputActions.Masks} icon={} /> */} {/* } onClick={() => { chatStore.updateCurrentSession((session) => { if (session.clearContextIndex === session.messages.length) { session.clearContextIndex = undefined; } else { session.clearContextIndex = session.messages.length; session.memoryPrompt = ""; // will clear memory } }); }} /> */} {/* setShowModelSelector(true)} text={currentModelName} icon={} /> */} {showModelSelector && ( ({ title: `${m.displayName}${m?.provider?.providerName ? "(" + m?.provider?.providerName + ")" : "" }`, value: `${m.name}@${m?.provider?.providerName}`, }))} onClose={() => setShowModelSelector(false)} onSelection={(s) => { if (s.length === 0) return; const [model, providerName] = s[0].split("@"); chatStore.updateCurrentSession((session) => { session.mask.modelConfig.model = model as ModelType; session.mask.modelConfig.providerName = providerName as ServiceProvider; session.mask.syncGlobalConfig = false; }); if (providerName == "ByteDance") { const selectedModel = models.find( (m) => m.name == model && m?.provider?.providerName == providerName, ); showToast(selectedModel?.displayName ?? ""); } else { showToast(model); } }} /> )} {isDalle3(currentModel) && ( setShowSizeSelector(true)} text={currentSize} icon={} /> )} {showSizeSelector && ( ({ title: m, value: m, }))} onClose={() => setShowSizeSelector(false)} onSelection={(s) => { if (s.length === 0) return; const size = s[0]; chatStore.updateCurrentSession((session) => { session.mask.modelConfig.size = size; }); showToast(size); }} /> )} {/* setShowPluginSelector(true)} text={Locale.Plugin.Name} icon={} /> */} {showPluginSelector && ( setShowPluginSelector(false)} onSelection={(s) => { const plugin = s[0]; chatStore.updateCurrentSession((session) => { session.mask.plugin = s; }); if (plugin) { showToast(plugin); } }} /> )}
); } export function EditMessageModal(props: { onClose: () => void }) { const chatStore = useChatStore(); const session = chatStore.currentSession(); const [messages, setMessages] = useState(session.messages.slice()); return (
} key="cancel" onClick={() => { props.onClose(); }} />, } key="ok" onClick={() => { chatStore.updateCurrentSession( (session) => (session.messages = messages), ); props.onClose(); }} />, ]} > chatStore.updateCurrentSession( (session) => (session.topic = e.currentTarget.value), ) } > { const newMessages = messages.slice(); updater(newMessages); setMessages(newMessages); }} />
); } export function DeleteImageButton(props: { deleteImage: () => void }) { return (
); } function _Chat() { type RenderMessage = ChatMessage & { preview?: boolean }; const chatStore = useChatStore(); const session = chatStore.currentSession(); const config = useAppConfig(); config.sendPreviewBubble = false; const fontSize = config.fontSize; const fontFamily = config.fontFamily; const [showExport, setShowExport] = useState(false); const inputRef = useRef(null); const [userInput, setUserInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const [sendStatus, setSendStatus] = useState(false); const { submitKey, shouldSubmit } = useSubmitHandler(); const scrollRef = useRef(null); const isScrolledToBottom = scrollRef?.current ? Math.abs( scrollRef.current.scrollHeight - (scrollRef.current.scrollTop + scrollRef.current.clientHeight), ) <= 1 : false; const { setAutoScroll, scrollDomToBottom } = useScrollToBottom( scrollRef, isScrolledToBottom, ); const [hitBottom, setHitBottom] = useState(true); const isMobileScreen = useMobileScreen(); const navigate = useNavigate(); const [attachImages, setAttachImages] = useState([]); const [uploading, setUploading] = useState(false); // prompt hints const promptStore = usePromptStore(); const [promptHints, setPromptHints] = useState([]); const onSearch = useDebouncedCallback( (text: string) => { const matchedPrompts = promptStore.search(text); setPromptHints(matchedPrompts); }, 100, { leading: true, trailing: true }, ); type AppList = { label: string, value: string, desc: string, }[]; const [appList, setAppList] = useState([]); const [appValue, setAppValue] = useState(); const globalStore = useGlobalStore(); type QuestionList = string[]; const [questionList, setQuestionList] = useState([]); const location = useLocation(); const [loading, setLoading] = useState(false); const fruits = [ { id: 'LOCAL', name: 'deepseek' }, { id: 'ONLINE', name: '智谱AI' }, ]; const [selectedFruit, setSelectedFruit] = React.useState(chatStore.chatMode); // 获取应用列表 const fetchApplicationList = async (chatMode?: string) => { let mode = chatMode || selectedFruit; setLoading(true); try { let url = null; if (mode === 'LOCAL') { url = '/deepseek/api/application/list'; } else { url = '/bigmodel/api/application/list'; } const res = await api.get(url); const list = res.data.filter((item: any) => item.appId !== '1234567890123456789').map((item: any) => { return { label: item.name, value: item.appId, desc: item.desc, } }) setAppList(list); const search = location.search; const params = new URLSearchParams(search); const appId = params.get('appId') || ''; if (appId) { setAppValue(appId); } else { setAppValue(list[0]?.value); } } catch (error) { console.error(error); } finally { setLoading(false); } } // 获取预设问题列表 const fetchDefaultQuestion = async (appId: string) => { try { let url = null; if (selectedFruit === 'LOCAL') { url = '/deepseek/api/presets'; } else { url = '/bigmodel/api/presets'; } const res = await api.get(url + `/${appId}`); setQuestionList(res.data); } catch (error) { console.error(error); } finally { setLoading(false); } } const init = async (chatMode?: string) => { await fetchApplicationList(chatMode); } useEffect(() => { const search = location.search; const params = new URLSearchParams(search); const chatMode = params.get('chatMode'); if (!chatMode) { init(); } }, [selectedFruit]) useEffect(() => { const search = location.search; const params = new URLSearchParams(search); const chatMode = params.get('chatMode'); if (chatMode) { setSelectedFruit(chatMode as "ONLINE" | "LOCAL"); const appId = params.get('appId'); if (appId) { setAppValue(appId); globalStore.setSelectedAppId(appId); chatStore.updateCurrentSession((session) => { session.appId = appId; }); } init(chatMode); } }, []) useEffect(() => { if (appValue) { fetchDefaultQuestion(appValue); } }, [appValue, chatStore.chatMode]) const [inputRows, setInputRows] = useState(2); const measure = useDebouncedCallback( () => { const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1; const inputRows = Math.min( 20, Math.max(2 + Number(!isMobileScreen), rows), ); setInputRows(inputRows); }, 100, { leading: true, trailing: true, }, ); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(measure, [userInput]); // chat commands shortcuts const chatCommands = useChatCommand({ new: () => chatStore.newSession(), newm: () => navigate(Path.NewChat), prev: () => chatStore.nextSession(-1), next: () => chatStore.nextSession(1), clear: () => chatStore.updateCurrentSession( (session) => (session.clearContextIndex = session.messages.length), ), del: () => chatStore.deleteSession(chatStore.currentSessionIndex), }); // only search prompts when user input is short const SEARCH_TEXT_LIMIT = 30; const onInput = (text: string) => { setUserInput(text); const n = text.trim().length; // clear search results if (n === 0) { setPromptHints([]); } else if (text.match(ChatCommandPrefix)) { setPromptHints(chatCommands.search(text)); } else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) { // check if need to trigger auto completion if (text.startsWith("/")) { let searchText = text.slice(1); onSearch(searchText); } } }; const doSubmit = (userInput: string) => { if (userInput.trim() === "") return; const matchCommand = chatCommands.match(userInput); if (matchCommand.matched) { setUserInput(""); setPromptHints([]); matchCommand.invoke(); return; } setIsLoading(true); // debugger chatStore.onUserInput([], userInput, attachImages).then(() => setIsLoading(false)); setAttachImages([]); localStorage.setItem(LAST_INPUT_KEY, userInput); setUserInput(""); setPromptHints([]); if (!isMobileScreen) inputRef.current?.focus(); setAutoScroll(true); setSendStatus(true); }; const onPromptSelect = (prompt: RenderPrompt) => { setTimeout(() => { setPromptHints([]); const matchedChatCommand = chatCommands.match(prompt.content); if (matchedChatCommand.matched) { // if user is selecting a chat command, just trigger it matchedChatCommand.invoke(); setUserInput(""); } else { // or fill the prompt setUserInput(prompt.content); } inputRef.current?.focus(); }, 30); }; // stop response const onUserStop = (messageId: string) => { ChatControllerPool.stop(session.id, messageId); }; useEffect(() => { chatStore.updateCurrentSession((session) => { const stopTiming = Date.now() - REQUEST_TIMEOUT_MS; session.messages.forEach((m) => { // check if should stop all stale messages if (m.isError || new Date(m.date).getTime() < stopTiming) { if (m.streaming) { m.streaming = false; } if (m.content.length === 0) { m.isError = true; m.content = prettyObject({ error: true, message: "empty response", }); } } }); // auto sync mask config from global config if (session.mask.syncGlobalConfig) { console.log("[Mask] syncing from global, name = ", session.mask.name); session.mask.modelConfig = { ...config.modelConfig }; } }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // check if should send message const onInputKeyDown = (e: React.KeyboardEvent) => { if ( e.key === "ArrowUp" && userInput.length <= 0 && !(e.metaKey || e.altKey || e.ctrlKey) ) { setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? ""); e.preventDefault(); return; } if (shouldSubmit(e) && promptHints.length === 0) { doSubmit(userInput); e.preventDefault(); } }; const onRightClick = (e: any, message: ChatMessage) => { // copy to clipboard if (selectOrCopy(e.currentTarget, getMessageTextContent(message))) { if (userInput.length === 0) { setUserInput(getMessageTextContent(message)); } e.preventDefault(); } }; const deleteMessage = (msgId?: string) => { chatStore.updateCurrentSession( (session) => (session.messages = session.messages.filter((m) => m.id !== msgId)), ); }; const onDelete = (msgId: string) => { deleteMessage(msgId); }; const onResend = (message: ChatMessage) => { // when it is resending a message // 1. for a user's message, find the next bot response // 2. for a bot's message, find the last user's input // 3. delete original user input and bot's message // 4. resend the user's input const resendingIndex = session.messages.findIndex( (m) => m.id === message.id, ); if (resendingIndex < 0 || resendingIndex >= session.messages.length) { console.error("[Chat] failed to find resending message", message); return; } let userMessage: ChatMessage | undefined; let botMessage: ChatMessage | undefined; if (message.role === "assistant") { // if it is resending a bot's message, find the user input for it botMessage = message; for (let i = resendingIndex; i >= 0; i -= 1) { if (session.messages[i].role === "user") { userMessage = session.messages[i]; break; } } } else if (message.role === "user") { // if it is resending a user's input, find the bot's response userMessage = message; for (let i = resendingIndex; i < session.messages.length; i += 1) { if (session.messages[i].role === "assistant") { botMessage = session.messages[i]; break; } } } if (userMessage === undefined) { console.error("[Chat] failed to resend", message); return; } // delete the original messages deleteMessage(userMessage.id); deleteMessage(botMessage?.id); // resend the message setIsLoading(true); const textContent = getMessageTextContent(userMessage); const images = getMessageImages(userMessage); chatStore.onUserInput([], textContent, images).then(() => setIsLoading(false)); inputRef.current?.focus(); }; const onPinMessage = (message: ChatMessage) => { chatStore.updateCurrentSession((session) => session.mask.context.push(message), ); showToast(Locale.Chat.Actions.PinToastContent, { text: Locale.Chat.Actions.PinToastAction, onClick: () => { setShowPromptModal(true); }, }); }; const context: RenderMessage[] = useMemo(() => { return session.mask.hideContext ? [] : session.mask.context.slice(); }, [session.mask.context, session.mask.hideContext]); const accessStore = useAccessStore(); if ( context.length === 0 && session.messages.at(0)?.content !== BOT_HELLO.content ) { const copiedHello = Object.assign({}, BOT_HELLO); if (!accessStore.isAuthorized()) { copiedHello.content = Locale.Error.Unauthorized; } context.push(copiedHello); } // preview messages const renderMessages = useMemo(() => { return context.concat(session.messages as RenderMessage[]).concat( isLoading ? [ { ...createMessage({ role: "assistant", content: "……", }), preview: true, }, ] : [], ).concat( userInput.length > 0 && config.sendPreviewBubble ? [ { ...createMessage({ role: "user", content: userInput, }), preview: true, }, ] : [], ); }, [ config.sendPreviewBubble, context, isLoading, session.messages, userInput, ]); const [msgRenderIndex, _setMsgRenderIndex] = useState( Math.max(0, renderMessages.length - CHAT_PAGE_SIZE), ); function setMsgRenderIndex(newIndex: number) { newIndex = Math.min(renderMessages.length - CHAT_PAGE_SIZE, newIndex); newIndex = Math.max(0, newIndex); _setMsgRenderIndex(newIndex); } const messages = useMemo(() => { const endRenderIndex = Math.min( msgRenderIndex + 3 * CHAT_PAGE_SIZE, renderMessages.length, ); return renderMessages.slice(msgRenderIndex, endRenderIndex); }, [msgRenderIndex, renderMessages]); const onChatBodyScroll = (e: HTMLElement) => { const bottomHeight = e.scrollTop + e.clientHeight; const edgeThreshold = e.clientHeight; const isTouchTopEdge = e.scrollTop <= edgeThreshold; const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold; const isHitBottom = bottomHeight >= e.scrollHeight - (isMobileScreen ? 4 : 10); const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE; const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE; if (isTouchTopEdge && !isTouchBottomEdge) { setMsgRenderIndex(prevPageMsgIndex); } else if (isTouchBottomEdge) { setMsgRenderIndex(nextPageMsgIndex); } setHitBottom(isHitBottom); setAutoScroll(isHitBottom); }; function scrollToBottom() { setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE); scrollDomToBottom(); } // clear context index = context length + index in messages const clearContextIndex = (session.clearContextIndex ?? -1) >= 0 ? session.clearContextIndex! + context.length - msgRenderIndex : -1; const [showPromptModal, setShowPromptModal] = useState(false); const clientConfig = useMemo(() => getClientConfig(), []); const autoFocus = !isMobileScreen; // wont auto focus on mobile screen const showMaxIcon = !isMobileScreen && !clientConfig?.isApp; useCommand({ fill: setUserInput, submit: (text) => { doSubmit(text); }, code: (text) => { if (accessStore.disableFastLink) return; console.log("[Command] got code from url: ", text); showConfirm(Locale.URLCommand.Code + `code = ${text}`).then((res) => { if (res) { accessStore.update((access) => (access.accessCode = text)); } }); }, settings: (text) => { if (accessStore.disableFastLink) return; try { const payload = JSON.parse(text) as { key?: string; url?: string; }; console.log("[Command] got settings from url: ", payload); if (payload.key || payload.url) { showConfirm( Locale.URLCommand.Settings + `\n${JSON.stringify(payload, null, 4)}`, ).then((res) => { if (!res) return; if (payload.key) { accessStore.update( (access) => (access.openaiApiKey = payload.key!), ); } if (payload.url) { accessStore.update((access) => (access.openaiUrl = payload.url!)); } accessStore.update((access) => (access.useCustomConfig = true)); }); } } catch { console.error("[Command] failed to get settings from url: ", text); } }, }); // edit / insert message modal const [isEditingMessage, setIsEditingMessage] = useState(false); // remember unfinished input useEffect(() => { // try to load from local storage const key = UNFINISHED_INPUT(session.id); const mayBeUnfinishedInput = localStorage.getItem(key); if (mayBeUnfinishedInput && userInput.length === 0) { setUserInput(mayBeUnfinishedInput); localStorage.removeItem(key); } const dom = inputRef.current; return () => { localStorage.setItem(key, dom?.value ?? ""); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handlePaste = useCallback( async (event: React.ClipboardEvent) => { const currentModel = chatStore.currentSession().mask.modelConfig.model; if (!isVisionModel(currentModel)) { return; } const items = (event.clipboardData || window.clipboardData).items; for (const item of items) { if (item.kind === "file" && item.type.startsWith("image/")) { event.preventDefault(); const file = item.getAsFile(); if (file) { const images: string[] = []; images.push(...attachImages); images.push( ...(await new Promise((res, rej) => { setUploading(true); const imagesData: string[] = []; uploadImageRemote(file).then((dataUrl) => { imagesData.push(dataUrl); setUploading(false); res(imagesData); }).catch((e) => { setUploading(false); rej(e); }); })), ); const imagesLength = images.length; if (imagesLength > 3) { images.splice(3, imagesLength - 3); } setAttachImages(images); } } } }, [attachImages, chatStore], ); async function uploadImage() { const images: string[] = []; images.push(...attachImages); images.push( ...(await new Promise((res, rej) => { const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = "image/png, image/jpeg, image/webp, image/heic, image/heif"; fileInput.multiple = true; fileInput.onchange = (event: any) => { setUploading(true); const files = event.target.files; const imagesData: string[] = []; for (let i = 0; i < files.length; i++) { const file = event.target.files[i]; uploadImageRemote(file).then((dataUrl) => { imagesData.push(dataUrl); if ( imagesData.length === 3 || imagesData.length === files.length ) { setUploading(false); res(imagesData); } }).catch((e) => { setUploading(false); rej(e); }); } }; fileInput.click(); })), ); const imagesLength = images.length; if (imagesLength > 3) { images.splice(3, imagesLength - 3); } setAttachImages(images); } const getAppName = () => { const search = location.search; const params = new URLSearchParams(search); const chatMode = params.get('chatMode'); let appId = ''; if (chatMode) { appId = globalStore.selectedAppId; } else { appId = appValue as string; } const item = appList.find(item => item.value === appId); if (!item) { return ''; } return item.label; } const getDesc = () => { const search = location.search; const params = new URLSearchParams(search); const chatMode = params.get('chatMode'); let appId = ''; if (chatMode) { appId = globalStore.selectedAppId; } else { appId = appValue as string; } const item = appList.find(item => item.value === appId); if (!item) { return ''; } return item.desc; } const couldStop = ChatControllerPool.hasPending(); const stopAll = () => ChatControllerPool.stopAll(); const [isClickStop, setIsClickStop] = useState(false); useEffect(() => { if (!couldStop) { setIsClickStop(false) } }, [couldStop]) interface FileIconProps { fileName: string; } const FileIcon: React.FC = (props: FileIconProps) => { const style = { fontSize: '30px', color: '#3875f6', } let icon = if (props.fileName) { const suffix = props.fileName.split('.').pop() || ''; switch (suffix) { case 'pdf': icon = break; case 'txt': icon = break; case 'doc': case 'docx': icon = break; default: break; } } return icon; } const [drawerOpen, setDrawerOpen] = useState(false); const [drawerData, setDrawerData] = useState({ knowledge_id: '', doc_name: '', chunk_info: { doc_id: '', chunk_list: [], } }); const SliceDrawer: React.FC = () => { const [pageLoading, setPageLoading] = useState(false); const [list, setList] = useState([]); const init = async () => { setPageLoading(true); try { const res: any = await api.post('/deepseek/api/slicePage', { knowledge_id: drawerData.knowledge_id, chunk_info: drawerData.chunk_info, pageNum: 1, pageSize: 9999, }); console.log(res.rows, 'res.rows'); setList(res.rows); } catch (error) { console.error(error); } finally { setPageLoading(false); } } useEffect(() => { init() }, []) return ( { setDrawerOpen(false); }} > {list.map((item: any, index) => { const score = parseFloat(item.rerankScore); const formattedScore = isNaN(score) ? '0.00' : score.toFixed(2); return
片段{index + 1}
rerank得分{formattedScore}
{item.sliceText}
})}
) } useEffect(() => { // 当显示欢迎页面时,确保滚动到顶部 if (messages.length <= 1 && scrollRef.current) { setTimeout(() => { if (scrollRef.current) { scrollRef.current.scrollTo(0, 0); } }, 0); } }, [messages.length]); return (
{
{
} { false &&
{ appList.length > 0 ?