1
This commit is contained in:
BIN
src/components/bs-comp/.DS_Store
vendored
Normal file
BIN
src/components/bs-comp/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -1,16 +1,18 @@
|
||||
import { ClearIcon } from "@/components/bs-icons/clear";
|
||||
import { FormIcon } from "@/components/bs-icons/form";
|
||||
import { SendIcon } from "@/components/bs-icons/send";
|
||||
import { Button } from "@/components/bs-ui/button";
|
||||
import { Textarea } from "@/components/bs-ui/input";
|
||||
import { useToast } from "@/components/bs-ui/toast/use-toast";
|
||||
import { locationContext } from "@/contexts/locationContext";
|
||||
import { PauseIcon } from "@radix-ui/react-icons";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMessageStore } from "./messageStore";
|
||||
import GuideQuestions from "./GuideQuestions";
|
||||
import { ClearIcon } from "@/components/bs-icons/clear";
|
||||
import { useMessageStore } from "./messageStore";
|
||||
import { formatDate } from "@/util/utils";
|
||||
import { StopIcon } from "@radix-ui/react-icons";
|
||||
import duihua_send from "../../../assets/chat/duihua-send.png";
|
||||
import { Button } from "@/components/bs-ui/button";
|
||||
import { StopCircle } from "lucide-react";
|
||||
|
||||
export default function ChatInput({ clear, form, questions, inputForm, wsUrl, onBeforSend }) {
|
||||
const { toast } = useToast()
|
||||
@@ -21,11 +23,15 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
|
||||
const [showWhenLocked, setShowWhenLocked] = useState(false) // 强制开启表单按钮,不限制于input锁定
|
||||
const [inputLock, setInputLock] = useState({ locked: false, reason: '' })
|
||||
|
||||
const { messages, chatId, createSendMsg, createWsMsg, updateCurrentMessage, destory, setShowGuideQuestion } = useMessageStore()
|
||||
const { messages, hisMessages, chatId, createSendMsg, createWsMsg, updateCurrentMessage, destory, setShowGuideQuestion } = useMessageStore()
|
||||
const currentChatIdRef = useRef(null)
|
||||
const inputRef = useRef(null)
|
||||
const continueRef = useRef(false)
|
||||
// 停止状态
|
||||
const [isStop, setIsStop] = useState(true)
|
||||
const [stop, setStop] = useState({
|
||||
show: false,
|
||||
disable: false
|
||||
})
|
||||
/**
|
||||
* 记录会话切换状态,等待消息加载完成时,控制表单在新会话自动展开
|
||||
*/
|
||||
@@ -36,16 +42,17 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
|
||||
if (changeChatedRef.current) {
|
||||
changeChatedRef.current = false
|
||||
// 新建的 form 技能,弹出窗口并锁定 input
|
||||
if (form && messages.length === 0) {
|
||||
if (form && messages.length === 0 && hisMessages.length === 0) {
|
||||
setInputLock({ locked: true, reason: '' })
|
||||
setFormShow(true)
|
||||
setShowWhenLocked(true)
|
||||
}
|
||||
}
|
||||
|
||||
}, [messages])
|
||||
}, [messages, hisMessages])
|
||||
useEffect(() => {
|
||||
if (!chatId) return
|
||||
continueRef.current = false
|
||||
setInputLock({ locked: false, reason: '' })
|
||||
// console.log('message chatid', messages, form, chatId);
|
||||
setShowWhenLocked(false)
|
||||
@@ -85,13 +92,14 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
|
||||
const event = new Event('input', { bubbles: true, cancelable: true });
|
||||
inputRef.current.value = ''
|
||||
inputRef.current.dispatchEvent(event); // 触发调节input高度
|
||||
const [wsMsg, inputKey] = onBeforSend('', value)
|
||||
const contunue = continueRef.current ? 'continue' : ''
|
||||
continueRef.current = false
|
||||
const [wsMsg, inputKey] = onBeforSend(contunue, value)
|
||||
// msg to store
|
||||
createSendMsg(wsMsg.inputs, inputKey)
|
||||
// 锁定 input
|
||||
setInputLock({ locked: true, reason: '' })
|
||||
await createWebSocket(chatId)
|
||||
// console.log(wsMsg,inputKey);
|
||||
sendWsMsg(wsMsg)
|
||||
|
||||
// 滚动聊天到底
|
||||
@@ -100,15 +108,13 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
|
||||
messageDom.scrollTop = messageDom.scrollHeight;
|
||||
}
|
||||
}
|
||||
const stop = async () => {
|
||||
const [wsMsg] = onBeforSend('', '')
|
||||
wsMsg.action = "stop"
|
||||
sendWsMsg(wsMsg)
|
||||
// console.log(wsMsg);
|
||||
// sendWsMsg(wsMsg)
|
||||
}
|
||||
|
||||
const diffRef = useRef(0)
|
||||
const sendWsMsg = async (msg) => {
|
||||
try {
|
||||
diffRef.current = Date.now()
|
||||
// console.log('WebSocket send: ' + diffRef.current + ' 毫秒');
|
||||
|
||||
wsRef.current.send(JSON.stringify(msg))
|
||||
} catch (error) {
|
||||
toast({
|
||||
@@ -128,14 +134,23 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
try {
|
||||
let startTime = Date.now();
|
||||
const ws = new WebSocket(`${webSocketProtocol}://${wsUrl}&chat_id=${chatId}`)
|
||||
wsRef.current = ws
|
||||
// websocket linsen
|
||||
ws.onopen = () => {
|
||||
// 记录连接成功的时间
|
||||
let endTime = Date.now();
|
||||
|
||||
// 计算连接建立所需的时间
|
||||
let connectionTime = endTime - startTime;
|
||||
|
||||
// console.log('WebSocket 连接建立时间: ' + connectionTime + ' 毫秒');
|
||||
console.log("WebSocket connection established!");
|
||||
res('ok')
|
||||
};
|
||||
ws.onmessage = (event) => {
|
||||
// console.log(`WebSocket get: ${Date.now()} 毫秒;与send差值${Date.now() - diffRef.current}毫秒`);
|
||||
const data = JSON.parse(event.data);
|
||||
const errorMsg = data.category === 'error' ? data.intermediate_steps : ''
|
||||
// 异常类型处理,提示
|
||||
@@ -145,13 +160,17 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
|
||||
handleWsMessage(data)
|
||||
// 群聊@自己时,开启input
|
||||
if (['end', 'end_cover'].includes(data.type) && data.receiver?.is_self) {
|
||||
setInputLock({ locked: true, reason: '' })
|
||||
setInputLock({ locked: false, reason: '' })
|
||||
setStop({ show: false, disable: false })
|
||||
continueRef.current = true
|
||||
}
|
||||
}
|
||||
ws.onclose = (event) => {
|
||||
wsRef.current = null
|
||||
console.error('链接手动断开 event :>> ', event);
|
||||
if ([1005, 1008].includes(event.code)) {
|
||||
setStop({ show: false, disable: false })
|
||||
|
||||
if ([1005, 1008, 1009].includes(event.code)) {
|
||||
console.warn('即将废弃 :>> ');
|
||||
setInputLock({ locked: true, reason: event.reason })
|
||||
} else {
|
||||
@@ -167,8 +186,8 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
|
||||
};
|
||||
ws.onerror = (ev) => {
|
||||
wsRef.current = null
|
||||
setStop({ show: false, disable: false })
|
||||
console.error('链接异常error', ev);
|
||||
setIsStop(true)
|
||||
toast({
|
||||
title: `${t('chat.networkError')}:`,
|
||||
variant: 'error',
|
||||
@@ -189,15 +208,14 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
|
||||
|
||||
// 接受 ws 消息
|
||||
const handleWsMessage = (data) => {
|
||||
// console.log(data)
|
||||
if (Array.isArray(data) && data.length) return
|
||||
if (data.type === "begin") {
|
||||
setIsStop(false)
|
||||
}else if (data.type === 'start') {
|
||||
if (data.type === 'start') {
|
||||
// 非continue时,展示stop按钮
|
||||
!continueRef.current && setStop({ show: true, disable: false })
|
||||
createWsMsg(data)
|
||||
} else if (data.type === 'stream') {
|
||||
//@ts-ignore
|
||||
updateCurrentMessage({
|
||||
flow_id: data.flow_id,
|
||||
chat_id: data.chat_id,
|
||||
message: data.message,
|
||||
thought: data.intermediate_steps
|
||||
@@ -209,16 +227,16 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
|
||||
thought: data.intermediate_steps || '',
|
||||
messageId: data.message_id,
|
||||
noAccess: false,
|
||||
liked: 0
|
||||
liked: 0,
|
||||
update_time: formatDate(new Date(), 'yyyy-MM-ddTHH:mm:ss')
|
||||
}, data.type === 'end_cover')
|
||||
} else if (data.type === "close") {
|
||||
setIsStop(true)
|
||||
setStop({ show: false, disable: false })
|
||||
setInputLock({ locked: false, reason: '' })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 监听重发消息事件
|
||||
// 触发发送消息事件(重试、表单)
|
||||
useEffect(() => {
|
||||
const handleCustomEvent = (e) => {
|
||||
if (!showWhenLocked && inputLock.locked) return console.error('弹窗已锁定,消息无法发送')
|
||||
@@ -247,12 +265,12 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
|
||||
// setInputEmpty(textarea.value.trim() === '')
|
||||
}
|
||||
|
||||
return <div className="absolute bottom-0 w-full bg-[#fff] dark:bg-[#000000]">
|
||||
<div className={`relative pt-[10px]`}>
|
||||
return <div className="absolute bottom-0 w-full pt-1 bg-[#fff] dark:bg-[#000]">
|
||||
<div className={`relative ${clear && 'pl-9'}`}>
|
||||
{/* form */}
|
||||
{
|
||||
formShow && <div className="relative">
|
||||
<div className="absolute left-0 bottom-2 bg-[#1a1a1a] px-4 py-2 rounded-md w-[50%] min-w-80">
|
||||
<div className="absolute left-0 border bottom-2 bg-background-login px-4 py-2 rounded-md w-[50%] min-w-80 z-50">
|
||||
{inputForm}
|
||||
</div>
|
||||
</div>
|
||||
@@ -265,34 +283,51 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
|
||||
onClick={handleClickGuideWord}
|
||||
/>
|
||||
{/* clear */}
|
||||
{/* <div className="flex absolute left-0 top-4 z-10">
|
||||
<div className="flex absolute left-0 top-4 z-10">
|
||||
{
|
||||
clear && <div
|
||||
className={`w-6 h-6 rounded-sm hover:bg-gray-200 cursor-pointer flex justify-center items-center `}
|
||||
onClick={() => { !inputLock.locked && destory() }}
|
||||
><ClearIcon className={!showWhenLocked && inputLock.locked ? 'text-gray-400' : 'text-gray-950'} ></ClearIcon></div>
|
||||
><ClearIcon className={`${!showWhenLocked && inputLock.locked ? 'text-gray-400' : 'text-gray-950'} dark:text-slate-50 dark:hover:bg-[#282828]`} ></ClearIcon></div>
|
||||
}
|
||||
</div> */}
|
||||
{/* form */}
|
||||
</div>
|
||||
{/* form switch */}
|
||||
<div className="flex absolute left-3 top-4 z-10">
|
||||
{
|
||||
form && <div
|
||||
className={`w-6 h-6 rounded-sm hover:bg-gray-200 cursor-pointer flex justify-center items-center `}
|
||||
onClick={() => (showWhenLocked || !inputLock.locked) && setFormShow(!formShow)}
|
||||
><FormIcon className={!showWhenLocked && inputLock.locked ? 'text-gray-400' : 'text-gray-950'}></FormIcon></div>
|
||||
><FormIcon className={!showWhenLocked && inputLock.locked ? 'text-gray-400' : 'text-gray-800'}></FormIcon></div>
|
||||
}
|
||||
</div>
|
||||
{/* send */}
|
||||
<div className="flex gap-2 absolute right-[2.5%] z-10">
|
||||
<div
|
||||
id="bs-send-btn"
|
||||
className="w-[68px] h-[40px] bg-[#FFD54C] cursor-pointer flex justify-center items-center"
|
||||
onClick={() => { !inputLock.locked && handleSendClick() }}
|
||||
style={{borderRadius:"20px"}}
|
||||
>
|
||||
{/* <SendIcon className={inputLock.locked ? 'text-gray-400' : 'text-gray-950'}></SendIcon> */}
|
||||
<img src={duihua_send} className="w-[20px]" alt="" />
|
||||
</div>
|
||||
{stop.show ?
|
||||
<div
|
||||
id="bs-send-btn"
|
||||
className="w-[68px] h-[40px] bg-[#FFD54C] cursor-pointer flex justify-center items-center"
|
||||
style={{borderRadius:"20px"}} onClick={() => {
|
||||
if (stop.disable) return
|
||||
setStop({ show: true, disable: true });
|
||||
sendWsMsg({ "action": "stop" });
|
||||
}}>
|
||||
{/* <SendIcon className={`${inputLock.locked ? 'text-muted-foreground' : 'text-foreground'}`} /> */}
|
||||
{/* <StopIcon className={`mt-1 rounded-sm bg-[#000000] cursor-pointer ${stop.disable && 'bg-muted-foreground text-muted-foreground'}`}
|
||||
onClick={() => {
|
||||
if (stop.disable) return
|
||||
setStop({ show: true, disable: true });
|
||||
sendWsMsg({ "action": "stop" });
|
||||
}} /> */}
|
||||
<div className="w-[16px] h-[16px] bg-[#000000]" style={{borderRadius:"3px"}}></div>
|
||||
</div>
|
||||
: <div
|
||||
id="bs-send-btn"
|
||||
className="w-[68px] h-[40px] bg-[#FFD54C] cursor-pointer flex justify-center items-center"
|
||||
onClick={() => { !inputLock.locked && handleSendClick() }} style={{borderRadius:"20px"}}>
|
||||
{/* <SendIcon className={`${inputLock.locked ? 'text-muted-foreground' : 'text-foreground'}`} /> */}
|
||||
<img src={duihua_send} className="w-[20px]" alt="" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{/* question */}
|
||||
<textarea
|
||||
@@ -303,8 +338,8 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
|
||||
disabled={inputLock.locked}
|
||||
onInput={handleTextAreaHeight}
|
||||
placeholder={inputLock.locked ? inputLock.reason : t('chat.inputPlaceholder')}
|
||||
// className={"resize-none py-4 pr-10 text-md min-h-6 max-h-[200px] scrollbar-hide dark:bg-[#2A2B2E] text-gray-800" + (form && ' pl-10')}
|
||||
className="questionTextarea w-full resize-none border-none bg-transparent outline-none max-h-[160px]"
|
||||
// className={"resize-none py-4 pr-10 text-md min-h-6 max-h-[200px] scrollbar-hide dark:bg-[#2A2B2E] text-gray-800" + (form && ' pl-10')}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
@@ -312,12 +347,7 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
|
||||
}
|
||||
}}
|
||||
></textarea>
|
||||
{!isStop && <div className=" absolute w-full flex justify-center bottom-32 pointer-events-none">
|
||||
<Button className="rounded-full pointer-events-auto" variant="outline" disabled={isStop} onClick={() => { setIsStop(true); stop(); }}><StopCircle className="mr-2" />Stop</Button>
|
||||
</div>}
|
||||
|
||||
{/* <p className="w-[100%] text-center text-[#666666]">内容由AI生成,仅供参考</p> */}
|
||||
</div>
|
||||
<p className="text-center text-sm pt-2 pb-4 text-gray-400">{appConfig.dialogTips}</p>
|
||||
</div>
|
||||
};
|
||||
};
|
||||
@@ -36,7 +36,9 @@ export default function FileBs({ data,flow_type }) {
|
||||
<div className="w-fit min-h-8 rounded-2xl px-6 py-4 max-w-[90%]">
|
||||
{data.sender && <p className="text-primary text-xs mb-2" style={{ background: avatarColor }}>{data.sender}</p>}
|
||||
<div className="flex gap-2 ">
|
||||
{data.flow_id && <TitleIconBg className="w-[40px] h-[40px]" img={data.avatar_img} id={data.avatar_color ? data.avatar_color : data.flow_id} ><img src={data.avatar_img ? data.avatar_img : (flow_type == "assistant" ? npcIcon : nengliIcon)} alt="" /></TitleIconBg>}
|
||||
{/* {data.flow_id && <TitleIconBg className="w-[40px] h-[40px]" img={data.avatar_img} id={data.avatar_color ? data.avatar_color : data.flow_id} ><img src={data.avatar_img ? data.avatar_img : (flow_type == "assistant" ? npcIcon : nengliIcon)} alt="" /></TitleIconBg>} */}
|
||||
{flow_type.id && <TitleIconBg className="w-[40px] h-[40px] mr-[10px]" img={flow_type.avatar_img} id={flow_type.avatar_color ? flow_type.avatar_color : flow_type.id} ><img src={flow_type.avatar_img ? flow_type.avatar_img : (flow_type.type == "assistant" ? npcIcon : nengliIcon)} alt="" /></TitleIconBg>}
|
||||
|
||||
{/* <div className="w-6 h-6 min-w-6 flex justify-center items-center rounded-full" style={{ background: avatarColor }} ><AvatarIcon /></div> */}
|
||||
<div
|
||||
className="flex gap-2 w-52 shadow-sm bg-[#1a1a1a] px-4 py-2 rounded-sm cursor-pointer"
|
||||
|
||||
@@ -85,7 +85,6 @@ export default function MessageBs({ data, onUnlike = () => { }, flow_type, onSou
|
||||
const handleCopyMessage = () => {
|
||||
copyText(messageRef.current)
|
||||
}
|
||||
// console.log(data)
|
||||
const chatId = useMessageStore(state => state.chatId)
|
||||
return <div className="flex w-full py-1">
|
||||
<div className="w-[100%]">
|
||||
@@ -94,7 +93,7 @@ export default function MessageBs({ data, onUnlike = () => { }, flow_type, onSou
|
||||
{/* {(data.flow_id == "06b1d374-ba97-46e6-8782-c56dec8dcc17" || data.flow_id == "ed8e21f6-9757-43d0-b076-8c6e81bb0580") && <img src={robot2} className="w-[50px]" alt=""/>}
|
||||
{data.flow_id == "ca214b41-2b73-4585-b172-bf1e546cf6ec" && <img src={robot3} className="w-[50px]" alt=""/>}
|
||||
{(data.flow_id != "06b1d374-ba97-46e6-8782-c56dec8dcc17" && data.flow_id != "ed8e21f6-9757-43d0-b076-8c6e81bb0580" && data.flow_id != "ca214b41-2b73-4585-b172-bf1e546cf6ec") && <img src={robot} className="w-[50px]" alt=""/>} */}
|
||||
{data.flow_id && <TitleIconBg className="w-[40px] h-[40px] mr-[10px]" img={flow_type.avatar_img} id={flow_type.avatar_color ? flow_type.avatar_color : data.flow_id} ><img src={flow_type.avatar_img ? flow_type.avatar_img : (flow_type.type == "assistant" ? npcIcon : nengliIcon)} alt="" /></TitleIconBg>}
|
||||
{flow_type && flow_type.id && <TitleIconBg className="w-[40px] h-[40px] mr-[10px]" img={flow_type.avatar_img} id={flow_type.avatar_color ? flow_type.avatar_color : flow_type.id} ><img src={flow_type.avatar_img ? flow_type.avatar_img : (flow_type.type == "assistant" ? npcIcon : nengliIcon)} alt="" /></TitleIconBg>}
|
||||
|
||||
<div ref={messageRef} className="text-sm max-w-[calc(100%-100px)]">
|
||||
{/* <div className="chat-start-zk relative">
|
||||
|
||||
@@ -10,7 +10,7 @@ import RunLog from "./RunLog";
|
||||
import Separator from "./Separator";
|
||||
import { useMessageStore } from "./messageStore";
|
||||
|
||||
export default function MessagePanne({ useName, guideWord, loadMore, flow_type }) {
|
||||
export default function MessagePanne({logo, useName, guideWord, loadMore, flow_type }) {
|
||||
const { t } = useTranslation()
|
||||
const { chatId, messages } = useMessageStore()
|
||||
|
||||
@@ -75,7 +75,6 @@ export default function MessagePanne({ useName, guideWord, loadMore, flow_type }
|
||||
} else if (msg.thought) {
|
||||
type = 'system'
|
||||
}
|
||||
// console.log(type)
|
||||
switch (type) {
|
||||
case 'user':
|
||||
return <MessageUser key={msg.id} useName={useName} data={msg} />;
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import ChatInput from "./ChatInput";
|
||||
import MessagePanne from "./MessagePanne";
|
||||
|
||||
export default function ChatComponent({ clear = false, questions = [], form = false, useName, inputForm = null, guideWord, wsUrl, onBeforSend, type, loadMore = () => { } }) {
|
||||
export default function ChatComponent({
|
||||
stop = false,
|
||||
logo = '',
|
||||
clear = false,
|
||||
questions = [],
|
||||
form = false,
|
||||
useName,
|
||||
inputForm = null,
|
||||
guideWord,
|
||||
wsUrl,
|
||||
onBeforSend,
|
||||
type,
|
||||
loadMore = () => { }
|
||||
}) {
|
||||
return <div className="relative h-full">
|
||||
<MessagePanne useName={useName} guideWord={guideWord} loadMore={loadMore} flow_type={type}></MessagePanne>
|
||||
<MessagePanne logo={logo} useName={useName} guideWord={guideWord} loadMore={loadMore} flow_type={type}></MessagePanne>
|
||||
<ChatInput clear={clear} questions={questions} form={form} wsUrl={wsUrl} inputForm={inputForm} onBeforSend={onBeforSend} ></ChatInput>
|
||||
</div>
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { MessageDB, getChatHistory } from '@/controllers/API'
|
||||
import { ChatMessageType } from '@/types/chat'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { create } from 'zustand'
|
||||
import { formatDate } from '@/util/utils';
|
||||
|
||||
/**
|
||||
* 会话消息管理
|
||||
@@ -19,6 +20,8 @@ type State = {
|
||||
/** 没有更多历史纪录 */
|
||||
historyEnd: boolean,
|
||||
messages: ChatMessageType[]
|
||||
/** 历史回话独立存储 */
|
||||
hisMessages: ChatMessageType[]
|
||||
/**
|
||||
* 控制引导问题的显示状态
|
||||
*/
|
||||
@@ -26,8 +29,8 @@ type State = {
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
loadHistoryMsg: (flowid: string, chatId: string, flow_type: string) => Promise<void>;
|
||||
loadMoreHistoryMsg: (flowid: string, flow_type: string) => Promise<void>;
|
||||
loadHistoryMsg: (flowid: string, chatId: string, data: { appendHistory: boolean, lastMsg: string }, flow_type: string) => Promise<void>;
|
||||
loadMoreHistoryMsg: (flowid: string, appendHistory: boolean, flow_type: string) => Promise<void>;
|
||||
destory: () => void;
|
||||
createSendMsg: (inputs: any, inputKey?: string) => void;
|
||||
createWsMsg: (data: any) => void;
|
||||
@@ -38,6 +41,7 @@ type Actions = {
|
||||
insetSystemMsg: (text: string) => void;
|
||||
insetBsMsg: (text: string) => void;
|
||||
setShowGuideQuestion: (text: boolean) => void;
|
||||
clearMsgs: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,18 +80,32 @@ export const useMessageStore = create<State & Actions>((set, get) => ({
|
||||
running: false,
|
||||
chatId: '',
|
||||
messages: [],
|
||||
hisMessages: [],
|
||||
historyEnd: false,
|
||||
showGuideQuestion: false,
|
||||
setShowGuideQuestion(bln: boolean) {
|
||||
set({ showGuideQuestion: bln })
|
||||
},
|
||||
async loadHistoryMsg(flowid, chatId, flow_type) {
|
||||
async loadHistoryMsg(flowid, chatId, { appendHistory, lastMsg }, flow_type) {
|
||||
const res = await getChatHistory(flowid, chatId, 30, 0, flow_type)
|
||||
const msgs = handleHistoryMsg(res)
|
||||
currentChatId = chatId
|
||||
set({ historyEnd: false, messages: msgs.reverse() })
|
||||
const hisMessages = appendHistory ? [] : msgs.reverse()
|
||||
if (hisMessages.length) {
|
||||
hisMessages.push({
|
||||
...bsMsgItem,
|
||||
id: Math.random() * 1000000,
|
||||
category: 'divider',
|
||||
message: lastMsg,
|
||||
})
|
||||
}
|
||||
set({
|
||||
historyEnd: false,
|
||||
messages: appendHistory ? msgs.reverse() : [],
|
||||
hisMessages
|
||||
})
|
||||
},
|
||||
async loadMoreHistoryMsg(flowid, flow_type) {
|
||||
async loadMoreHistoryMsg(flowid, appendHistory, flow_type) {
|
||||
if (get().running) return // 会话进行中禁止加载more历史
|
||||
if (get().historyEnd) return // 没有更多历史纪录
|
||||
const chatId = get().chatId
|
||||
@@ -101,11 +119,16 @@ export const useMessageStore = create<State & Actions>((set, get) => ({
|
||||
}
|
||||
const msgs = handleHistoryMsg(res)
|
||||
if (msgs.length) {
|
||||
set({ messages: [...msgs.reverse(), ...prevMsgs] })
|
||||
set({ [appendHistory ? 'messages' : 'hisMessages']: [...msgs.reverse(), ...prevMsgs] })
|
||||
} else {
|
||||
set({ historyEnd: true })
|
||||
}
|
||||
},
|
||||
clearMsgs() {
|
||||
setTimeout(() => {
|
||||
set({ hisMessages: [], messages: [], historyEnd: true })
|
||||
}, 0);
|
||||
},
|
||||
destory() {
|
||||
set({ chatId: '', messages: [] })
|
||||
},
|
||||
@@ -122,7 +145,8 @@ export const useMessageStore = create<State & Actions>((set, get) => ({
|
||||
category: '',
|
||||
files: [],
|
||||
end: false,
|
||||
user_name: ""
|
||||
user_name: "",
|
||||
update_time: formatDate(new Date(), 'yyyy-MM-ddTHH:mm:ss')
|
||||
}]
|
||||
}))
|
||||
},
|
||||
@@ -151,13 +175,20 @@ export const useMessageStore = create<State & Actions>((set, get) => ({
|
||||
// if (wsdata.end) {
|
||||
// debugger
|
||||
// }
|
||||
console.log('change updateCurrentMessage');
|
||||
// console.log('change updateCurrentMessage');
|
||||
const messages = get().messages
|
||||
const isRunLog = runLogsTypes.includes(wsdata.category);
|
||||
// run log类型存在嵌套情况,使用 extra 匹配 currentMessage; 否则取最近
|
||||
const currentMessageIndex = isRunLog ?
|
||||
messages.findLastIndex((msg) => msg.extra === wsdata.extra)
|
||||
: messages.findLastIndex((msg) => !runLogsTypes.includes(msg.category))
|
||||
let currentMessageIndex = 0
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (isRunLog && messages[i].extra === wsdata.extra) {
|
||||
currentMessageIndex = i;
|
||||
break;
|
||||
} else if (!isRunLog && !runLogsTypes.includes(messages[i].category)) {
|
||||
currentMessageIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const currentMessage = messages[currentMessageIndex]
|
||||
|
||||
const newCurrentMessage = {
|
||||
@@ -166,13 +197,17 @@ export const useMessageStore = create<State & Actions>((set, get) => ({
|
||||
id: isRunLog ? wsdata.extra : wsdata.messageId, // 每条消息必唯一
|
||||
message: isRunLog ? JSON.parse(wsdata.message) : currentMessage.message + wsdata.message,
|
||||
thought: currentMessage.thought + (wsdata.thought ? `${wsdata.thought}\n` : ''),
|
||||
files: wsdata.files || null,
|
||||
files: wsdata.files || [],
|
||||
category: wsdata.category || '',
|
||||
source: wsdata.source
|
||||
}
|
||||
// 无id补上(如文件解析完成消息,后端无返回messageid)
|
||||
if (!newCurrentMessage.id) {
|
||||
newCurrentMessage.id = Math.random() * 1000000
|
||||
// console.log('msg:', newCurrentMessage);
|
||||
}
|
||||
|
||||
messages[currentMessageIndex] = newCurrentMessage
|
||||
// console.log(messages,currentMessageIndex,newCurrentMessage)
|
||||
// 会话特殊处理,兼容后端的缺陷
|
||||
if (!isRunLog) {
|
||||
// start - end 之间没有内容删除load
|
||||
|
||||
24
src/components/bs-comp/loadMore/index.tsx
Normal file
24
src/components/bs-comp/loadMore/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function LoadMore({ onScrollLoad }) {
|
||||
// scroll load
|
||||
const footerRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(function () {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
onScrollLoad()
|
||||
}
|
||||
});
|
||||
}, {
|
||||
// root: null, // 视口
|
||||
rootMargin: '0px', // 视口的边距
|
||||
threshold: 0.1 // 目标元素超过视口的10%即触发回调
|
||||
});
|
||||
|
||||
observer.observe(footerRef.current);
|
||||
return () => footerRef.current && observer.unobserve(footerRef.current);
|
||||
}, [])
|
||||
|
||||
return <div ref={footerRef} style={{ height: 20 }}></div>
|
||||
};
|
||||
48
src/components/bs-comp/selectComponent/Users.tsx
Normal file
48
src/components/bs-comp/selectComponent/Users.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import MultiSelect from "@/components/bs-ui/select/multi";
|
||||
import { getUsersApi } from "@/controllers/API/user";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function UsersSelect({ multiple = false, lockedValues = [], value, disabled = false, onChange, children }:
|
||||
{ multiple?: boolean, lockedValues?: any[], value: any, disabled?: boolean, onChange: (a: any) => any, children?: (fun: any) => React.ReactNode }) {
|
||||
|
||||
const { t } = useTranslation()
|
||||
const [options, setOptions] = useState<any>([]);
|
||||
const originOptionsRef = useRef([])
|
||||
|
||||
const pageRef = useRef(1)
|
||||
const reload = (page, name) => {
|
||||
getUsersApi({ page, pageSize: 40, name }).then(res => {
|
||||
pageRef.current = page
|
||||
originOptionsRef.current = res.data
|
||||
const opts = res.data.map(el => ({ label: el.user_name, value: el.user_id }))
|
||||
setOptions(_ops => page > 1 ? [..._ops, ...opts] : opts)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload(1, '')
|
||||
}, [])
|
||||
|
||||
// 加载更多
|
||||
const loadMore = (name) => {
|
||||
reload(pageRef.current + 1, name)
|
||||
}
|
||||
|
||||
return <MultiSelect
|
||||
className=" max-w-[630px]"
|
||||
multiple={multiple}
|
||||
value={value}
|
||||
lockedValues={lockedValues}
|
||||
disabled={disabled}
|
||||
options={options}
|
||||
placeholder={'请选择用户'}
|
||||
searchPlaceholder={'搜索用户名称'}
|
||||
onChange={onChange}
|
||||
onLoad={() => reload(1, '')}
|
||||
onSearch={(val) => reload(1, val)}
|
||||
onScrollLoad={(val) => loadMore(val)}
|
||||
>
|
||||
{children?.(reload)}
|
||||
</MultiSelect>
|
||||
};
|
||||
51
src/components/bs-comp/selectComponent/knowledge.tsx
Normal file
51
src/components/bs-comp/selectComponent/knowledge.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import MultiSelect from "@/components/bs-ui/select/multi";
|
||||
import { readFileLibDatabase } from "@/controllers/API";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function KnowledgeSelect({ multiple = false, value, disabled = false, onChange, children }:
|
||||
{ multiple?: boolean, value: any, disabled?: boolean, onChange: (a: any) => any, children?: (fun: any) => React.ReactNode }) {
|
||||
|
||||
const { t } = useTranslation()
|
||||
const [options, setOptions] = useState<any>([]);
|
||||
const originOptionsRef = useRef([])
|
||||
|
||||
const pageRef = useRef(1)
|
||||
const reload = (page, name) => {
|
||||
readFileLibDatabase(page, 60, name).then(res => {
|
||||
pageRef.current = page
|
||||
originOptionsRef.current = res.data
|
||||
const opts = res.data.map(el => ({ label: el.name, value: el.id }))
|
||||
setOptions(_ops => page > 1 ? [..._ops, ...opts] : opts)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload(1, '')
|
||||
}, [])
|
||||
|
||||
// const handleChange = (res) => {
|
||||
// // id => obj
|
||||
// onChange(res.map(el => originOptionsRef.current.find(el2 => el2.id === el)))
|
||||
// }
|
||||
|
||||
// 加载更多
|
||||
const loadMore = (name) => {
|
||||
reload(pageRef.current + 1, name)
|
||||
}
|
||||
|
||||
return <MultiSelect
|
||||
multiple={multiple}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
options={options}
|
||||
placeholder={t('build.selectKnowledgeBase')}
|
||||
searchPlaceholder={t('build.searchBaseName')}
|
||||
onChange={onChange}
|
||||
onLoad={() => reload(1, '')}
|
||||
onSearch={(val) => reload(1, val)}
|
||||
onScrollLoad={(val) => loadMore(val)}
|
||||
>
|
||||
{children?.(reload)}
|
||||
</MultiSelect>
|
||||
};
|
||||
@@ -19,6 +19,8 @@ import zidingyi1 from "../../../assets/npc/zidingyi1.png";
|
||||
import zidingyi2 from "../../../assets/npc/zidingyi2.png";
|
||||
import npcIcon from "../../../assets/npc/npcIcon.png";
|
||||
import nengliIcon from "../../../assets/npc/nengliIcon.png";
|
||||
import { useDebounce } from "@/util/hook";
|
||||
import LoadMore from "../loadMore";
|
||||
|
||||
export default function SkillChatSheet({ children, onSelect }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
@@ -30,34 +32,45 @@ export default function SkillChatSheet({ children, onSelect }) {
|
||||
const [keyword, setKeyword] = useState(' ')
|
||||
const allDataRef = useRef([])
|
||||
|
||||
useEffect(() => {
|
||||
open && getChatOnlineApi().then(res => {
|
||||
allDataRef.current = res
|
||||
setKeyword('')
|
||||
const pageRef = useRef(1)
|
||||
const searchRef = useRef('')
|
||||
const [options, setOptions] = useState<any>([])
|
||||
|
||||
const loadData = (more = false) => {
|
||||
open && getChatOnlineApi(pageRef.current, searchRef.current).then(res => {
|
||||
setOptions(opts => more ? [...opts, ...res] : res)
|
||||
})
|
||||
}
|
||||
const debounceLoad = useDebounce(loadData, 600, false)
|
||||
|
||||
useEffect(() => {
|
||||
// open && getChatOnlineApi().then(res => {
|
||||
// allDataRef.current = res
|
||||
// setKeyword('')
|
||||
// })
|
||||
// setKeyword(' ')
|
||||
pageRef.current = 1
|
||||
searchRef.current = ''
|
||||
loadData()
|
||||
}, [open])
|
||||
|
||||
const options = useMemo(() => {
|
||||
return allDataRef.current.filter(el => el.name.toLowerCase().includes(keyword.toLowerCase()))
|
||||
}, [keyword])
|
||||
// const options = useMemo(() => {
|
||||
// return allDataRef.current.filter(el => el.name.toLowerCase().includes(keyword.toLowerCase()))
|
||||
// }, [keyword])
|
||||
|
||||
const handleSearch = (e) => {
|
||||
pageRef.current = 1
|
||||
searchRef.current = e.target.value
|
||||
debounceLoad()
|
||||
}
|
||||
|
||||
const handleLoadMore = () => {
|
||||
pageRef.current++
|
||||
loadData(true)
|
||||
}
|
||||
|
||||
const render = (item: any) => (
|
||||
<Flexbox align={'flex-start'} className={`selectNpcFlexbox relative`} onClick={() => { onSelect(item); setOpen(false) }}>
|
||||
{/* <Avatar size={24} src={item.favicon} style={{ flex: 'none' }} /> */}
|
||||
{/* <Flexbox>
|
||||
<div style={{ fontSize: 15, fontWeight: 600 }}>{item.name}</div>
|
||||
<div style={{ opacity: 0.6 }}>{item.name}</div>
|
||||
</Flexbox> */}
|
||||
{/* <Card key={item.id} className="w-[300px] overflow-hidden cursor-pointer" onClick={() => onSelect(item)}>
|
||||
<CardHeader>
|
||||
<CardTitle className=" flex items-center gap-2">
|
||||
<div className={"rounded-full w-[30px] h-[30px] " + gradients[parseInt(item.id, 16) % gradients.length]}></div>
|
||||
<span>{item.name}</span>
|
||||
</CardTitle>
|
||||
<CardDescription className="">{item.description}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card> */}
|
||||
<div className="npcInfoItemBg">
|
||||
<span>
|
||||
<span>
|
||||
@@ -68,16 +81,11 @@ export default function SkillChatSheet({ children, onSelect }) {
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{/* {(item.id == "06b1d374-ba97-46e6-8782-c56dec8dcc17" || item.id == "ed8e21f6-9757-43d0-b076-8c6e81bb0580") && <img src={robot2} className="w-[42px]" alt=""/>}
|
||||
{item.id == "ca214b41-2b73-4585-b172-bf1e546cf6ec" && <img src={robot3} className="w-[42px]" alt=""/>}
|
||||
{(item.id != "06b1d374-ba97-46e6-8782-c56dec8dcc17" && item.id != "ed8e21f6-9757-43d0-b076-8c6e81bb0580" && item.id != "ca214b41-2b73-4585-b172-bf1e546cf6ec") && <img src={robot} className="w-[42px]" alt=""/>} */}
|
||||
{/* <img src={robot} className="w-[42px]" alt=""/> */}
|
||||
<TitleIconBg className="w-[40px] h-[40px] min-w-[40px]" img={item.avatar_img} id={item.avatar_color ? item.avatar_color : item.id} ><img src={item.avatar_img ? item.avatar_img : (item.flow_type == "assistant" ? npcIcon : nengliIcon)} alt="" /></TitleIconBg>
|
||||
<div>
|
||||
<p>{item.name}</p>
|
||||
<div>
|
||||
{/* <div>绘画类</div>
|
||||
<div>绘画类</div> */}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,31 +108,12 @@ export default function SkillChatSheet({ children, onSelect }) {
|
||||
<div className="w-[280px] p-6">
|
||||
<SheetTitle>选择对话</SheetTitle>
|
||||
<SheetDescription className="text-[#999999]">选择一个您想使用的上线NPC或能力</SheetDescription>
|
||||
<SearchInput value={keyword} placeholder="搜索" className="my-6" onChange={(e) => setKeyword(e.target.value)} />
|
||||
{/* <SearchInput value={keyword} placeholder="搜索" className="my-6" onChange={(e) => setKeyword(e.target.value)} /> */}
|
||||
<SearchInput placeholder="搜索" className="my-6" onChange={handleSearch} />
|
||||
</div>
|
||||
<div className="w-[690px] overflow-y-auto bg-[#000000] scrollbar-hide skillSheet">
|
||||
{/* {
|
||||
options.length ? options.map((flow, i) => (
|
||||
<CardComponent key={i}
|
||||
id={i + 1}
|
||||
data={flow}
|
||||
title={flow.name}
|
||||
description={flow.desc}
|
||||
type="sheet"
|
||||
icon={flow.flow_type === 'flow' ? SkillIcon : AssistantIcon}
|
||||
footer={
|
||||
<Badge className={`absolute right-0 bottom-0 rounded-none rounded-br-md ${flow.flow_type === 'flow' && 'bg-gray-950'}`}>
|
||||
{flow.flow_type === 'flow' ? '技能' : 'NPC'}
|
||||
</Badge>
|
||||
}
|
||||
onClick={() => { onSelect(flow); setOpen(false) }}
|
||||
/>
|
||||
)) : <div className="flex flex-col items-center justify-center pt-40 w-full">
|
||||
<p className="text-sm text-muted-foreground mb-3">{t('build.empty')}</p>
|
||||
<Button className="w-[200px]" onClick={() => navigate('/build/assist')}>{t('build.onlineSA')}</Button>
|
||||
</div>
|
||||
} */}
|
||||
<SpotlightCard items={options} renderItem={render} className="mt-[14px] skillSheetSpotlightCard"/>
|
||||
<LoadMore onScrollLoad={handleLoadMore} />
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
|
||||
Reference in New Issue
Block a user