This commit is contained in:
zhangkai
2024-08-12 11:27:32 +08:00
parent 5199dbb6e5
commit e12f955105
166 changed files with 6288 additions and 991 deletions

BIN
src/components/bs-comp/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -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>
};
};

View File

@@ -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"

View File

@@ -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">

View File

@@ -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} />;

View File

@@ -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>
};

View File

@@ -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

View 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>
};

View 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>
};

View 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>
};

View File

@@ -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>