| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954 |
- // 第三方库导入
- import { useDebouncedCallback } from "use-debounce";
- import React, {
- useState,
- useRef,
- useEffect,
- useMemo,
- useCallback,
- Fragment,
- RefObject,
- } from "react";
- import dynamic from "next/dynamic";
- import { useNavigate, useLocation } from "react-router-dom";
- // 本地组件和工具导入
- import { IconButton } from "./button";
- import { MaskAvatar } from "./mask";
- import styles from "./chat.module.scss";
- // 图标资源导入
- import LeftIcon from "../icons/left.svg";
- import SendWhiteIcon from "../icons/send-white.svg";
- import BrainIcon from "../icons/brain.svg";
- import CopyIcon from "../icons/copy.svg";
- import LoadingIcon from "../icons/three-dots.svg";
- import ResetIcon from "../icons/reload.svg";
- import DeleteIcon from "../icons/clear.svg";
- import ConfirmIcon from "../icons/confirm.svg";
- import CancelIcon from "../icons/cancel.svg";
- import SizeIcon from "../icons/size.svg";
- import avatar from "../icons/aiIcon.png";
- import sdsk from "../icons/sdsk.png";
- import sdsk_selected from "../icons/sdsk_selected.png";
- import hlw from "../icons/hlw.png";
- import hlw_selected from "../icons/hlw_selected.png";
- import BotIcon from "../icons/bot.svg";
- import BlackBotIcon from "../icons/black-bot.svg";
- // 状态管理和类型导入
- import {
- ChatMessage,
- SubmitKey,
- useChatStore,
- useAccessStore,
- Theme,
- useAppConfig,
- DEFAULT_TOPIC,
- ModelType,
- useGlobalStore,
- } from "../store";
- import { Prompt, usePromptStore } from "../store/prompt";
- // 工具函数导入
- import {
- copyToClipboard,
- selectOrCopy,
- autoGrowTextArea,
- useMobileScreen,
- getMessageTextContent,
- getMessageImages,
- isVisionModel,
- isDalle3,
- } from "../utils";
- import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
- // 客户端和类型导入
- import { ChatControllerPool } from "../client/controller";
- import { DalleSize } from "../typing";
- import type { RequestMessage } from "../client/api";
- // UI 组件导入
- import {
- List,
- ListItem,
- Modal,
- Selector,
- showConfirm,
- showToast,
- } from "./ui-lib";
- // 常量和本地化
- import Locale from "../locales";
- import {
- CHAT_PAGE_SIZE,
- LAST_INPUT_KEY,
- Path,
- REQUEST_TIMEOUT_MS,
- UNFINISHED_INPUT,
- ServiceProvider,
- Plugin,
- } from "../constant";
- import { ContextPrompts, 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 { nanoid } from "nanoid";
- import { message, Upload, UploadProps, Tooltip, Drawer, Button } from "antd";
- import {
- PaperClipOutlined,
- SendOutlined,
- FileOutlined,
- FilePdfOutlined,
- FileTextOutlined,
- FileWordOutlined,
- RightOutlined
- } from '@ant-design/icons';
- // Avatar组件替代实现
- function Avatar( props : { model? : string; avatar? : string } ) {
- if ( props.model ) {
- return (
- <div className="no-dark">
- { props.model?.startsWith( "gpt-4" ) ? (
- <BlackBotIcon className="user-avatar" />
- ) : (
- <BotIcon className="user-avatar" />
- ) }
- </div>
- );
- }
-
- return (
- <div className="user-avatar">
- {/* 移除emoji头像,使用默认bot图标 */ }
- <BotIcon className="user-avatar" />
- </div>
- );
- }
- export function createMessage( override : Partial<ChatMessage> ) : ChatMessage {
- return {
- id: nanoid(),
- date: new Date().toLocaleString(),
- role: "user",
- content: "",
- ...override,
- };
- }
- export const BOT_HELLO : ChatMessage = createMessage( {
- role: "assistant",
- content: '你好,我是小智~\n' +
- '我可以帮助你快速查询作业指导书、规范条文、公司信息等内容,如需获取上述内容,请点击上方导航栏中的「专业知识」或「职能管理」,选择相应的智能体进行提问。无论是现场技术,还是制度流程,我都会尽力为你解答!\n' +
- '请注意:在这个对话框内,我只能请DeepSeek来帮忙回答常见通用问题哦!',
- } );
- const Markdown = dynamic( async () => ( await import("./markdown") ).Markdown, {
- loading: () => <LoadingIcon />,
- } );
- export function SessionConfigModel( props : { onClose : () => void } ) {
- const chatStore = useChatStore();
- const session = chatStore.currentSession();
- const maskStore = useMaskStore();
- const navigate = useNavigate();
-
- return (
- <div className="modal-mask">
- <Modal
- title={ Locale.Context.Edit }
- onClose={ () => props.onClose() }
- actions={ [
- <IconButton
- key="reset"
- icon={ <ResetIcon /> }
- bordered
- text={ Locale.Chat.Config.Reset }
- onClick={ async () => {
- if ( await showConfirm( Locale.Memory.ResetConfirm ) ) {
- chatStore.updateCurrentSession(
- ( session ) => ( session.memoryPrompt = "" ),
- );
- }
- } }
- />,
- <IconButton
- key="copy"
- icon={ <CopyIcon /> }
- bordered
- text={ Locale.Chat.Config.SaveAs }
- onClick={ () => {
- navigate( Path.Masks );
- setTimeout( () => {
- maskStore.create( session.mask );
- }, 500 );
- } }
- />,
- ] }
- >
- <MaskConfig
- mask={ session.mask }
- updateMask={ ( updater ) => {
- const mask = { ...session.mask };
- updater( mask );
- chatStore.updateCurrentSession( ( session ) => ( session.mask = mask ) );
- } }
- shouldSyncFromGlobal
- extraListItems={
- session.mask.modelConfig.sendMemory ? (
- <ListItem
- className="copyable"
- title={ `${ Locale.Memory.Title } (${ session.lastSummarizeIndex } of ${ session.messages.length })` }
- subTitle={ session.memoryPrompt || Locale.Memory.EmptyContent }
- ></ListItem>
- ) : (
- <></>
- )
- }
- ></MaskConfig>
- </Modal>
- </div>
- );
- }
- // 提示词
- 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 <span
- key={ index }
- style={ {
- padding: '5px 10px',
- background: '#f6f7f8',
- color: '#5e5e66',
- borderRadius: 4,
- margin: '0 5px 10px 0',
- cursor: 'pointer',
- fontSize: 12
- } }
- onClick={ () => {
- const plan : string = '2';
- if ( plan === '1' ) {
- // 方案1.点击后出现在输入框内,用户自己点击发送
- setUserInput( item.text );
- } else {
- // 方案2.点击后直接发送
- doSubmit( item.text )
- }
- } }
- >
- { item.title }
- </span>
- } )
- }
- </>
- )
- }
- function PromptToast( props : {
- showToast? : boolean;
- showModal? : boolean;
- setShowModal : ( _ : boolean ) => void;
- } ) {
- const chatStore = useChatStore();
- const session = chatStore.currentSession();
- const context = session.mask.context;
-
- return (
- <div className={ styles[ "prompt-toast" ] } key="prompt-toast">
- { props.showToast && (
- <div
- className={ styles[ "prompt-toast-inner" ] + " clickable" }
- role="button"
- onClick={ () => props.setShowModal( true ) }
- >
- <BrainIcon />
- <span className={ styles[ "prompt-toast-content" ] }>
- { Locale.Context.Toast( context.length ) }
- </span>
- </div>
- ) }
- { props.showModal && (
- <SessionConfigModel onClose={ () => props.setShowModal( false ) } />
- ) }
- </div>
- );
- }
- 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<HTMLTextAreaElement> ) => {
- // 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<Prompt, "title" | "content">;
- export function PromptHints( props : {
- prompts : RenderPrompt[];
- onPromptSelect : ( prompt : RenderPrompt ) => void;
- } ) {
- const noPrompts = props.prompts.length === 0;
- const [ selectIndex, setSelectIndex ] = useState( 0 );
- const selectedRef = useRef<HTMLDivElement>( 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 (
- <div className={ styles[ "prompt-hints" ] }>
- { props.prompts.map( ( prompt, i ) => (
- <div
- ref={ i === selectIndex ? selectedRef : null }
- className={
- styles[ "prompt-hint" ] +
- ` ${ i === selectIndex ? styles[ "prompt-hint-selected" ] : "" }`
- }
- key={ prompt.title + i.toString() }
- onClick={ () => props.onPromptSelect( prompt ) }
- onMouseEnter={ () => setSelectIndex( i ) }
- >
- <div className={ styles[ "hint-title" ] }>{ prompt.title }</div>
- <div className={ styles[ "hint-content" ] }>{ prompt.content }</div>
- </div>
- ) ) }
- </div>
- );
- }
- function ClearContextDivider() {
- const chatStore = useChatStore();
-
- return (
- <div
- className={ styles[ "clear-context" ] }
- onClick={ () =>
- chatStore.updateCurrentSession(
- ( session ) => ( session.clearContextIndex = undefined ),
- )
- }
- >
- <div className={ styles[ "clear-context-tips" ] }>{ Locale.Context.Clear }</div>
- <div className={ styles[ "clear-context-revert-btn" ] }>
- { Locale.Context.Revert }
- </div>
- </div>
- );
- }
- export function ChatAction( props : {
- text : string;
- icon : JSX.Element;
- onClick : () => void;
- } ) {
- const iconRef = useRef<HTMLDivElement>( null );
- const textRef = useRef<HTMLDivElement>( 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 (
- <div
- className={ `${ styles[ "chat-input-action" ] } clickable` }
- onClick={ () => {
- props.onClick();
- setTimeout( updateWidth, 1 );
- } }
- onMouseEnter={ updateWidth }
- onTouchStart={ updateWidth }
- style={
- {
- "--icon-width": `${ width.icon }px`,
- "--full-width": `${ width.full }px`,
- } as React.CSSProperties
- }
- >
- <div ref={ iconRef } className={ styles[ "icon" ] }>
- { props.icon }
- </div>
- <div className={ styles[ "text" ] } ref={ textRef }>
- { props.text }
- </div>
- </div>
- );
- }
- function useScrollToBottom(
- scrollRef : RefObject<HTMLDivElement>,
- 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 : {
- 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<GuessList>( [] );
-
- 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 ] );
-
- return (
- <div className={ styles[ "chat-input-actions" ] }>
- { showModelSelector && (
- <Selector
- defaultSelectedValue={ `${ currentModel }@${ currentProviderName }` }
- items={ models.map( ( m ) => ( {
- 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 ) && (
- <ChatAction
- onClick={ () => setShowSizeSelector( true ) }
- text={ currentSize }
- icon={ <SizeIcon /> }
- />
- ) }
-
- { showSizeSelector && (
- <Selector
- defaultSelectedValue={ currentSize }
- items={ dalle3Sizes.map( ( m ) => ( {
- 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 );
- } }
- />
- ) }
-
- { showPluginSelector && (
- <Selector
- multiple
- defaultSelectedValue={ chatStore.currentSession().mask?.plugin }
- items={ [
- {
- title: Locale.Plugin.Artifacts,
- value: Plugin.Artifacts,
- },
- ] }
- onClose={ () => setShowPluginSelector( false ) }
- onSelection={ ( s ) => {
- const plugin = s[ 0 ];
- chatStore.updateCurrentSession( ( session ) => {
- session.mask.plugin = s;
- } );
- if ( plugin ) {
- showToast( plugin );
- }
- } }
- />
- ) }
- </div>
- );
- }
- export function EditMessageModal( props : { onClose : () => void } ) {
- const chatStore = useChatStore();
- const session = chatStore.currentSession();
- const [ messages, setMessages ] = useState( session.messages.slice() );
-
- return (
- <div className="modal-mask">
- <Modal
- title={ Locale.Chat.EditMessage.Title }
- onClose={ props.onClose }
- actions={ [
- <IconButton
- text={ Locale.UI.Cancel }
- icon={ <CancelIcon /> }
- key="cancel"
- onClick={ () => {
- props.onClose();
- } }
- />,
- <IconButton
- type="primary"
- text={ Locale.UI.Confirm }
- icon={ <ConfirmIcon /> }
- key="ok"
- onClick={ () => {
- chatStore.updateCurrentSession(
- ( session ) => ( session.messages = messages ),
- );
- props.onClose();
- } }
- />,
- ] }
- >
- <List>
- <ListItem
- title={ Locale.Chat.EditMessage.Topic.Title }
- subTitle={ Locale.Chat.EditMessage.Topic.SubTitle }
- >
- <input
- type="text"
- value={ session.topic }
- onInput={ ( e ) =>
- chatStore.updateCurrentSession(
- ( session ) => ( session.topic = e.currentTarget.value ),
- )
- }
- ></input>
- </ListItem>
- </List>
- <ContextPrompts
- context={ messages }
- updateContext={ ( updater ) => {
- const newMessages = messages.slice();
- updater( newMessages );
- setMessages( newMessages );
- } }
- />
- </Modal>
- </div>
- );
- }
- export function DeleteImageButton( props : { deleteImage : () => void } ) {
- return (
- <div className={ styles[ "delete-image" ] } onClick={ props.deleteImage }>
- <DeleteIcon />
- </div>
- );
- }
- 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<HTMLTextAreaElement>( null );
- const [ userInput, setUserInput ] = useState( "" );
- const [ isLoading, setIsLoading ] = useState( false );
- const { submitKey, shouldSubmit } = useSubmitHandler();
- const scrollRef = useRef<HTMLDivElement>( 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<string[]>( [] );
- const [ uploading, setUploading ] = useState( false );
-
- // prompt hints
- const promptStore = usePromptStore();
- const [ promptHints, setPromptHints ] = useState<RenderPrompt[]>( [] );
- const onSearch = useDebouncedCallback(
- ( text : string ) => {
- const matchedPrompts = promptStore.search( text );
- setPromptHints( matchedPrompts );
- },
- 100,
- { leading: true, trailing: true },
- );
-
- useEffect( () => {
- chatStore.updateCurrentSession( ( session ) => {
- session.appId = '1881269958412521255';
- } );
- }, [] )
-
- 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.MaskChat), // 关闭mask入口 ,后续有需求再二开
- 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 );
- chatStore.onUserInput( fileList, userInput, attachImages ).then( () => setIsLoading( false ) );
- setAttachImages( [] );
- localStorage.setItem( LAST_INPUT_KEY, userInput );
- setUserInput( "" );
- setPromptHints( [] );
- if ( !isMobileScreen ) inputRef.current?.focus();
- setAutoScroll( 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<HTMLTextAreaElement> ) => {
- 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<HTMLTextAreaElement> ) => {
- 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<string[]>( ( 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<string[]>( ( 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 [ fileList, setFileList ] = useState<any[]>( [] );
-
- // 上传配置
- const uploadConfig : UploadProps = {
- action: '/deepseek-api' + '/upload/file',
- method: 'POST',
- accept: [ '.pdf', '.txt', '.doc', '.docx' ].join( ',' ),
- };
-
- interface FileIconProps {
- fileName : string;
- }
-
- const FileIcon : React.FC<FileIconProps> = ( props : FileIconProps ) => {
- const style = {
- fontSize: '30px',
- color: '#3875f6',
- }
-
- let icon = <FileOutlined style={ style } />
- if ( props.fileName ) {
- const suffix = props.fileName.split( '.' ).pop() || '';
- switch ( suffix ) {
- case 'pdf':
- icon = <FilePdfOutlined style={ style } />
- break;
- case 'txt':
- icon = <FileTextOutlined style={ style } />
- break;
- case 'doc':
- case 'docx':
- icon = <FileWordOutlined style={ style } />
- break;
- default:
- break;
- }
- }
- return icon;
- }
-
- const [ isDeepThink, setIsDeepThink ] = useState<boolean>( chatStore.isDeepThink );
-
- // 切换聊天窗口后清理上传文件信息
- useEffect( () => {
- setFileList( [] )
- }, [ chatStore.currentSession() ] )
-
- const couldStop = ChatControllerPool.hasPending();
- const stopAll = () => ChatControllerPool.stopAll();
-
- // 切换聊天窗口后清理上传文件信息
- useEffect( () => {
- setWebSearch( false );
- }, [ chatStore.currentSession() ] )
-
- const [ webSearch, setWebSearch ] = useState<boolean>( chatStore.web_search );
-
- const [ drawerOpen, setDrawerOpen ] = useState( false );
- type DrawerList = {
- title : string,
- content : string,
- web_url : string,
- }[]
- const [ drawerList, setDrawerList ] = useState<DrawerList>( [] );
-
- interface NetworkDrawerProps {
- list : DrawerList,
- }
-
- const NetworkDrawer : React.FC<NetworkDrawerProps> = ( props ) => {
- return (
- <Drawer
- title='网页搜索'
- open={ drawerOpen }
- onClose={ () => {
- setDrawerOpen( false );
- } }
- >
- { props.list.map( ( item, index ) => {
- return <div
- style={ {
- padding: 10,
- background: '#fafafa',
- borderRadius: 4,
- marginBottom: 10,
- cursor: 'pointer',
- } }
- key={ index }
- onClick={ () => {
- window.open( item.web_url );
- } }
- >
- <div style={ {
- margin: '5px 0',
- fontSize: 16,
- display: '-webkit-box',
- WebkitBoxOrient: 'vertical',
- WebkitLineClamp: 2,// 限制显示两行
- overflow: 'hidden',
- } }>
- { item.title }
- </div>
- <div style={ {
- color: '#afafaf',
- display: '-webkit-box',
- WebkitBoxOrient: 'vertical',
- WebkitLineClamp: 4,// 限制显示两行
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- } }>
- { item.content }
- </div>
- </div>
- } )
- }
- </Drawer>
- )
- }
-
- return (
- <div className={ styles.chat } key={ session.id }>
- {
- isMobileScreen && location.pathname !== '/' &&
- <div className="window-header" data-tauri-drag-region>
- <div style={ { display: 'flex', alignItems: 'center' } }
- className={ `window-header-title ${ styles[ "chat-body-title" ] }` }>
- <div>
- <IconButton
- style={ { padding: 0, marginRight: 20 } }
- icon={ <LeftIcon /> }
- text={ Locale.NewChat.Return }
- onClick={ () => navigate( '/deepseekChat' ) }
- />
- </div>
- </div>
- </div>
- }
- <div
- className={ styles[ "chat-body" ] }
- ref={ scrollRef }
- onScroll={ ( e ) => onChatBodyScroll( e.currentTarget ) }
- onMouseDown={ () => inputRef.current?.blur() }
- onTouchStart={ () => {
- inputRef.current?.blur();
- setAutoScroll( false );
- } }
- >
- <>
- { messages.map( ( message, i ) => {
- const isUser = message.role === "user";
- const isContext = i < context.length;
- const showActions =
- i > 0 &&
- !( message.preview || message.content.length === 0 ) &&
- !isContext;
- const showTyping = message.preview || message.streaming;
-
- const shouldShowClearContextDivider = i === clearContextIndex - 1;
-
- return (
- <Fragment key={ message.id }>
- <div
- className={
- isUser ? styles[ "chat-message-user" ] : styles[ "chat-message" ]
- }
- >
- <div className={ styles[ "chat-message-container" ] }
- style={ { display: 'flex', flexDirection: 'column' } }>
- <div className={ styles[ "chat-message-header" ] }>
- <div className={ styles[ "chat-message-avatar" ] }>
- { isUser ? (
- // 在这里换头像
- <div style={ { position: 'relative' } }>
- <div
- style={ {
- position: 'absolute',
- zIndex: 2,
- top: '50%',
- left: '50%',
- transform: ' translate(-110%, -100%)',
- fontSize: 14,
- } }>
- 我
- </div>
- </div>
- ) : (
- <>
- { [ "system" ].includes( message.role ) ? (
- <Avatar avatar="2699-fe0f" />
- ) : (
- <MaskAvatar
- avatar={ session.mask.avatar }
- model={
- message.model || session.mask.modelConfig.model
- }
- />
- ) }
- </>
- ) }
- </div>
- </div>
- {
- isUser && message.document && message.document.id &&
- <a style={ {
- padding: '10px',
- background: '#f7f7f7',
- borderRadius: '10px',
- textDecoration: 'none',
- color: '#24292f',
- display: 'flex',
- alignItems: 'center'
- } } href={ message.document.url } target="_blank">
- <FileIcon fileName={ message.document.name } />
- <div style={ { marginLeft: 8, fontSize: '14px' } }>
- { message.document.name }
- </div>
- </a>
- }
- {/* {showTyping && (
- <div className={styles["chat-message-status"]}>
- 正在输入…
- </div>
- )} */ }
- {
- message.networkInfo && message.networkInfo.list.length > 0 &&
- <div style={ { marginTop: 10 } }>
- <Button
- icon={ <RightOutlined /> }
- iconPosition='end'
- onClick={ () => {
- setDrawerList( message.networkInfo!.list );
- setDrawerOpen( true );
- } }
- >
- 搜索到{ message.networkInfo.list.length }篇相关资料
- </Button>
- {
- drawerOpen &&
- <NetworkDrawer
- list={ message.networkInfo.list }
- />
- }
- </div>
- }
- <div className={ styles[ "chat-message-item" ] }>
- <Markdown
- key={ message.streaming ? "loading" : "done" }
- content={ getMessageTextContent( message ) }
- loading={
- ( message.preview || message.streaming ) &&
- message.content.length === 0 &&
- !isUser
- }
- onDoubleClickCapture={ () => {
- if ( !isMobileScreen ) return;
- setUserInput( getMessageTextContent( message ) );
- } }
- fontSize={ fontSize }
- fontFamily={ fontFamily }
- parentRef={ scrollRef }
- defaultShow={ i >= messages.length - 6 }
- />
- { getMessageImages( message ).length == 1 && (
- <img
- className={ styles[ "chat-message-item-image" ] }
- src={ getMessageImages( message )[ 0 ] }
- alt=""
- />
- ) }
- { getMessageImages( message ).length > 1 && (
- <div
- className={ styles[ "chat-message-item-images" ] }
- style={
- {
- "--image-count": getMessageImages( message ).length,
- } as React.CSSProperties
- }
- >
- { getMessageImages( message ).map( ( image, index ) => {
- return (
- <img
- className={
- styles[ "chat-message-item-image-multi" ]
- }
- key={ index }
- src={ image }
- alt=""
- />
- );
- } ) }
- </div>
- ) }
- </div>
- </div>
- </div>
- { shouldShowClearContextDivider && <ClearContextDivider /> }
- </Fragment>
- );
- } ) }
- </>
- </div>
- <div className={ styles[ "chat-input-panel" ] }>
- <ChatActions
- setUserInput={ setUserInput }
- doSubmit={ doSubmit }
- uploadImage={ uploadImage }
- setAttachImages={ setAttachImages }
- setUploading={ setUploading }
- showPromptModal={ () => setShowPromptModal( true ) }
- scrollToBottom={ scrollToBottom }
- hitBottom={ hitBottom }
- uploading={ uploading }
- showPromptHints={ () => {
- if ( promptHints.length > 0 ) {
- setPromptHints( [] );
- return;
- }
- inputRef.current?.focus();
- setUserInput( "/" );
- onSearch( "" );
- } }
- />
- {
- fileList.length > 0 &&
- <div style={ { marginBottom: 20 } }>
- <Upload
- fileList={ fileList }
- onRemove={ ( file ) => {
- setFileList( fileList.filter( item => item.uid !== file.uid ) );
- } }
- />
- </div>
- }
- <label
- className={ `${ styles[ "chat-input-panel-inner" ] } ${ attachImages.length != 0
- ? styles[ "chat-input-panel-inner-attach" ]
- : ""
- }` }
- htmlFor="chat-input"
- >
- <textarea
- id="chat-input"
- ref={ inputRef }
- className={ styles[ "chat-input2" ] }
- placeholder={ Locale.Chat.Input( submitKey ) }
- onInput={ ( e ) => onInput( e.currentTarget.value ) }
- value={ userInput }
- onKeyDown={ onInputKeyDown }
- onFocus={ scrollToBottom }
- onClick={ scrollToBottom }
- onPaste={ handlePaste }
- rows={ inputRows }
- autoFocus={ autoFocus }
- style={ {
- fontSize: config.fontSize,
- fontFamily: config.fontFamily,
- } }
- />
- { attachImages.length != 0 && (
- <div className={ styles[ "attach-images" ] }>
- { attachImages.map( ( image, index ) => {
- return (
- <div
- key={ index }
- className={ styles[ "attach-image" ] }
- style={ { backgroundImage: `url("${ image }")` } }
- >
- <div className={ styles[ "attach-image-mask" ] }>
- <DeleteImageButton
- deleteImage={ () => {
- setAttachImages(
- attachImages.filter( ( _, i ) => i !== index ),
- );
- } }
- />
- </div>
- </div>
- );
- } ) }
- </div>
- ) }
- {/* 修改样式:输入框内部按钮区域 */ }
- {/* <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 10 }}> */ }
- <div className={ styles[ "chat-input-bottom-bar" ] }>
- {/* <div style={{ display: 'flex', alignItems: 'center' }}> */ }
- <div className={ styles[ "left-options" ] }>
-
- {/*深度思考R1按钮*/ }
-
- <Tooltip
- title={
- <span style={ { fontSize: 12, lineHeight: 1.4, minHeight: 24, padding: '4px 8px' } }>
- { isDeepThink ? '关闭深度思考模式' : '启用深度思考模式' }
- </span>
- }
- placement="left"
- >
- <div
- // className={styles["option-item"]}
- style={ {
- padding: '0 12px',
- height: 28,
- borderRadius: 18,
- fontSize: 12,
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- // marginRight: 10,
- cursor: 'pointer',
- background: isDeepThink ? '#dee9fc' : '#f3f4f6',
- color: isDeepThink ? '#3875f6' : '#000000',
- // border: `1px solid ${isDeepThink ? '#3875f6' : 'transparent'}`,
- transition: 'all 0.2s ease',
- userSelect: 'none'
- } }
- onClick={ () => {
- setIsDeepThink( !isDeepThink );
- chatStore.setIsDeepThink( !isDeepThink );
- } }
- >
- <img src={ isDeepThink ? sdsk_selected.src : sdsk.src }
- style={ {
- width: 16,
- height: 16,
- } }
- />
- <span style={ { fontSize: 11, marginLeft: 5 } }>
- 深度思考
- </span>
- </div>
- </Tooltip>
- {/*联网搜索按钮*/ }
- <div style={ {
- padding: '0 12px',
- height: 28,
- borderRadius: 18,
- fontSize: 12,
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- cursor: 'pointer',
- background: webSearch ? '#dee9fc' : '#f3f4f6',
- color: webSearch ? '#3875f6' : '#000000',
- transition: 'all 0.2s ease',
- userSelect: 'none'
- } }
- onClick={ () => {
- setWebSearch( !webSearch );
- chatStore.setWebSearch( !webSearch );
- } }
- >
-
- <img src={ webSearch ? hlw_selected.src : hlw.src }
- style={ {
- width: 16,
- height: 16,
- } }
- />
- <span style={ { fontSize: 11, marginLeft: 5, marginRight: 10 } }>
- 联网搜索
- </span>
- </div>
- </div>
-
- <div style={ { display: 'flex', alignItems: 'center' } }>
- {
- !webSearch &&
- <div style={ { marginRight: 10 } }>
- <Upload
- { ...uploadConfig }
- showUploadList={ false }
- maxCount={ 1 }
- onChange={ ( info ) => {
- const fileList = info.fileList.map( ( file ) => {
- const data = file.response;
- return {
- ...file,
- url: data?.document_url || file.url,
- documentId: data?.document_id || '',
- }
- } );
- setFileList( fileList );
- if ( info.file.status === 'done' ) {// 上传成功
- const { code, message: msg } = info.file.response;
- if ( code === 200 ) {
- message.success( '上传成功' );
- } else {
- message.error( msg );
- }
- } else if ( info.file.status === 'error' ) {// 上传失败
- message.error( '上传失败' );
- }
- } }
- >
- <Tooltip
- title={
- <div style={ { padding: '4px 8px' } }>
- <div style={ {
- fontSize: 12,
- lineHeight: 1.4,
- marginBottom: 6,
- } }>
- 上传附件 (识别文本和图表中的内容)
- </div>
- <div style={ {
- fontSize: 10,
- color: '#8c8c8c',
- lineHeight: 1.4,
- }}>
- <span>
- 仅支持单个PDF/Word/TXT文件格式
- </span>
- <span>
- (单个文件≤50MB)
- </span>
- </div>
- </div>}
- placement="top"
- >
- <div
- style={ {
- width: 28,
- height: 28,
- borderRadius: '50%',
- background: '#4357d2',
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- cursor: 'pointer',
- transition: 'all 0.2s ease',
- userSelect: 'none'
- } }
- >
- <PaperClipOutlined style={ { color: '#FFFFFF', fontSize: '18px' } } />
- </div>
- </Tooltip>
- </Upload>
- </div>
- }
- <div
- style={ {
- width: 28,
- height: 28,
- borderRadius: '50%',
- background: '#4357d2',
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- cursor: 'pointer',
- } }
- onClick={ () => {
- if ( couldStop ) {
- stopAll();
- } else {
- doSubmit( userInput );
- }
- } }
- >
- {
- couldStop ?
- <div style={ { width: 13, height: 13, background: '#FFFFFF', borderRadius: 2 } }></div>
- :
- <div style={ { transform: 'rotate(-45deg)', padding: '0px 0px 3px 5px' } }>
- <SendOutlined style={ { color: '#FFFFFF' } } />
- </div>
- }
-
- </div>
- </div>
- </div>
- </label>
-
- <div style={ { marginTop: 8, textAlign: 'center', color: '#888888', fontSize: 12 } }>
- 内容由AI生成,仅供参考
- </div>
- </div>
- {
- showExport && (
- <ExportMessageModal onClose={ () => setShowExport( false ) } />
- )
- }
- {
- isEditingMessage && (
- <EditMessageModal
- onClose={ () => {
- setIsEditingMessage( false );
- } }
- />
- )
- }
- </div>
- );
- }
- export function Chat() {
- const globalStore = useGlobalStore();
- const chatStore = useChatStore();
- const sessionIndex = chatStore.currentSessionIndex;
-
- useEffect( () => {
- globalStore.setShowMenu( true );
- chatStore.setModel( 'DeepSeek' );
- chatStore.setWebSearch( false );
- }, [] );
-
- return <_Chat key={ sessionIndex }></_Chat>;
- }
|