This commit is contained in:
zhangkai
2024-06-22 18:34:39 +08:00
parent fb39b66431
commit 1c07d4b9df
72 changed files with 4283 additions and 2158 deletions

BIN
src/.DS_Store vendored

Binary file not shown.

View File

@@ -191,9 +191,9 @@
}
path.react-flow__edge-interaction:hover {
stroke: #eebbbb;
stroke: #ffe999;
stroke-opacity: 1;
stroke-width: 12;
stroke-width: 10;
}
.selection-tool-box {

BIN
src/assets/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

BIN
src/assets/npc/border-r.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
src/assets/npc/jianhua.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
src/assets/npc/lingcun.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
src/assets/npc/tuichu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
src/assets/npc/xiaoxi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -60,8 +60,11 @@ export const gradients = [
// 'bg-fuchsia-700',
// 'bg-pink-600',
// 'bg-rose-600'
export function TitleIconBg({ id, className = '', children = <SkillIcon /> }) {
return <div className={cname(`flex justify-center items-center cursor-pointer ${gradients[parseInt(id + '', 16) % gradients.length]}`, className)} style={{borderRadius:"7px"}}>{children}</div>
export function TitleIconBg({ img, id, className = '', children = <SkillIcon /> }) {
if(img) return <div className={cname(`flex justify-center items-center cursor-pointer overflow-hidden`, className)} style={{borderRadius:"7px"}}>
{children}
</div>
return <div className={cname(`flex justify-center items-center cursor-pointer ${id!="" && gradients[parseInt(id + '', 16) % gradients.length]}`, className)} style={{borderRadius:"7px"}}>{children}</div>
}
export default function CardComponent<T>({

View File

@@ -232,11 +232,11 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
}
return <div className="absolute bottom-0 w-full bg-[#fff] dark:bg-[#000000]">
<div className={`relative`}>
<div className={`relative pt-[10px]`}>
{/* form */}
{
formShow && <div className="relative">
<div className="absolute left-0 border bottom-2 bg-[#fff] px-4 py-2 rounded-md w-[50%] min-w-80">
<div className="absolute left-0 bottom-2 bg-[#1a1a1a] px-4 py-2 rounded-md w-[50%] min-w-80">
{inputForm}
</div>
</div>
@@ -296,7 +296,7 @@ export default function ChatInput({ clear, form, questions, inputForm, wsUrl, on
}
}}
></textarea>
<p className="w-[100%] text-center text-[#666666]">AI生成</p>
{/* <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

@@ -11,6 +11,17 @@ import remarkMath from "remark-math";
import MessageButtons from "./MessageButtons";
import SourceEntry from "./SourceEntry";
import { useMessageStore } from "./messageStore";
import robot from "../../../assets/robot.png";
import robotU from "../../../assets/robotU.png";
import robot2 from "../../../assets/robot2.png";
import robot3 from "../../../assets/robot3.png";
import btnEdit from "../../../assets/chat/btn-edit.png";
import btnReSend from "../../../assets/chat/btn-reSend.png";
import btnDel from "../../../assets/chat/btn-del.png";
import npcIcon from "../../../assets/npc/npcIcon.png";
import nengliIcon from "../../../assets/npc/nengliIcon.png";
import { TitleIconBg } from "../cardComponent";
import Thumbs from "@/pages/ChatAppPage/components/Thumbs";
// 颜色列表
const colorList = [
@@ -76,27 +87,51 @@ export default function MessageBs({ data, onUnlike = () => { }, onSource }: { da
}
const chatId = useMessageStore(state => state.chatId)
console.log(data)
return <div className="flex w-full py-1">
<div className="w-fit max-w-[90%]">
{data.sender && <p className="text-gray-600 text-xs mb-2">{data.sender}</p>}
<div className="ml-[14px] min-h-8 px-6 py-4 rounded-2xl bg-[#13110D] dark:bg-[#13110D] text-[#fff]">
<div className="flex gap-2">
<div className="flex items-start avatarZk">
{/* {(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]" img={data.avatar_img} id={data.avatar_color ? data.avatar_color : data.flow_id} ><img src={data.avatar_img ? data.avatar_img : (data.flow_type == "assistant" ? npcIcon : nengliIcon)} alt="" /></TitleIconBg>}
<div ref={messageRef} className={`min-h-8 min-w-[110px] max-w-[50vw] ml-[10px] ${data.id && data.end && 'pb-8'}`}>
<div className="chat-start-zk relative">
{data.message.toString() ? mkdown : <span className="loading loading-ring loading-md"></span>}
{/* @user */}
{data.receiver && <p className="text-blue-500 text-sm">@ {data.receiver.user_name}</p>}
{/* 光标 */}
{data.message.toString() && !data.end && <div className="animate-cursor absolute w-2 h-5 ml-1 bg-gray-600" style={{ left: cursor.x, top: cursor.y }}></div>}
</div>
{/* 赞 踩 */}
{!!data.id && data.end && <Thumbs
id={data.id}
data={data.liked}
onCopy={handleCopyMessage}
onDislike={onUnlike}
className="chat-start-btn"
></Thumbs>
}
</div>
{/* <div className="flex gap-2">
<div className="w-6 h-6 min-w-6 flex justify-center items-center rounded-full" style={{ background: avatarColor }} ><AvatarIcon /></div>
{data.message.toString() ?
<div ref={messageRef} className="text-sm max-w-[calc(100%-24px)]">
{mkdown}
{mkdown} */}
{/* @user */}
{data.receiver && <p className="text-blue-500 text-sm">@ {data.receiver.user_name}</p>}
{/* {data.receiver && <p className="text-blue-500 text-sm">@ {data.receiver.user_name}</p>} */}
{/* 光标 */}
{/* {data.message.toString() && !data.end && <div className="animate-cursor absolute w-2 h-5 ml-1 bg-gray-600" style={{ left: cursor.x, top: cursor.y }}></div>} */}
</div>
{/* </div>
: <div><LoadIcon className="text-gray-400" /></div>
}
</div>
</div> */}
</div>
{/* 附加信息 */}
{
{/* {
!!data.id && data.end && <div className="flex justify-between mt-2">
<SourceEntry
extra={data.extra}
@@ -115,7 +150,7 @@ export default function MessageBs({ data, onUnlike = () => { }, onSource }: { da
onCopy={handleCopyMessage}
></MessageButtons>
</div>
}
} */}
</div>
</div>
};

View File

@@ -56,7 +56,7 @@ export default function MessagePanne({ useName, guideWord, loadMore }) {
return () => messagesRef.current?.removeEventListener('scroll', handleScroll)
}, [messagesRef.current, messages, chatId]);
return <div id="message-panne" ref={messagesRef} className="h-full overflow-y-auto scrollbar-hide pt-12 pb-60">
return <div id="message-panne" ref={messagesRef} className="h-full overflow-y-auto scrollbar-hide pt-[50px] pb-60 px-3">
{guideWord && <MessageBs
key={9999}
data={{ message: guideWord, isSend: false, chatKey: '', end: true, user_name: '' }} />}

View File

@@ -24,7 +24,7 @@ export default function MessageSystem({ data }) {
() => (
data.thought && <ReactMarkdown
linkTarget="_blank"
className="bs-mkdown text-gray-600 dark:text-[white] inline-block break-all max-w-full text-sm [&>pre]:text-wrap"
className="markdown text-gray-600 inline-block break-all max-w-full text-sm"
>
{data.thought.toString()}
</ReactMarkdown>
@@ -33,9 +33,10 @@ export default function MessageSystem({ data }) {
)
const border = { system: 'border-slate-500', question: 'border-amber-500', processing: 'border-cyan-600', answer: 'border-lime-600', report: 'border-slate-500', guide: 'border-none' }
const style = { system: 'style-system', question: 'style-question', processing: 'border-cyan-600', answer: 'style-answer', report: 'style-system' }
return <div className="py-1">
<div className={`relative rounded-sm px-6 py-4 border text-sm ${data.category === 'guide' ? 'bg-[#EDEFF6]' : 'bg-slate-50'} ${border[data.category || 'system']}`}>
<div className={`log rounded-xl whitespace-pre-wrap mt-[14px] relative ${style[data.category || 'system']}`}>
{logMkdown}
{data.category === 'report' && <CopyIcon className=" absolute right-4 top-2 cursor-pointer" onClick={(e) => handleCopy(e.target.parentNode)}></CopyIcon>}
</div>

View File

@@ -3,6 +3,9 @@ import { ChatMessageType } from "@/types/chat";
import { MagnifyingGlassIcon, Pencil2Icon, ReloadIcon } from "@radix-ui/react-icons";
import { useContext } from "react";
import { useMessageStore } from "./messageStore";
import robotU from "../../../assets/robotU.png";
import btnEdit from "../../../assets/chat/btn-edit.png";
import btnReSend from "../../../assets/chat/btn-reSend.png";
export default function MessageUser({ useName, data }: { data: ChatMessageType }) {
const msg = data.message[data.chatKey]
@@ -25,16 +28,33 @@ export default function MessageUser({ useName, data }: { data: ChatMessageType }
}
return <div className="flex justify-end w-full py-1">
<div className="w-fit min-h-8 max-w-[90%]">
<div className="flex items-start avatarZk">
<div className="mr-[10px]">
<div className="chat-end-zk">
{/* {chat.category === 'loading' && <span className="loading loading-spinner loading-xs mr-4 align-middle"></span>} */}
{msg}
</div>
<div className='chat-end-btn'>
{!running && <img src={btnEdit} onClick={() => handleResend(false)} className="w-[28px] cursor-pointer" alt=""/>}
{!running && <img src={btnReSend} onClick={() => handleResend(true)} className="w-[28px] cursor-pointer" alt=""/>}
{/* <img src={btnDel} className="w-[28px] cursor-pointer" alt=""/> */}
{/* {!showSearch && <Search size={18} className="cursor-pointer hover:text-blue-600 text-blue-400" onClick={() => onSearch(chat.message[chat.chatKey])}></Search>} */}
</div>
</div>
{/* <p className="mr-[20px] text-[14px]">{userName}</p> */}
<img src={robotU} className="w-[50px]" alt=""/>
</div>
{/* <div className="w-fit min-h-8 max-w-[90%]">
{useName && <p className="text-gray-600 text-xs mb-2 text-right">{useName}</p>}
<div className="mr-[14px] rounded-2xl px-6 py-4 bg-[#EEF2FF] dark:bg-[#333A48]">
<div className="flex gap-2 ">
<div className="text-[#0D1638] dark:text-[#CFD5E8] text-sm break-all whitespace-break-spaces">{msg}</div>
<div className="w-6 h-6 min-w-6"><img src="/user.png" alt="" /></div>
</div>
</div>
</div> */}
{/* 附加信息 */}
{
{/* {
// 数组类型的 data通常是文件上传消息不展示附加按钮
!Array.isArray(data.message.data) && <div className="flex justify-between mt-2 mr-[14px]">
<span></span>
@@ -45,6 +65,6 @@ export default function MessageUser({ useName, data }: { data: ChatMessageType }
</div>
</div>
}
</div>
</div> */}
</div>
};

View File

@@ -41,7 +41,7 @@ export default function RunLog({ data }) {
return <div className="py-1">
<div className="rounded-sm border">
<div className="flex justify-between items-center px-4 py-2 cursor-pointer" onClick={() => setOpen(!open)}>
<div className="flex items-center font-bold gap-2 text-sm">
<div className="flex items-center font-bold gap-2 text-sm text-[#fff]">
{
data.end ? <ToastIcon type={lost ? 'error' : 'success'} /> :
<LoadIcon className="text-primary duration-300" />
@@ -50,7 +50,7 @@ export default function RunLog({ data }) {
</div>
<CaretDownIcon className={open && 'rotate-180'} />
</div>
<div className={cname('bg-gray-100 px-4 py-2 text-gray-500 overflow-hidden text-sm ', open ? 'h-auto' : 'h-0 p-0')}>
<div className={cname('bg-[#0B1F26] px-4 py-2 text-[#ccc] overflow-hidden text-sm ', open ? 'h-auto' : 'h-0 p-0')}>
<p>{data.thought}</p>
</div>
</div>

View File

@@ -26,8 +26,8 @@ type State = {
}
type Actions = {
loadHistoryMsg: (flowid: string, chatId: string) => Promise<void>;
loadMoreHistoryMsg: (flowid: string) => Promise<void>;
loadHistoryMsg: (flowid: string, chatId: string, flow_type: string) => Promise<void>;
loadMoreHistoryMsg: (flowid: string, flow_type: string) => Promise<void>;
destory: () => void;
createSendMsg: (inputs: any, inputKey?: string) => void;
createWsMsg: (data: any) => void;
@@ -81,20 +81,21 @@ export const useMessageStore = create<State & Actions>((set, get) => ({
setShowGuideQuestion(bln: boolean) {
set({ showGuideQuestion: bln })
},
async loadHistoryMsg(flowid, chatId) {
const res = await getChatHistory(flowid, chatId, 30, 0)
async loadHistoryMsg(flowid, chatId, flow_type) {
console.log(111)
const res = await getChatHistory(flowid, chatId, 30, 0, flow_type)
const msgs = handleHistoryMsg(res)
currentChatId = chatId
set({ historyEnd: false, messages: msgs.reverse() })
},
async loadMoreHistoryMsg(flowid) {
async loadMoreHistoryMsg(flowid, flow_type) {
if (get().running) return // 会话进行中禁止加载more历史
if (get().historyEnd) return // 没有更多历史纪录
const chatId = get().chatId
const prevMsgs = get().messages
// 最后一条消息id不存在忽略 loadmore
if (!prevMsgs[0]?.id) return
const res = await getChatHistory(flowid, chatId, 10, prevMsgs[0]?.id || 0)
const res = await getChatHistory(flowid, chatId, 10, prevMsgs[0]?.id || 0, flow_type)
// 过滤非同一会话消息
if (res[0]?.chat_id !== currentChatId) {
return console.warn('loadMoreHistoryMsg chatId not match, ignore')

View File

@@ -5,10 +5,20 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { SearchInput } from "../../bs-ui/input";
import { Sheet, SheetContent, SheetDescription, SheetTitle, SheetTrigger } from "../../bs-ui/sheet";
import CardComponent from "../cardComponent";
import CardComponent, { TitleIconBg } from "../cardComponent";
import { SkillIcon } from "@/components/bs-icons/skill";
import { AssistantIcon } from "@/components/bs-icons/assistant";
import { useTranslation } from "react-i18next";
import borderR from "../../../assets/npc/border-r.png";
import { SpotlightCard } from "@lobehub/ui";
import { Flexbox } from 'react-layout-kit';
import robot from "../../../assets/robot.png";
import robot2 from "../../../assets/robot2.png";
import robot3 from "../../../assets/robot3.png";
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";
export default function SkillChatSheet({ children, onSelect }) {
const [open, setOpen] = useState(false)
@@ -32,19 +42,71 @@ export default function SkillChatSheet({ children, onSelect }) {
return allDataRef.current.filter(el => el.name.toLowerCase().includes(keyword.toLowerCase()))
}, [keyword])
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>
<div>
{/* <img src={robot} className="w-[160px]" alt=""/> */}
{(item.id == "06b1d374-ba97-46e6-8782-c56dec8dcc17" || item.id == "ed8e21f6-9757-43d0-b076-8c6e81bb0580") && <img src={robot2} className="w-[160px]" alt=""/>}
{item.id == "ca214b41-2b73-4585-b172-bf1e546cf6ec" && <img src={robot3} className="w-[160px]" 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-[160px]" alt=""/>}
</div>
</span>
</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>
<p className="mt-[10px] test-[13px]">{item.desc}</p>
<div className={`absolute right-0 top-0 w-[41px] h-[16px] flex justify-center items-center text-[9px] ${item.flow_type === 'flow' ? 'text-[#333333] bg-[#FFD54C]' : 'text-[#FFFFFF] bg-[#2586FF]'}`} style={{borderRadius:"0px 10px 0px 7px",fontWeight:"bold"}}>
{item.flow_type === 'flow' ? '能力' : 'NPC'}
</div>
</Flexbox>
);
return <Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
{children}
</SheetTrigger>
<SheetContent className="sm:min-w-[966px] bg-gray-100">
<SheetContent className="sm:min-w-[1000px] bg-[#1A1A1A]">
<div className="flex h-full" onClick={e => e.stopPropagation()}>
<div className="w-fit p-6">
<SheetTitle>{t('chat.dialogueSelection')}</SheetTitle>
<SheetDescription>{t('chat.chooseSkillOrAssistant')}</SheetDescription>
<SearchInput value={keyword} placeholder={t('chat.search')} className="my-6" onChange={(e) => setKeyword(e.target.value)} />
<div className="xinDuiHua-boxR">
{/* <img src={borderR} className="w-[30px] h-[100%]" alt="" /> */}
</div>
<div className="flex-1 min-w-[696px] bg-[#fff] p-5 pt-12 h-full flex flex-wrap gap-1.5 overflow-y-auto scrollbar-hide content-start">
{
<div className="w-fit p-6">
<SheetTitle></SheetTitle>
<SheetDescription className="text-[#999999]">使线NPC或能力</SheetDescription>
<SearchInput value={keyword} placeholder="搜索" className="my-6" onChange={(e) => setKeyword(e.target.value)} />
</div>
<div className="min-w-[696px] overflow-y-auto bg-[#000000] scrollbar-hide skillSheet">
{/* {
options.length ? options.map((flow, i) => (
<CardComponent key={i}
id={i + 1}
@@ -55,7 +117,7 @@ export default function SkillChatSheet({ children, onSelect }) {
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' ? '技能' : '助手'}
{flow.flow_type === 'flow' ? '技能' : 'NPC'}
</Badge>
}
onClick={() => { onSelect(flow); setOpen(false) }}
@@ -64,7 +126,8 @@ export default function SkillChatSheet({ children, onSelect }) {
<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"/>
</div>
</div>
</SheetContent>

View File

@@ -19,6 +19,7 @@ import robot2 from "../../../assets/robot2.png";
import robot3 from "../../../assets/robot3.png";
import zidingyi1 from "../../../assets/npc/zidingyi1.png";
import zidingyi2 from "../../../assets/npc/zidingyi2.png";
import borderR from "../../../assets/npc/border-r.png";
export default function SkillSheet({ select, children, onSelect }) {
const [keyword, setKeyword] = useState("");
@@ -107,9 +108,12 @@ export default function SkillSheet({ select, children, onSelect }) {
return (
<Sheet>
<SheetTrigger asChild>{children}</SheetTrigger>
<SheetContent className="bg-[#1A1A1A] sm:min-w-[966px]">
<SheetContent className="bg-[#1A1A1A] sm:min-w-[1000px]">
<div className="flex h-full" onClick={(e) => e.stopPropagation()}>
<div className="w-[270px] p-6">
<div className="xinDuiHua-boxR">
{/* <img src={borderR} className="w-[30px] h-[100%]" alt="" /> */}
</div>
<div className="p-6">
<SheetTitle>{t("build.addSkill")}</SheetTitle>
<SearchInput
value={keyword}

View File

@@ -11,6 +11,7 @@ import sousuo from "../../../assets/npc/sousuo.png";
import gongjuAdd from "../../../assets/npc/gongjuAdd.png";
import gongjuIcon from "../../../assets/npc/gongjuIcon.png";
import gongjuIcon1 from "../../../assets/npc/gongjuIcon1.png";
import borderR from "../../../assets/npc/border-r.png";
export default function ToolsSheet({ select, onSelect, children }) {
const { t } = useTranslation()
@@ -38,8 +39,11 @@ export default function ToolsSheet({ select, onSelect, children }) {
<SheetTrigger asChild>
{children}
</SheetTrigger>
<SheetContent className="w-[1000px] sm:max-w-[1000px] bg-[#121212]">
<SheetContent className="w-[1000px] sm:max-w-[1000px] bg-[#1a1a1a]">
<div className="flex h-full" onClick={e => e.stopPropagation()}>
<div className="xinDuiHua-boxR">
{/* <img src={borderR} className="w-[30px] h-[100%]" alt="" /> */}
</div>
<div className="w-fit p-6">
<SheetTitle>{t('build.addTool')}</SheetTitle>
<div className="relative mt-[14px]">
@@ -67,7 +71,7 @@ export default function ToolsSheet({ select, onSelect, children }) {
>
{/* <PersonIcon /> */}
{type === "default" ? <img src={gongjuIcon1} className="w-[14px]" alt="" /> : <img src={gongjuIcon} className="w-[14px]" alt="" />}
<span className="ml-[8px] text-[#999999]"></span>
<span className={`ml-[8px] text-[#999999] ${type === "default" && "text-[#FFD025]"}`}></span>
</div>
<div
className={`flex items-center gap-2 px-4 py-2 rounded-md cursor-pointer hover:bg-muted-foreground/10 transition-all duration-200 mt-1 ${type === 'custom' && 'bg-[#2A271D] text-[#FFD54C]'}`}
@@ -75,7 +79,7 @@ export default function ToolsSheet({ select, onSelect, children }) {
>
{type === "custom" ? <img src={gongjuIcon1} className="w-[14px]" alt="" /> : <img src={gongjuIcon} className="w-[14px]" alt="" />}
{/* <StarFilledIcon /> */}
<span className="ml-[8px] text-[#999999]"></span>
<span className={`ml-[8px] text-[#999999] ${type === "custom" && "text-[#FFD025]"}`}></span>
</div>
</div>
</div>

View File

@@ -14,14 +14,14 @@ export default function ActionButton({
...props
}) {
return <div className="flex items-center">
return <div className="flex items-center bg-[#1A1A1A]">
<>
{buttonTipContent ? <TooltipProvider>
<Tooltip delayDuration={delayDuration}>
<TooltipTrigger asChild>
<Button variant={variant} className={`rounded-r-none ${className}`} {...props}>{children}</Button>
</TooltipTrigger>
<TooltipContent className="bg-[#fff] text-gray-800">
<TooltipContent className="bg-[#1A1A1A] text-[#CCCCCC]">
{buttonTipContent}
</TooltipContent>
</Tooltip>
@@ -34,10 +34,11 @@ export default function ActionButton({
<Button
size="icon"
variant={variant}
className="rounded-l-none ml-[1px] [&[data-state=open]>svg]:rotate-180"
className=" ml-[1px] [&[data-state=open]>svg]:rotate-180 bg-[#262626] w-[18px] h-[18px] mr-[5px]"
style={{borderRadius:"3px"}}
><CaretDownIcon /></Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align={align}>
<PopoverContent className="w-80 p-0 bg-[#1A1A1A]" align={align}>
{dropDown}
</PopoverContent>
</Popover>

View File

@@ -115,7 +115,7 @@ const InputList = React.forwardRef<HTMLDivElement, InputProps & {
<Input
key={item.id}
defaultValue={item.value}
className={cname('pr-8', inputClassName)}
className={cname('pr-8 npcInput2', inputClassName)}
placeholder={props.placeholder || ''}
onChange={(e) => handleChange(e.target.value, item.id, index)}
onInput={(e) => {

View File

@@ -23,7 +23,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cname(
"group flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-[#1a1a1a] px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 data-[placeholder]:text-gray-400",
"group flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-[#1a1a1a] px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 data-[placeholder]:text-gray-400",
className
)}
{...props}

View File

@@ -40,7 +40,7 @@ const sheetVariants = cva(
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
"inset-y-0 right-0 h-full w-3/4 data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {

View File

@@ -99,7 +99,7 @@ const ToastTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cname("text-sm font-semibold text-[#fff] [&+div]:text-xs group-[.info]:text-[#fff] group-[.success]:text-[#fff] group-[.warning]:text-[#fff] group-[.error]:text-[#fff]", className)}
className={cname("text-sm font-semibold text-[#fff] [&+div]:text-xs group-[.info]:text-[#024FE5] group-[.success]:text-[#fff] group-[.warning]:text-[#EA991F] group-[.error]:text-[#D8341E]", className)}
{...props}
/>
))

View File

@@ -22,8 +22,8 @@ export const getAssistantsApi = async (page, limit, name): Promise<AssistantItem
};
// 创建助手
export const createAssistantsApi = async (name, prompt) => {
return await axios.post(`/api/v1/assistant`, { name, prompt, logo: '' })
export const createAssistantsApi = async (name, prompt, avatar_img, avatar_color) => {
return await axios.post(`/api/v1/assistant`, { name, prompt, logo: '', avatar_img, avatar_color })
};
// 获取助手详情

View File

@@ -203,7 +203,9 @@ export async function updateFlowApi(
name: updatedFlow.name,
data: updatedFlow.data,
description: updatedFlow.description,
guide_word: updatedFlow.guide_word
guide_word: updatedFlow.guide_word,
avatar_img: updatedFlow.avatar_img,
avatar_color: updatedFlow.avatar_color,
});
}

View File

@@ -107,9 +107,9 @@ export function updateTempApi(temp_id, data) {
* 获取知识库列表
*
*/
export async function readFileLibDatabase(page = 1, pageSize = 40, name = '', parentId = '',type = '') {
export async function readFileLibDatabase(page = 1, pageSize = 40, name = '') {
try {
const response: { data: any[], total: number } = await axios.get(`/api/v1/knowledge/?page_num=${page}&page_size=${pageSize}&name=${name}&parent_id=${parentId}&type=${type}`);
const response: { data: any[], total: number } = await axios.get(`/api/v1/knowledge/?page_num=${page}&page_size=${pageSize}&name=${name}`);
// const { data, total } = response
return response;
} catch (error) {
@@ -300,8 +300,38 @@ export const deleteChatApi = (chatId) => {
* @param id flow_id chat_id - .
* @returns {Promise<any>} his data.
*/
export async function getChatHistory(flowId: string, chatId: string, pageSize: number, id?: number): Promise<any[]> {
return await axios.get(`/api/v1/chat/history?flow_id=${flowId}&chat_id=${chatId}&page_size=${pageSize}&id=${id || ''}`);
export interface MessageDB {
/** 场景 */
category: string;
chat_id: string;
create_time: string;
extra: string;
/** 文件列表 */
files: string;
flow_id: string;
id: number;
/** 日志 */
intermediate_steps: string;
/** 机器人回复 */
is_bot: boolean;
/** 已点赞 */
liked: number;
/** 消息内容 */
message: string;
receiver: null;
remark: null;
sender: string;
solved: number;
/** 有溯源 */
source: number;
type: string;
update_time: string;
user_id: number;
flow_type: string;
}
export async function getChatHistory(flowId: string, chatId: string, pageSize: number, id?: number, flow_type: string): Promise<MessageDB[]> {
return await axios.get(`/api/v1/chat/history?flow_id=${flowId}&chat_id=${chatId}&page_size=${pageSize}&id=${id || ''}&flow_type=${flow_type}`);
}
/**
@@ -335,9 +365,11 @@ export async function getVersion() {
*
*/
export async function getBuildStatus(
flowId: string
flowId: string,
versionId?: number
): Promise<BuildStatusTypeAPI> {
return await axios.get(`/api/v1/build/${flowId}/status`);
const qstr = versionId ? `?version_id=${versionId}` : "";
return await axios.get(`/api/v1/build/${flowId}/status${qstr}`);
}
//docs for postbuildinit
@@ -347,11 +379,14 @@ export async function getBuildStatus(
* @returns {Promise<InitTypeAPI>} A promise that resolves to an AxiosResponse containing the build status.
*
*/
export async function postBuildInit(
flow: FlowType,
export async function postBuildInit(data: {
flow: FlowType
chatId?: string
): Promise<any> {
return await axios.post(`/api/v1/build/init/${flow.id}`, chatId ? { chat_id: chatId } : flow);
versionId?: number
}): Promise<any> {
const { flow, chatId, versionId } = data;
const qstr = versionId ? `?version_id=${versionId}` : ''
return await axios.post(`/api/v1/build/init/${flow.id}${qstr}`, chatId ? { chat_id: chatId } : flow);
}
// fetch(`/upload/${id}`, {

View File

@@ -1,7 +1,7 @@
import { toast } from "@/components/bs-ui/toast/use-toast";
import axios from "axios";
import i18next from "i18next";
axios.defaults.withCredentials = true;
const customAxios = axios.create({
// 配置
});
@@ -23,7 +23,12 @@ customAxios.interceptors.response.use(function (response) {
return Promise.reject(error);
}
// app 弹窗
window.errorAlerts([error.message])
toast({
title: `${i18next.t('prompt')}`,
variant: 'error',
description: error.message
})
// window.errorAlerts([error.message])
return Promise.reject(null);
})
@@ -38,7 +43,11 @@ export function captureAndAlertRequestErrorHoc(apiFunc, iocFunc?) {
console.log('error :>> ', error);
iocFunc?.(error)
// 弹窗
window.errorAlerts([error])
toast({
title: `${i18next.t('prompt')}`,
variant: 'error',
description: typeof error === 'string' ? error : JSON.stringify(error)
})
console.error('逻辑异常 :>> ', error);
return false
})

View File

@@ -29,7 +29,7 @@ export default function MainLayout() {
})
}
return <div className="flex">
<div className="bg-white h-screen w-[84px] border-r dark:shadow-slate-700 relative text-center">
<div className="bg-white h-screen w-[84px] dark:shadow-slate-700 relative text-center">
<Link className="inline-block mt-[21px]" to='/'><img src={login} className="w-[42px] h-[42px]" alt="" /></Link>
{/* <h1 className="text-white font-bold text-xl text-center">{t('title')}</h1> */}
<nav className="mt-8">
@@ -134,7 +134,7 @@ export default function MainLayout() {
</div>
</div>
</div>
<div className="flex-1 overflow-hidden">
<div className="flex-1 overflow-hidden xinDuiHua-box">
<ErrorBoundary
onReset={() => window.location.href = window.location.href}
FallbackComponent={CrashErrorComponent}

View File

@@ -77,21 +77,35 @@ export default function chatShare() {
setChatId(generateUUID(32))
})
}, [flowId])
// select chat
const handleSelectChat = useDebounce(async (chat) => {
if (chat.chat_id === chatId) return
// select chat
const handleSelectChat = useDebounce(async (chat) => {
if (chat.chat_id === chatId) return
const flow = initFlow?.id === chat.flow_id ? initFlow : await getFlowApi(chat.flow_id)
const flow = initFlow?.id === chat.flow_id ? initFlow : await getFlowApi(chat.flow_id)
// if (!flow) {
// setInputState({ lock: true, errorCode: '1004' })
// clearHistory()
// return setFace(false)
// }
// if (!flow) {
// setInputState({ lock: true, errorCode: '1004' })
// clearHistory()
// return setFace(false)
// }
setFlow(flow)
setChatId(chat.chat_id)
}, 100, false)
const wsUrl = useMemo(() => {
const params = [];
if (libId) params.push(`knowledge_id=${libId}`);
if (tweak) params.push(`tweak=${tweak}`);
const paramStr = params.length > 0 ? `${params.join('&')}` : '';
return `/api/v2/chat/ws/${flowId}?type=L1&${paramStr}`
}, [libId, tweak])
const [data] = useState<any>({ id: flowId, chatId: generateUUID(32), type: 'flow' })
setFlow(flow)
setChatId(chat.chat_id)
}, 100, false)
if (!flowId) return <div></div>
return <div className="chatShare">
@@ -127,13 +141,14 @@ export default function chatShare() {
</div>
</div> */}
{/* chat */}
{flow
{/* {flow
? <div className="flex-1 chat-box relative">
{flow && <ChatPanne version='v2' queryString={queryString} chatId={chatId} flow={flow} />}
</div>
:<div className="flex-1 chat-box h-screen overflow-hidden relative">
<p className="text-center mt-[100px] text-sm text-gray-600">{t('chat.selectChat')}</p>
</div>}
</div>} */}
<ChatPanne customWsHost={wsUrl} data={data} />
</div>
{/* {flow ? <ChatPanne version='v2' queryString={queryString} chatId={chatId} flow={flow} /> : null} */}
{/* 选择对话技能 */}

View File

@@ -235,23 +235,24 @@ export const ChatMessage = ({ chat, userName, disabledReSend, showSearch, onSour
// if (chat.isSend) return chat.files.length ? <>
// 发送消息
if (chat.isSend) return <div className="flex flex-col items-end">
<div className="flex items-center avatarZk">
<p className="mr-[20px] text-[14px]">{userName}</p>
<div className="flex items-start avatarZk">
<div className="mr-[10px]">
<div className="chat-end-zk">
{chat.category === 'loading' && <span className="loading loading-spinner loading-xs mr-4 align-middle"></span>}
{chat.message[chat.chatKey]}
</div>
<div className='chat-end-btn'>
{!disabledReSend && <img src={btnEdit} onClick={() => !disabledReSend && onEdit(chat.message[chat.chatKey])} className="w-[28px] cursor-pointer" alt=""/>}
{!disabledReSend && <img src={btnReSend} onClick={() => !disabledReSend && onReSend(chat.message[chat.chatKey])} className="w-[28px] cursor-pointer" alt=""/>}
{/* <img src={btnDel} className="w-[28px] cursor-pointer" alt=""/> */}
{/* {!showSearch && <Search size={18} className="cursor-pointer hover:text-blue-600 text-blue-400" onClick={() => onSearch(chat.message[chat.chatKey])}></Search>} */}
</div>
</div>
{/* <p className="mr-[20px] text-[14px]">{userName}</p> */}
<img src={robotU} className="w-[50px]" alt=""/>
</div>
<div className="mt-[10px] mr-[60px]">
<div className="chat-end-zk">
{chat.category === 'loading' && <span className="loading loading-spinner loading-xs mr-4 align-middle"></span>}
{chat.message[chat.chatKey]}
</div>
<div className='chat-end-btn'>
{!disabledReSend && <img src={btnEdit} onClick={() => !disabledReSend && onEdit(chat.message[chat.chatKey])} className="w-[28px] cursor-pointer" alt=""/>}
{!disabledReSend && <img src={btnReSend} onClick={() => !disabledReSend && onReSend(chat.message[chat.chatKey])} className="w-[28px] cursor-pointer" alt=""/>}
{/* <img src={btnDel} className="w-[28px] cursor-pointer" alt=""/> */}
{/* {!showSearch && <Search size={18} className="cursor-pointer hover:text-blue-600 text-blue-400" onClick={() => onSearch(chat.message[chat.chatKey])}></Search>} */}
</div>
</div>
</div>
{/* 文件 */ }
// <div className="chat chat-end">
@@ -275,11 +276,12 @@ export const ChatMessage = ({ chat, userName, disabledReSend, showSearch, onSour
{/* <div className="chat-image avatar">
<div className="w-[40px] h-[40px] rounded-full flex items-center justify-center" style={{ background: avatarColor }}><Bot color="#fff" size={28} /></div>
</div> */}
<div className="flex items-center avatarZk">
<div className="flex items-start avatarZk">
{(chat.flow_id == "06b1d374-ba97-46e6-8782-c56dec8dcc17" || chat.flow_id == "ed8e21f6-9757-43d0-b076-8c6e81bb0580") && <img src={robot2} className="w-[50px]" alt=""/>}
{chat.flow_id == "ca214b41-2b73-4585-b172-bf1e546cf6ec" && <img src={robot3} className="w-[50px]" alt=""/>}
{(chat.flow_id != "06b1d374-ba97-46e6-8782-c56dec8dcc17" && chat.flow_id != "ed8e21f6-9757-43d0-b076-8c6e81bb0580" && chat.flow_id != "ca214b41-2b73-4585-b172-bf1e546cf6ec") && <img src={robot} className="w-[50px]" alt=""/>}
<p className="ml-[20px] text-[14px]">{userName}</p>
</div>
{chat.sender && <div className="chat-header text-gray-400 text-sm">{chat.sender}</div>}
<Card className={`my-2 w-[200px] relative ${chat.files[0]?.file_url && 'cursor-pointer'}`} onClick={() => handleDownloadFile(chat.files[0])}>
@@ -295,33 +297,34 @@ export const ChatMessage = ({ chat, userName, disabledReSend, showSearch, onSour
{/* <div className="chat-image avatar">
<div className="w-[40px] h-[40px] rounded-full flex items-center justify-center" style={{ background: avatarColor }}><Bot color="#fff" size={28} /></div>
</div> */}
<div className="flex items-center avatarZk">
<div className="flex items-start avatarZk">
{(chat.flow_id == "06b1d374-ba97-46e6-8782-c56dec8dcc17" || chat.flow_id == "ed8e21f6-9757-43d0-b076-8c6e81bb0580") && <img src={robot2} className="w-[50px]" alt=""/>}
{chat.flow_id == "ca214b41-2b73-4585-b172-bf1e546cf6ec" && <img src={robot3} className="w-[50px]" alt=""/>}
{(chat.flow_id != "06b1d374-ba97-46e6-8782-c56dec8dcc17" && chat.flow_id != "ed8e21f6-9757-43d0-b076-8c6e81bb0580" && chat.flow_id != "ca214b41-2b73-4585-b172-bf1e546cf6ec") && <img src={robot} className="w-[50px]" alt=""/>}
<p className="ml-[20px] text-[14px]">{userName}</p>
{/* <p className="ml-[20px] text-[14px]">{userName}</p> */}
<div ref={textRef} className={`min-h-8 min-w-[110px] max-w-[50vw] ml-[10px] ${chat.id && chat.end && 'pb-8'}`}>
<div className="chat-start-zk relative">
{chat.message.toString() ? mkdown : <span className="loading loading-ring loading-md"></span>}
{/* @user */}
{chat.receiver && <p className="text-blue-500 text-sm">@ {chat.receiver.user_name}</p>}
{/* 光标 */}
{chat.message.toString() && !chat.end && <div className="animate-cursor absolute w-2 h-5 ml-1 bg-gray-600" style={{ left: cursor.x, top: cursor.y }}></div>}
</div>
{/* 赞 踩 */}
{!!chat.id && chat.end && <Thumbs
id={chat.id}
data={chat.liked}
onCopy={handleCopy}
onDislike={onDislike}
className="chat-start-btn"
></Thumbs>
// className={`absolute w-full left-0 bottom-[8px] justify-end pr-5`}></Thumbs>
}
</div>
</div>
{/* {chat.sender && <div className="chat-header text-gray-400 text-sm">{chat.sender}</div>} */}
<div ref={textRef} className={`min-h-8 min-w-[110px] max-w-[50vw] mt-[10px] ml-[60px] ${chat.id && chat.end && 'pb-8'}`}>
<div className="chat-start-zk relative">
{chat.message.toString() ? mkdown : <span className="loading loading-ring loading-md"></span>}
{/* @user */}
{chat.receiver && <p className="text-blue-500 text-sm">@ {chat.receiver.user_name}</p>}
{/* 光标 */}
{chat.message.toString() && !chat.end && <div className="animate-cursor absolute w-2 h-5 ml-1 bg-gray-600" style={{ left: cursor.x, top: cursor.y }}></div>}
</div>
{/* 赞 踩 */}
{!!chat.id && chat.end && <Thumbs
id={chat.id}
data={chat.liked}
onCopy={handleCopy}
onDislike={onDislike}
className="chat-start-btn"
></Thumbs>
// className={`absolute w-full left-0 bottom-[8px] justify-end pr-5`}></Thumbs>
}
</div>
{chat.source !== SourceType.NONE && chat.end && sourceContent(chat.source)}
</div>
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,948 @@
import cloneDeep from "lodash-es/cloneDeep";
import { ClipboardList, FileInput, FileText, Send, StopCircle } from "lucide-react";
import { forwardRef, useContext, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import ShadTooltip from "../../../components/ShadTooltipComponent";
import { Button } from "../../../components/ui/button";
import { alertContext } from "../../../contexts/alertContext";
import { TabsContext } from "../../../contexts/tabsContext";
import { getChatHistory, postBuildInit, postValidatePrompt } from "../../../controllers/API";
import { Variable } from "../../../controllers/API/flow";
import { sendAllProps } from "../../../types/api";
import { ChatMessageType } from "../../../types/chat";
import { FlowType, NodeType } from "../../../types/flow";
import { validateNode } from "../../../utils";
import { ChatMessage } from "./ChatMessage";
import ChatReportForm from "./ChatReportForm";
import ResouceModal from "./ResouceModal";
import ThumbsMessage from "./ThumbsMessage";
import NpcInfo from "./NpcInfo";
import Quote from "./quote";
import { locationContext } from "../../../contexts/locationContext";
import CompleteRecords from "./completeRecords";
import DataContent from "./dataContent";
import DialogueDetails from "./dialogueDetails";
import robot from "../../../assets/robot.png";
import robot2 from "../../../assets/robot2.png";
import robot3 from "../../../assets/robot3.png";
import duihuaGengduo from "../../../assets/chat/duihua-gengduo.png";
import duihuaSend from "../../../assets/chat/duihua-send.png";
interface Iprops {
chatId: string
flow: FlowType
queryString?: string
version?: string
}
export default forwardRef(function ChatPanne({ chatId, flow, queryString, version = 'v1' }: Iprops,ref) {
const { t } = useTranslation()
const { tabsState } = useContext(TabsContext);
const { isRoom, isForm, isReport, checkPrompt } = useFlowState(flow)
// build
const build = useBuild(flow, chatId)
// 消息列表
const { messages, messagesRef, loadHistory, setChatHistory, initGuide, changeHistoryByScroll } = useMessages(chatId, flow)
// ws通信
const { stop, connectWS, begin: chating, checkReLinkWs, sendAll } = useWebsocket(chatId, flow, setChatHistory, queryString, version)
// 停止状态
const [isStop, setIsStop] = useState(true)
// 输入框状态
const { inputState, inputEmpty, inputDisabled, inputRef,
formShow, setFormShow,
setInputState, setInputEmpty, handleTextAreaHeight } = useInputState({ flow, chatId, chating, messages, isForm, isReport })
const { appConfig } = useContext(locationContext)
// npc信息
const [isNpcInfo, setIsNpcInfo] = useState(false)
// 知识库引用信息
const [isQuote, setIsQuote] = useState(false)
// 完整对话记录
const [isCompleteRecords, setIsCompleteRecords] = useState(false)
// 数据内容
const [isDataContent, setIsDataContent] = useState(false)
// 对话详情
const [isDialogueDetails, setIsDialogueDetails] = useState(false)
// 开始构建&切换初始化会话
const initChat = async () => {
await checkPrompt(flow)
await build()
const historyData = version === 'v1' ? await loadHistory() : (initGuide(), [])
await connectWS({ setInputState, setIsStop, changeHistoryByScroll })
setInputState({ lock: false, errorMsg: '' });
// 第一条消息,用来初始化会话
sendAll({
chatHistory: messages,
name: flow.name,
description: flow.description,
inputs: {},
flow_id: flow.id,
chat_id: chatId
})
changeHistoryByScroll.current = false
// 自动聚焦
if (inputRef.current) inputRef.current.value = ''
setTimeout(() => {
inputRef.current?.focus()
}, 500);
const isNewChat = historyData.length === 0 || historyData[0].id === 9999
setFormShow(isNewChat && isForm)
}
useEffect(() => {
initChat()
}, [flow])
// sendmsg user name
const sendUserName = useMemo(() => {
const node = flow.data.nodes.find(el => el.data.type === 'AutoGenUser')
return node?.data.node.template['name'].value || ''
}, [flow])
const handleSend = async () => {
const msg = inputRef.current?.value
setTimeout(() => {
if (inputRef.current) {
inputRef.current.value = ''
inputRef.current.style.height = 'auto'
}
setInputEmpty(true)
}, 100);
if (msg.trim() === '') return
setInputState({ lock: true, errorMsg: '' });
let inputs = tabsState[flow.id].formKeysData.input_keys;
const input = inputs.find((el: any) => !el.type)
const inputKey = input ? Object.keys(input)[0] : '';
setChatHistory((old) => {
let newChat = cloneDeep(old);
newChat.push({
isSend: true,
message: { ...input, [inputKey]: msg },
chatKey: inputKey,
thought: '',
category: '',
files: [],
end: false,
user_name: ""
})
return newChat
});
await checkReLinkWs(async () => {
// await build()
await connectWS({ setInputState, setIsStop, changeHistoryByScroll })
})
const chatInfo = {
chat_id: chatId,
flow_id: flow.id,
inputs: { ...input, [inputKey]: msg }
}
// @ts-ignore
isRoom && chating ? sendAll({ action: "continue", ...chatInfo })
: sendAll({
chatHistory: messages,
name: flow.name,
description: flow.description,
...chatInfo
});
}
// 报表请求
const sendReport = (items: Variable[], str) => {
let inputs = tabsState[flow.id].formKeysData.input_keys;
const input = inputs.find((el: any) => !el.type)
const inputKey = input ? Object.keys(input)[0] : '';
setChatHistory((old) => {
let newChat = cloneDeep(old);
newChat.push({
isSend: true,
message: { ...input, [inputKey]: str },
chatKey: inputKey,
thought: '',
category: '',
files: [],
end: false,
user_name: ""
})
return newChat
});
const data = items.map(item => ({
id: item.nodeId,
name: item.name,
file_path: item.type === 'file' ? item.value : '',
value: item.type === 'file' ? '' : item.value
}))
setIsStop(false)
setFormShow(false)
sendAll({
inputs: {
...input,
[inputKey]: str,
data
},
chatHistory: messages,
name: flow.name,
description: flow.description,
chat_id: chatId,
flow_id: flow.id,
});
}
// 溯源
const [souce, setSouce] = useState<ChatMessageType>(null)
const thumbRef = useRef(null)
return <div className="overflow-hidden relative duihua-chat">
<div className="absolute duihua-chat-top">
<div>
{/* <img src={robot} alt=""/> */}
{(flow.id == "06b1d374-ba97-46e6-8782-c56dec8dcc17" || flow.id == "ed8e21f6-9757-43d0-b076-8c6e81bb0580") && <img src={robot2} alt=""/>}
{flow.id == "ca214b41-2b73-4585-b172-bf1e546cf6ec" && <img src={robot3} alt=""/>}
{(flow.id != "06b1d374-ba97-46e6-8782-c56dec8dcc17" && flow.id != "ed8e21f6-9757-43d0-b076-8c6e81bb0580" && flow.id != "ca214b41-2b73-4585-b172-bf1e546cf6ec") && <img src={robot} alt=""/>}
<p>{flow.name}</p>
{/* <div className="cursor-pointer">3条记录</div> */}
{/* <div className="cursor-pointer">模型列表</div> */}
</div>
<div className="cursor-pointer" onClick={() => setIsNpcInfo(!isNpcInfo)}>
{/* <img src={duihuaGengduo} alt=""/> */}
</div>
</div>
<div className="chata mt-[70px]" style={{ height: 'calc(100% - 170px)' }}>
{/* 会话记录 */}
<div ref={messagesRef} className={`chat-panne h-full overflow-y-scroll no-scrollbar px-[35px] ${isRoom || isReport ? 'pb-0' : 'pb-0'}`}>
{
messages.map((c, i) => <ChatMessage
key={c.id || i}
userName={sendUserName}
chat={c}
disabledReSend={inputDisabled}
showSearch={!!appConfig.dialogQuickSearch}
onSource={() => setSouce(c)}
onDislike={(chatId) => { thumbRef.current?.openModal(chatId) }}
onReSend={(msg) => {
inputRef.current.value = msg
handleSend()
}}
onEdit={(msg) => { inputRef.current.value = msg; setInputEmpty(!msg) }}
onSearch={(msg) => window.open(appConfig.dialogQuickSearch + encodeURIComponent(msg))}
></ChatMessage>)
}
</div>
{/* 输入框 */}
{version != "v3" && <div className="absolute w-full bottom-0 duihua-input-box pb-[40px]">
{/* <div className={`relative duihua-input
${inputDisabled && 'bg-gray-200 dark:bg-gray-600'}`}> */}
<div className={`relative duihua-input`}>
<textarea id='input'
ref={inputRef}
disabled={inputDisabled} rows={1}
className={`w-full resize-none border-none bg-transparent outline-none max-h-[160px]`}
placeholder={ (inputDisabled ? "当前处于回复中或不支持输入状态" : t('chat.inputPlaceholder'))}
onInput={handleTextAreaHeight}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) handleSend()
}}></textarea>
<div className="absolute right-0 bottom-0 w-[84px] duihua-input-btn cursor-pointer">
{/* <ShadTooltip content={t('chat.sendTooltip')}>
<button disabled={inputEmpty || inputDisabled} className=" disabled:text-gray-400" onClick={handleSend}><Send /></button>
</ShadTooltip> */}
<div className="duihua-input-btn-send">
<img src={duihuaSend} onClick={handleSend} alt=""/>
</div>
</div>
{inputState.errorMsg && <div className="absolute top-0 left-0 w-full h-full text-center align-middle pt-4" style={{background:"#2E1212",color:"#FF6060"}}>{inputState.errorMsg}</div>}
</div>
<p className="mb-2 text-center text-gray-400 text-sm">{appConfig.dialogTips}</p>
</div>}
</div>
{(isRoom || isReport) && <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>}
{/* 源文件类型 */}
<ResouceModal chatId={chatId} open={!!souce} data={souce} setOpen={() => setSouce(null)}></ResouceModal>
{/* 表单 */}
{isForm && formShow && <ChatReportForm flow={flow} onStart={sendReport} />}
{/* 踩 反馈 */}
<ThumbsMessage ref={thumbRef}></ThumbsMessage>
<NpcInfo isNpcInfo={isNpcInfo} setIsNpcInfo={setIsNpcInfo}></NpcInfo>
<Quote isQuote={isQuote} setIsQuote={setIsQuote}></Quote>
<CompleteRecords isCompleteRecords={isCompleteRecords} setIsCompleteRecords={setIsCompleteRecords} id={undefined}></CompleteRecords>
<DataContent isDataContent={isDataContent} setIsDataContent={setIsDataContent} id={undefined}></DataContent>
<DialogueDetails isDialogueDetails={isDialogueDetails} setIsDialogueDetails={setIsDialogueDetails}></DialogueDetails>
</div>
});
/**
* 输入框状态
* 分析 flow状态
* return 该技能含有表单、有报表、群聊
* @returns
*/
const useInputState = ({ flow, chatId, chating, messages, isForm, isReport }) => {
const { tabsState } = useContext(TabsContext);
const [inputState, setInputState] = useState({
lock: false,
errorMsg: ''
})
// 输入问答
const inputRef = useRef(null)
useEffect(() => {
!chating && setTimeout(() => {
// 对话结束自动聚焦
inputRef.current?.focus()
}, 1000);
}, [chating])
// input 滚动
const [inputEmpty, setInputEmpty] = useState(true)
useEffect(() => {
setInputEmpty(true)
if (inputRef.current) inputRef.current.value = ''
}, [chatId])
// 获取上传file input
const fileInputs = useMemo(() => {
return tabsState[flow.id]?.formKeysData?.input_keys?.filter((input: any) => input.type === 'file')
}, [tabsState, flow])
const handleTextAreaHeight = (e) => {
const textarea = e.target
textarea.style.height = 'auto'
textarea.style.height = textarea.scrollHeight + 'px'
setInputEmpty(textarea.value.trim() === '')
}
// input disabled
const inputDisabled = useMemo(() => {
return inputState.lock
// 表单 && 没回话或只有一个引导词
|| (isForm && (messages.length === 0 || (messages.length === 1 && messages[0].id === 9999)))
|| isReport
}, [inputState, fileInputs, isReport])
// 表单收起
const [formShow, setFormShow] = useState(true)
return {
inputState, inputEmpty, inputDisabled, inputRef,
formShow, setFormShow,
setInputState, setInputEmpty, handleTextAreaHeight
}
}
/**
* flow state
* 分析 flow状态
* return 该技能含有表单、有报表、群聊
* @returns
*/
const useFlowState = (flow: FlowType) => {
const flowSate = useMemo(() => {
// 是否群聊
const isRoom = !!flow.data?.nodes.find(node => node.data.type === "AutoGenChain")
// 是否展示表单
const isForm = !!flow.data?.nodes.find(node => ["VariableNode", "InputFileNode"].includes(node.data.type))
// 是否报表
const isReport = !!flow.data?.nodes.find(node => "Report" === node.data.type)
return { isRoom, isForm, isReport }
}, [flow])
// propmt类型补充自定义字段
const checkPrompt = async (_flow) => {
const params = _flow.data.nodes.map(node => {
const temps = []
const temp = node.data.node.template
Object.keys(temp).map(key => {
const { type, value } = temp[key]
if (type === 'prompt' && !!value) !temps.length && temps.push({ name: key, template: value, data: node.data })
})
return temps
}).flat()
const promises = params.map(param => {
return postValidatePrompt(param.name, param.template, param.data.node).then(res => {
if (res) param.data.node = res.frontend_node
})
})
return Promise.all(promises)
}
return { ...flowSate, checkPrompt }
}
/**
* 消息列表模块
* 翻页、追加、历史
* @returns
*/
const useMessages = (chatId, flow) => {
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
const lastIdRef = useRef(0)
// 控制开启自动随消息滚动(临时方案)
const changeHistoryByScroll = useRef(false)
const loadIdRef = useRef('') // 记录最后一个加载的 chatId
// 获取聊天记录
const loadHistory = async (lastId?: number) => {
loadIdRef.current = chatId
const res = await getChatHistory(flow.id, chatId, lastId ? 10 : 30, lastId)
const hisData = res.map(item => {
// let count = 0
let { message, files, is_bot, intermediate_steps, ...other } = item
try {
message = message && message[0] === '{' ? JSON.parse(message.replace(/([\t\n"])/g, '\\$1').replace(/'/g, '"')) : message || ''
} catch (e) {
// 未考虑的情况暂不处理
}
return {
...other,
chatKey: typeof message === 'string' ? undefined : Object.keys(message)[0],
end: true,
files: files ? JSON.parse(files) : [],
isSend: !is_bot,
message,
thought: intermediate_steps,
noAccess: true
}
})
lastIdRef.current = hisData[hisData.length - 1]?.id || lastIdRef.current || 0 // 记录最后一个id
let historyData = []
if (lastId) {
historyData = [...hisData.reverse(), ...chatHistory]
} else if (loadIdRef.current === chatId) { // 保证同一会话
historyData = hisData.reverse()
}
setChatHistory(historyData)
const pageSize = historyData.length < 30 ? 30 : 10 // 先偷懒
if (hisData.length < pageSize) initGuide()
return historyData
}
const loadLock = useRef(false)
const currentIdRef = useRef(0)
const loadNextPage = async () => {
if (loadLock.current) return
if (currentIdRef.current === lastIdRef.current) return // 最后一个相同表示聊天记录已到顶
loadLock.current = true
currentIdRef.current = lastIdRef.current
changeHistoryByScroll.current = true
await loadHistory(currentIdRef.current)
loadLock.current = false
// 滚动 hack TODO 滚动翻页设计
setTimeout(() => {
changeHistoryByScroll.current = false
}, 500);
}
const initGuide = () => {
const guideMsg = {
"category": "system",
"chat_id": chatId,
"end": true,
"create_time": "",
"extra": "{}",
"files": [],
"flow_id": flow.id,
"id": 9999,
"thought": flow.guide_word,
"is_bot": true,
"liked": 0,
"message": '',
"receiver": null,
"remark": null,
"sender": "",
"solved": 0,
isSend: false,
"source": 0,
"type": "end",
"update_time": "",
noAccess: true,
"user_id": 0
}
flow.guide_word && setChatHistory((chatHistory) =>
chatHistory[0]?.id === 9999 ? chatHistory : [guideMsg, ...chatHistory]
)
}
// 消息滚动
const messagesRef = useRef(null);
useEffect(() => {
if (messagesRef.current) { // 滚动加载不触发
// if (messagesRef.current && !changeHistoryByScroll.current) { // 滚动加载不触发
console.log(1,messagesRef,changeHistoryByScroll)
setTimeout(() => {
messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
}, 100);
}
}, [chatHistory, changeHistoryByScroll]);
// 消息滚动加载
useEffect(() => {
function handleScroll() {
if (messagesRef.current.scrollTop <= 30) {
loadNextPage()
}
}
messagesRef.current?.addEventListener('scroll', handleScroll);
return () => messagesRef.current?.removeEventListener('scroll', handleScroll)
}, [messagesRef.current, chatHistory, chatId]);
return {
messages: chatHistory, messagesRef, loadHistory, setChatHistory, initGuide, changeHistoryByScroll
}
}
/**
* websocket 通信
* 建立连接、重连、断开、接收、发送
* @returns
*/
const useWebsocket = (chatId, flow, setChatHistory, queryString, version) => {
const ws = useRef<WebSocket | null>(null);
// 接收ws状态
const [begin, setBegin] = useState(false)
const { setErrorData } = useContext(alertContext);
const { t } = useTranslation()
const { appConfig } = useContext(locationContext)
const chatIdRef = useRef(chatId);
useEffect(() => {
chatIdRef.current = chatId;
}, [chatId])
function heartbeat() {
if (!ws.current) return;
if (ws.current.readyState !== 1) return;
ws.current.send("heartbeat");
setTimeout(heartbeat, 30000);
}
function getWebSocketUrl(flowId, isDevelopment = false) {
const token = localStorage.getItem("ws_token") || '';
const isSecureProtocol = window.location.protocol === "https:";
const webSocketProtocol = isSecureProtocol ? "wss" : "ws";
const host = appConfig.websocketHost || window.location.host // isDevelopment ? "localhost:7860" : window.location.host;
const chatEndpoint = version === 'v1' ? `/api/v1/chat/${flowId}?type=L1&chat_id=${chatId}&t=${token}`
: `/api/v2/chat/ws/${flowId}?type=L1&chat_id=${chatId}${queryString}&t=${token}`
return `${webSocketProtocol}://${host}${chatEndpoint}`;
}
const newChatStart = useRef(false) // 处理当前会话上下文丢失,阻止上一次打字机效果
// 自动重连次数
const tryReLinkCount = useRef(0)
const reConnect = (params) => {
if (tryReLinkCount.current <= 3) {
connectWS(params)
tryReLinkCount.current++
} else {
console.warn('超过最大重试次数 :>> ');
}
}
useEffect(() => {
tryReLinkCount.current = 0
newChatStart.current = true
}, [chatId])
function connectWS(params) {
const { setInputState, setIsStop, changeHistoryByScroll } = params
if (ws.current) return Promise.resolve('ok');
// 连接断开重链接
return new Promise((res, rej) => {
try {
const urlWs = getWebSocketUrl(
flow.id,
process.env.NODE_ENV === "development"
);
const newWs = new WebSocket(urlWs);
newWs.onopen = () => {
console.log("WebSocket connection established!");
res('ok')
// heartbeat()
};
newWs.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.chat_id !== chatIdRef.current) return
console.log('newChatStart.current :>> ', newChatStart.current);
const errorMsg = data.category === 'error' ? data.intermediate_steps : ''
if (newChatStart.current) {
if (data.type === 'close') {
newChatStart.current = false
return setInputState({ lock: false, errorMsg })
} else {
return setInputState({ lock: true, errorMsg })
}
}
// 异常类型处理,提示
if (errorMsg) return setInputState({ lock: true, errorMsg })
handleWsMessage({ data, setIsStop, setInputState, changeHistoryByScroll });
// get chat history
// 群聊@自己时开启input
if (data.type === 'end' && data.receiver?.is_self) {
setInputState({ lock: false, errorMsg: '' })
}
};
newWs.onclose = (event) => {
ws.current = null
handleOnClose({ event, setIsStop, setInputState });
// reConnect(params)
};
newWs.onerror = (ev) => {
ws.current = null
console.error('链接异常error', ev);
setIsStop(true)
setErrorData({
title: `${t('chat.networkError')}:`,
list: [
t('chat.networkErrorList1'),
t('chat.networkErrorList2'),
t('chat.networkErrorList3')
],
});
reConnect(params)
};
ws.current = newWs;
console.log('newWs :>> ', newWs);
} catch (error) {
console.error('创建链接异常', error);
rej(error)
}
})
}
var isStream = false;
function handleWsMessage({ data, setIsStop, setInputState, changeHistoryByScroll }) {
if (Array.isArray(data) && data.length) return
if (data.type === "begin") {
setBegin(true)
setIsStop(false)
changeHistoryByScroll.current = false
}
if (data.type === "close") {
setBegin(false)
setIsStop(true)
setInputState({ lock: false, errorMsg: '' });
changeHistoryByScroll.current = true
}
if (data.type === "start") {
setChatHistory((old) => {
let newChat = cloneDeep(old);
newChat.push({
isSend: false,
message: '',
chatKey: '',
thought: data.intermediate_steps || '',
category: data.category || '',
files: [],
end: false
})
return newChat
});
isStream = true;
}
if (data.type === "stream" && isStream) {
updateLastMessage({ str: data.message, thought: data.intermediate_steps });
}
if (data.type === "end") {
updateLastMessage({
...data,
str: data.message,
files: data.files || null,
end: true,
thought: data.intermediate_steps || '',
cate: data.category || '',
messageId: data.message_id,
noAccess: false,
liked: 0
});
isStream = false;
}
}
function updateLastMessage({ str, thought = '', end = false, files = [], cate = '', messageId = 0, source = false, noAccess = false, ...data }: {
str: string;
messageId?: number
thought?: string;
cate?: string;
end?: boolean;
files?: Array<any>;
source?: boolean
noAccess?: boolean
}) {
setChatHistory((old) => {
const newChats = [...old]
// console.log('newchats :>> ', newChats);
let chatsLen = newChats.length
const prevChat = newChats[chatsLen - 2]
// hack 过滤重复最后消息
if (end
&& str
&& chatsLen > 1
&& str === prevChat.message
// && data.sender === prevChat.sender
&& !prevChat.thought) {
newChats.splice(chatsLen - 2, 1) // 删上一条
chatsLen = newChats.length
}
// 更新
const lastChat = newChats[chatsLen - 1]
const newLastChat = {
...newChats[chatsLen - 1],
...data,
id: messageId,
message: lastChat.message + str,
thought: lastChat.thought + (thought ? `${thought}\n` : ''),
files,
category: cate,
source,
noAccess,
end
}
newChats[chatsLen - 1] = newLastChat
// start - end 之间没有内容删除load
if (end && !(newLastChat.files.length || newLastChat.thought || newLastChat.message)) {
newChats.pop()
}
return newChats;
});
}
// 发送ws
async function sendAll(data: sendAllProps) {
try {
if (ws.current) {
if (JSON.stringify(data.inputs) !== '{}') {
newChatStart.current = false
}
ws.current.send(JSON.stringify(data));
}
} catch (error) {
setErrorData({
title: "There was an error sending the message",
list: [error.message],
});
}
}
// 处理主动断开
function handleOnClose({ event, setIsStop, setInputState }) {
console.error('链接手动断开 event :>> ', event);
setIsStop(true)
setBegin(false)
if ([1005, 1008].includes(event.code)) {
console.warn('即将废弃 :>> ');
setInputState({ lock: true, errorMsg: event.reason });
} else {
if (event.reason) {
setErrorData({ title: event.reason });
// setChatHistory((old) => {
// let newChat = cloneDeep(old);
// if (newChat.length) {
// newChat[newChat.length - 1].end = true;
// }
// newChat.push({ end: true, message: `${t('chat.connectionbreakTip')}${event.reason}`, isSend: false, chatKey: '', files: [] });
// return newChat
// })
}
setInputState({ lock: false, errorMsg: '' });
}
}
useEffect(() => {
// destory
return () => {
// close prev connection
if (ws.current) {
switch (ws.current.readyState) {
case WebSocket.OPEN:
console.warn('前端主动关闭1')
ws.current.close()
; break;
case WebSocket.CONNECTING:
ws.current.onopen = () => {
console.warn('前端主动关闭2')
ws.current.close()
};
}
ws.current = null
}
}
}, [])
// 检测并重连
const checkReLinkWs = async (reConnect) => {
if (ws.current) return true
// 重连
// 上一条加loading
setChatHistory((old) => {
let newChat = [...old];
newChat[newChat.length - 1].category = 'loading';
return newChat;
});
await reConnect()
// 链接成功
// 上一条去loading
setChatHistory((old) => {
let newChat = [...old];
newChat[newChat.length - 1].category = '';
return newChat;
});
}
const handleStop = () => {
try {
if (ws) {
ws.current.send(JSON.stringify({
"action": "stop"
}));
}
} catch (error) {
setErrorData({
title: "There was an error stop the message",
list: [error.message],
});
}
}
return { begin, stop: handleStop, checkReLinkWs, sendAll, connectWS }
}
/**
* build flow
* 校验每个节点展示进度及结果返回input_keys;end_of_stream断开链接
* 主要校验节点并设置更新setTabsState的 formKeysData
* @returns
*/
const useBuild = (flow: FlowType, chatId: string) => {
const { setErrorData } = useContext(alertContext);
const { setTabsState } = useContext(TabsContext);
const { t } = useTranslation()
// SSE 服务端推送
async function streamNodeData(flow: FlowType, chatId: string) {
// Step 1: Make a POST request to send the flow data and receive a unique session ID
const { flowId } = await postBuildInit(flow, chatId);
// Step 2: Use the session ID to establish an SSE connection using EventSource
let validationResults = [];
let finished = false;
let buildEnd = false
const apiUrl = `/api/v1/build/stream/${flowId}?chat_id=${chatId}`;
const eventSource = new EventSource(apiUrl);
eventSource.onmessage = (event) => {
// If the event is parseable, return
if (!event.data) {
return;
}
const parsedData = JSON.parse(event.data);
// if the event is the end of the stream, close the connection
if (parsedData.end_of_stream) {
eventSource.close(); // 结束关闭链接
buildEnd = true
return;
} else if (parsedData.log) {
// If the event is a log, log it
// setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys) {
setTabsState((old) => {
return {
...old,
[flowId]: {
...old[flowId],
formKeysData: parsedData,
},
};
});
} else {
// setProgress(parsedData.progress);
validationResults.push(parsedData.valid);
}
};
eventSource.onerror = (error: any) => {
console.error("EventSource failed:", error);
eventSource.close();
if (error.data) {
const parsedData = JSON.parse(error.data);
setErrorData({ title: parsedData.error });
}
};
// Step 3: Wait for the stream to finish
while (!finished) {
await new Promise((resolve) => setTimeout(resolve, 100));
finished = buildEnd // validationResults.length === flow.data.nodes.length;
}
// Step 4: Return true if all nodes are valid, false otherwise
return validationResults.every((result) => result);
}
// 延时器
async function enforceMinimumLoadingTime(
startTime: number,
minimumLoadingTime: number
) {
const elapsedTime = Date.now() - startTime;
const remainingTime = minimumLoadingTime - elapsedTime;
if (remainingTime > 0) {
return new Promise((resolve) => setTimeout(resolve, remainingTime));
}
}
async function handleBuild() {
try {
const errors = flow.data.nodes.flatMap((n: NodeType) => validateNode(n, flow.data.edges))
if (errors.length > 0) {
setErrorData({
title: t('chat.buildError'),
list: errors,
});
return;
}
const minimumLoadingTime = 200; // in milliseconds
const startTime = Date.now();
await streamNodeData(flow, chatId);
await enforceMinimumLoadingTime(startTime, minimumLoadingTime); // 至少等200ms, 再继续(强制最小load时间)
} catch (error) {
console.error("Error:", error);
} finally {
}
}
return handleBuild
}

View File

@@ -1,6 +1,6 @@
import { useContext, useEffect, useRef, useState } from "react";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/bs-ui/select";
import { forwardRef, useContext, useEffect, useImperativeHandle, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Dropdown from "../../../components/dropdownComponent";
import InputComponent from "../../../components/inputComponent";
import InputFileComponent from "../../../components/inputFileComponent";
import { Button } from "../../../components/ui/button";
@@ -12,14 +12,23 @@ import { Variable, VariableType, getVariablesApi } from "../../../controllers/AP
* @description
* 表单项数据由组件的参数信息和单独接口获取的必填信息及排序信息而来。
*/
export default function ChatReportForm({ flow, onStart }) {
const ChatReportForm = forwardRef(({ type = 'chat', vid = 0, flow, onStart }, ref) => {
const { setErrorData } = useContext(alertContext);
const { t } = useTranslation()
useImperativeHandle(ref, () => ({
submit: () => {
handleStart()
}
}));
// 从 api中获取
const [items, setItems] = useState<Variable[]>([])
useEffect(() => {
getVariablesApi({ flow_id: flow.id }).then(
// chat -》L1 diff -> 对比测试
type === 'chat' ? getVariablesApi({ flow_id: flow.flow_id || flow.id }).then(
res => setItems(res)
) : getVariablesApi({ version_id: vid, flow_id: flow.flow_id || flow.id }).then(
res => setItems(res)
)
}, [])
@@ -56,9 +65,9 @@ export default function ChatReportForm({ flow, onStart }) {
onStart(obj, str)
}
return <div className="absolute right-20 bottom-32 w-[90%] max-w-[680px] flex flex-col gap-6 rounded-xl p-4 md:p-6 bg-[#1A1A1A]">
return <div className="flex flex-col gap-6 rounded-xl p-4 ">
<div className="max-h-[520px] overflow-y-auto">
{items.map((item, i) => <div key={item.id} className="w-full text-sm" style={{color:"#fff"}}>
{items.map((item, i) => <div key={item.id} className="w-full text-sm">
{item.name}
<span className="text-status-red">{item.required ? " *" : ""}</span>
<div className="mt-2">
@@ -68,11 +77,18 @@ export default function ChatReportForm({ flow, onStart }) {
onChange={(val) => handleChange(i, val)}
/> :
item.type === VariableType.Select ?
<Dropdown
options={item.options.map(e => e.value)}
onSelect={(val) => handleChange(i, val)}
value={item.value}
></Dropdown> :
<Select onValueChange={(val) => handleChange(i, val)}>
<SelectTrigger>
<SelectValue placeholder="" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{
item.options.map(el => <SelectItem key={el.value} value={el.value}>{el.value}</SelectItem>)
}
</SelectGroup>
</SelectContent>
</Select> :
item.type === VariableType.File ?
<InputFileComponent
isSSO
@@ -90,6 +106,8 @@ export default function ChatReportForm({ flow, onStart }) {
</div>
)}
</div>
<Button size="sm" className="shengcheng-btn" onClick={handleStart}>{t('report.start')}</Button>
{type === 'chat' && <Button size="sm" className="shengcheng-btn" onClick={handleStart}>{t('report.start')}</Button>}
</div>
};
});
export default ChatReportForm

View File

@@ -18,6 +18,10 @@ import robot3 from "../../assets/robot3.png";
import duihuaItemTop from "../../assets/chat/duihua-item-top.png";
import duihuaItemJia from "../../assets/chat/duihua-item-+.png";
import duihuaItemGuan from "../../assets/chat/duihua-item-x.png";
import SkillChatSheet from "@/components/bs-comp/sheets/SkillChatSheet";
import { TitleIconBg } from "@/components/bs-comp/cardComponent";
import npcIcon from "../../assets/npc/npcIcon.png";
import nengliIcon from "../../assets/npc/nengliIcon.png";
export default function SkillChatPage() {
@@ -25,6 +29,9 @@ export default function SkillChatPage() {
const [face, setFace] = useState(true);
const { t } = useTranslation()
const [selectChat, setSelelctChat] = useState<any>({
id: '', chatId: '', type: ''
})
const { flow: initFlow } = useContext(TabsContext);
const [flow, setFlow] = useState<FlowType>(null)
@@ -39,45 +46,33 @@ export default function SkillChatPage() {
);
// 对话列表
const { chatList, chatId, chatsRef, setChatId, addChat, deleteChat } = useChatList()
const chatIdRef = useRef('')
// select flow
const handlerSelectFlow = async (node: FlowType) => {
const handlerSelectFlow = async (card) => {
// 会话ID
chatIdRef.current = generateUUID(32)
setOpen(false)
const _chatId = generateUUID(32)
// setOpen(false)
// add list
addChat({
"flow_name": node.name,
"flow_description": node.description,
"flow_id": node.id,
"chat_id": chatIdRef.current,
"flow_name": card.name,
"flow_description": card.desc,
"flow_id": card.id,
"chat_id": _chatId,
"create_time": "-",
"update_time": "-"
"update_time": "-",
"flow_type": card.flow_type
})
const flow = await getFlowApi(node.id)
setFlow(flow)
setChatId(chatIdRef.current)
setFace(false)
setSelelctChat({ id: card.id, chatId: _chatId, type: card.flow_type })
setChatId(_chatId)
}
// select chat
const handleSelectChat = useDebounce(async (chat) => {
console.log('chat.id :>> ', chat);
if (chat.chat_id === chatId) return
chatIdRef.current = chat.chat_id
const flow = initFlow?.id === chat.flow_id ? initFlow : await getFlowApi(chat.flow_id)
// if (!flow) {
// setInputState({ lock: true, errorCode: '1004' })
// clearHistory()
// return setFace(false)
// }
setFlow(flow)
setSelelctChat({ id: chat.flow_id, chatId: chat.chat_id, type: chat.flow_type })
setChatId(chat.chat_id)
setFace(false)
}, 100, false)
@@ -88,7 +83,7 @@ export default function SkillChatPage() {
desc: t('chat.confirmDeleteChat'),
onOk(next) {
deleteChat(id);
setFace(true)
setSelelctChat({ id: '', chatId: '', type: '' })
next()
}
})
@@ -96,9 +91,16 @@ export default function SkillChatPage() {
return <div className="flex">
<div className="h-screen w-[288px] border-r xinDuiHua-box relative">
<div className="h-screen w-[288px] relative">
<div className="xinDuiHua absolute">
<div className="xinDuiHua-btn cursor-pointer" onClick={() => setOpen(true)}>{t('chat.newChat')}</div>
<SkillChatSheet onSelect={handlerSelectFlow}>
<div id="newchat" className="xinDuiHua-btn cursor-pointer">
{/* <PlusBoxIcon className="dark:hidden"></PlusBoxIcon> */}
{/* <PlusBoxIconDark className="hidden dark:block"></PlusBoxIconDark> */}
{t('chat.newChat')}
</div>
</SkillChatSheet>
{/* <div className="xinDuiHua-btn cursor-pointer" onClick={() => setOpen(true)}>{t('chat.newChat')}</div> */}
{/* <div className="xinDuiHua-del cursor-pointer">
<img src={duihuaDel} alt=""/>
</div> */}
@@ -110,9 +112,10 @@ export default function SkillChatPage() {
className={` group item xinDuiHua-list-item relative hover:xinDuiHua-list-active cursor-pointer dark:hover:xinDuiHua-list-active ${chatId === chat.chat_id && 'xinDuiHua-list-active dark:xinDuiHua-list-active'}`}
onClick={() => handleSelectChat(chat)}>
<div>
{(chat.flow_id == "06b1d374-ba97-46e6-8782-c56dec8dcc17" || chat.flow_id == "ed8e21f6-9757-43d0-b076-8c6e81bb0580") && <img src={robot2} alt=""/>}
{/* {(chat.flow_id == "06b1d374-ba97-46e6-8782-c56dec8dcc17" || chat.flow_id == "ed8e21f6-9757-43d0-b076-8c6e81bb0580") && <img src={robot2} alt=""/>}
{chat.flow_id == "ca214b41-2b73-4585-b172-bf1e546cf6ec" && <img src={robot3} alt=""/>}
{(chat.flow_id != "06b1d374-ba97-46e6-8782-c56dec8dcc17" && chat.flow_id != "ed8e21f6-9757-43d0-b076-8c6e81bb0580" && chat.flow_id != "ca214b41-2b73-4585-b172-bf1e546cf6ec") && <img src={robot} alt=""/>}
{(chat.flow_id != "06b1d374-ba97-46e6-8782-c56dec8dcc17" && chat.flow_id != "ed8e21f6-9757-43d0-b076-8c6e81bb0580" && chat.flow_id != "ca214b41-2b73-4585-b172-bf1e546cf6ec") && <img src={robot} alt=""/>} */}
<TitleIconBg className="w-[40px] h-[40px]" img={chat.avatar_img} id={chat.avatar_color ? chat.avatar_color : chat.flow_id} ><img src={chat.avatar_img ? chat.avatar_img : (chat.flow_type == "assistant" ? npcIcon : nengliIcon)} alt="" /></TitleIconBg>
{/* <img src={robot} alt=""/> */}
<div>
<p>{chat.flow_name}</p>
@@ -132,13 +135,7 @@ export default function SkillChatPage() {
</div>
</div>
{/* chat */}
{face
? <div className="flex-1 chat-box h-screen overflow-hidden relative">
<p className="text-center mt-[100px] text-sm text-gray-600">{t('chat.selectChat')}</p>
</div>
: <div className="flex-1 chat-box h-screen relative">
{flow && <ChatPanne chatId={chatId} flow={flow} />}
</div>}
<ChatPanne data={selectChat}></ChatPanne>
{/* 选择对话技能 */}
<SkillTemps
flows={onlineFlows}

View File

@@ -232,23 +232,23 @@ export const ChatMessage = ({ chat, userName, disabledReSend, showSearch, onSour
// if (chat.isSend) return chat.files.length ? <>
// 发送消息
if (chat.isSend) return <div className="flex flex-col items-end">
<div className="flex items-center avatarZk">
<p className="mr-[11px] text-[13px]">{userName}</p>
<div className="flex items-start avatarZk">
<div className="mr-[10px]">
<div className="chat-end-zk">
{chat.category === 'loading' && <span className="loading loading-spinner loading-xs mr-4 align-middle"></span>}
{chat.message[chat.chatKey]}
</div>
<div className='chat-end-btn'>
{!disabledReSend && <img src={btnEdit} onClick={() => !disabledReSend && onEdit(chat.message[chat.chatKey])} className="w-[20px] ml-[10px] cursor-pointer" alt=""/>}
{!disabledReSend && <img src={btnReSend} onClick={() => !disabledReSend && onReSend(chat.message[chat.chatKey])} className="w-[20px] ml-[10px] cursor-pointer" alt=""/>}
{/* <img src={btnDel} className="w-[20px] ml-[10px] cursor-pointer" alt=""/> */}
{/* {!showSearch && <Search size={18} className="cursor-pointer hover:text-blue-600 text-blue-400" onClick={() => onSearch(chat.message[chat.chatKey])}></Search>} */}
</div>
</div>
{/* <p className="mr-[11px] text-[13px]">{userName}</p> */}
<img src={robot} className="w-[30px]" alt=""/>
</div>
<div className="mt-[10px]">
<div className="chat-end-zk">
{chat.category === 'loading' && <span className="loading loading-spinner loading-xs mr-4 align-middle"></span>}
{chat.message[chat.chatKey]}
</div>
<div className='chat-end-btn'>
{!disabledReSend && <img src={btnEdit} onClick={() => !disabledReSend && onEdit(chat.message[chat.chatKey])} className="w-[20px] ml-[10px] cursor-pointer" alt=""/>}
{!disabledReSend && <img src={btnReSend} onClick={() => !disabledReSend && onReSend(chat.message[chat.chatKey])} className="w-[20px] ml-[10px] cursor-pointer" alt=""/>}
{/* <img src={btnDel} className="w-[20px] ml-[10px] cursor-pointer" alt=""/> */}
{/* {!showSearch && <Search size={18} className="cursor-pointer hover:text-blue-600 text-blue-400" onClick={() => onSearch(chat.message[chat.chatKey])}></Search>} */}
</div>
</div>
</div>
{/* 文件 */ }
// <div className="chat chat-end">
@@ -286,31 +286,31 @@ export const ChatMessage = ({ chat, userName, disabledReSend, showSearch, onSour
{/* <div className="chat-image avatar">
<div className="w-[40px] h-[40px] rounded-full flex items-center justify-center" style={{ background: avatarColor }}><Bot color="#fff" size={28} /></div>
</div> */}
<div className="flex items-center avatarZk">
<div className="flex items-start avatarZk">
<img src={robot} className="w-[30px]" alt=""/>
<p className="ml-[10px] text-[13px]">{userName}</p>
{/* <p className="ml-[10px] text-[13px]">{userName}</p> */}
<div ref={textRef} className={`min-h-8 min-w-[110px] max-w-[50vw] mt-[10px] ${chat.id && chat.end && 'pb-8'}`}>
<div className="chat-start-zk relative">
{chat.message.toString() ? mkdown : <span className="loading loading-ring loading-md"></span>}
{/* @user */}
{chat.receiver && <p className="text-blue-500 text-sm">@ {chat.receiver.user_name}</p>}
{/* 光标 */}
{chat.message.toString() && !chat.end && <div className="animate-cursor absolute w-2 h-5 ml-1 bg-gray-600" style={{ left: cursor.x, top: cursor.y }}></div>}
</div>
{/* 赞 踩 */}
{!!chat.id && chat.end && <Thumbs
id={chat.id}
data={chat.liked}
onCopy={handleCopy}
onDislike={onDislike}
className="chat-start-btnM"
></Thumbs>
// className={`absolute w-full left-0 bottom-[8px] justify-end pr-5`}></Thumbs>
}
</div>
</div>
{/* {chat.sender && <div className="chat-header text-gray-400 text-sm">{chat.sender}</div>} */}
<div ref={textRef} className={`min-h-8 min-w-[110px] max-w-[50vw] mt-[10px] ${chat.id && chat.end && 'pb-8'}`}>
<div className="chat-start-zk relative">
{chat.message.toString() ? mkdown : <span className="loading loading-ring loading-md"></span>}
{/* @user */}
{chat.receiver && <p className="text-blue-500 text-sm">@ {chat.receiver.user_name}</p>}
{/* 光标 */}
{chat.message.toString() && !chat.end && <div className="animate-cursor absolute w-2 h-5 ml-1 bg-gray-600" style={{ left: cursor.x, top: cursor.y }}></div>}
</div>
{/* 赞 踩 */}
{!!chat.id && chat.end && <Thumbs
id={chat.id}
data={chat.liked}
onCopy={handleCopy}
onDislike={onDislike}
className="chat-start-btnM"
></Thumbs>
// className={`absolute w-full left-0 bottom-[8px] justify-end pr-5`}></Thumbs>
}
</div>
{chat.source !== SourceType.NONE && chat.end && sourceContent(chat.source)}
</div>
};

View File

@@ -0,0 +1,942 @@
import cloneDeep from "lodash-es/cloneDeep";
import { ClipboardList, FileInput, FileText, Send, StopCircle } from "lucide-react";
import { forwardRef, useContext, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import ShadTooltip from "../../../components/ShadTooltipComponent";
import { Button } from "../../../components/ui/button";
import { alertContext } from "../../../contexts/alertContext";
import { TabsContext } from "../../../contexts/tabsContext";
import { getChatHistory, postBuildInit, postValidatePrompt } from "../../../controllers/API";
import { Variable } from "../../../controllers/API/flow";
import { sendAllProps } from "../../../types/api";
import { ChatMessageType } from "../../../types/chat";
import { FlowType, NodeType } from "../../../types/flow";
import { validateNode } from "../../../utils";
import { ChatMessage } from "./ChatMessageM";
import ChatReportForm from "../components/ChatReportForm";
import ResouceModal from "../components/ResouceModal";
import ThumbsMessage from "../components/ThumbsMessage";
import NpcInfo from "./NpcInfoM";
import DuiHua from "./DuiHuaM";
import PopUp from "./popUp";
import Quote from "./quote";
import { locationContext } from "../../../contexts/locationContext";
import titIconL from "../../../assets/chatM/tit-icon-l.png";
import titIconR from "../../../assets/chatM/tit-icon-r.png";
import duihuaSend from "../../../assets/chat/duihua-send.png";
interface Iprops {
chatId: string
flow: FlowType
queryString?: string
version?: string
}
export default forwardRef(function ChatPanne({ chatId, flow, queryString, version = 'v1' }: Iprops,ref) {
const { t } = useTranslation()
const { tabsState } = useContext(TabsContext);
const { isRoom, isForm, isReport, checkPrompt } = useFlowState(flow)
// build
const build = useBuild(flow, chatId)
// 消息列表
const { messages, messagesRef, loadHistory, setChatHistory, initGuide, changeHistoryByScroll } = useMessages(chatId, flow)
// ws通信
const { stop, connectWS, begin: chating, checkReLinkWs, sendAll } = useWebsocket(chatId, flow, setChatHistory, queryString, version)
// 停止状态
const [isStop, setIsStop] = useState(true)
// 输入框状态
const { inputState, inputEmpty, inputDisabled, inputRef,
formShow, setFormShow,
setInputState, setInputEmpty, handleTextAreaHeight } = useInputState({ flow, chatId, chating, messages, isForm, isReport })
const { appConfig } = useContext(locationContext)
// npc信息
const [isNpcInfo, setIsNpcInfo] = useState(false)
// 对话信息
const [isDuiHua, setIsDuiHua] = useState(false)
// 弹窗信息
const [isPopUp, setIsPopUp] = useState(false)
// 引用弹窗信息
const [isQuote, setIsQuote] = useState(false)
// 开始构建&切换初始化会话
const initChat = async () => {
await checkPrompt(flow)
await build()
const historyData = version === 'v1' ? await loadHistory() : (initGuide(), [])
await connectWS({ setInputState, setIsStop, changeHistoryByScroll })
setInputState({ lock: false, errorMsg: '' });
// 第一条消息,用来初始化会话
sendAll({
chatHistory: messages,
name: flow.name,
description: flow.description,
inputs: {},
flow_id: flow.id,
chat_id: chatId
})
changeHistoryByScroll.current = false
// 自动聚焦
if (inputRef.current) inputRef.current.value = ''
setTimeout(() => {
inputRef.current?.focus()
}, 500);
const isNewChat = historyData.length === 0 || historyData[0].id === 9999
setFormShow(isNewChat && isForm)
}
useEffect(() => {
initChat()
}, [flow])
// document.documentElement.addEventListener(
// 'touchstart',
// function (event) {
// if (event.touches.length > 1) {
// event.preventDefault();
// }
// },
// {
// passive: false,
// },
// );
// sendmsg user name
const sendUserName = useMemo(() => {
const node = flow.data.nodes.find(el => el.data.type === 'AutoGenUser')
return node?.data.node.template['name'].value || ''
}, [flow])
const handleSend = async () => {
const msg = inputRef.current?.value
setTimeout(() => {
if (inputRef.current) {
inputRef.current.value = ''
inputRef.current.style.height = 'auto'
}
setInputEmpty(true)
}, 100);
if (msg.trim() === '') return
setInputState({ lock: true, errorMsg: '' });
let inputs = tabsState[flow.id].formKeysData.input_keys;
const input = inputs.find((el: any) => !el.type)
const inputKey = input ? Object.keys(input)[0] : '';
setChatHistory((old) => {
let newChat = cloneDeep(old);
newChat.push({
isSend: true,
message: { ...input, [inputKey]: msg },
chatKey: inputKey,
thought: '',
category: '',
files: [],
end: false,
user_name: ""
})
return newChat
});
await checkReLinkWs(async () => {
// await build()
await connectWS({ setInputState, setIsStop, changeHistoryByScroll })
})
const chatInfo = {
chat_id: chatId,
flow_id: flow.id,
inputs: { ...input, [inputKey]: msg }
}
// @ts-ignore
isRoom && chating ? sendAll({ action: "continue", ...chatInfo })
: sendAll({
chatHistory: messages,
name: flow.name,
description: flow.description,
...chatInfo
});
}
// 报表请求
const sendReport = (items: Variable[], str) => {
let inputs = tabsState[flow.id].formKeysData.input_keys;
const input = inputs.find((el: any) => !el.type)
const inputKey = input ? Object.keys(input)[0] : '';
setChatHistory((old) => {
let newChat = cloneDeep(old);
newChat.push({
isSend: true,
message: { ...input, [inputKey]: str },
chatKey: inputKey,
thought: '',
category: '',
files: [],
end: false,
user_name: ""
})
return newChat
});
const data = items.map(item => ({
id: item.nodeId,
name: item.name,
file_path: item.type === 'file' ? item.value : '',
value: item.type === 'file' ? '' : item.value
}))
setIsStop(false)
setFormShow(false)
sendAll({
inputs: {
...input,
[inputKey]: str,
data
},
chatHistory: messages,
name: flow.name,
description: flow.description,
chat_id: chatId,
flow_id: flow.id,
});
}
// 溯源
const [souce, setSouce] = useState<ChatMessageType>(null)
const thumbRef = useRef(null)
return <div className="overflow-hidden relative duihua-chat">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<div className="absolute chatShareM-tit">
<div></div>
{/* <div className="cursor-pointer" onClick={() => setIsDuiHua(!isDuiHua)}>
<img src={titIconL} alt=""/>
</div> */}
<p>{flow.name}</p>
{/* <div className="cursor-pointer" onClick={() => setIsNpcInfo(!isNpcInfo)}>
<img src={titIconR} alt=""/>
</div> */}
<div></div>
</div>
<div className="chata mt-[50px]" style={{ height: 'calc(100% - 50px)' }}>
{/* 会话记录 */}
<div ref={messagesRef} className={`chat-panne h-full overflow-y-scroll no-scrollbar px-[15px] ${isRoom || isReport ? 'pb-40' : 'pb-[60px]'}`}>
{
messages.map((c, i) => <ChatMessage
key={c.id || i}
userName={sendUserName}
chat={c}
disabledReSend={inputDisabled}
showSearch={!!appConfig.dialogQuickSearch}
onSource={() => setSouce(c)}
onDislike={(chatId) => { thumbRef.current?.openModal(chatId) }}
onReSend={(msg) => {
inputRef.current.value = msg
handleSend()
}}
onEdit={(msg) => { inputRef.current.value = msg; setInputEmpty(!msg) }}
onSearch={(msg) => window.open(appConfig.dialogQuickSearch + encodeURIComponent(msg))}
></ChatMessage>)
}
</div>
{/* 输入框 */}
<div className="absolute w-full bottom-0 duihua-input-box pb-[10px]">
{/* <div className={`relative duihua-input
${inputDisabled && 'bg-gray-200 dark:bg-gray-600'}`}> */}
<div className={`relative duihua-input`}>
<textarea id='input'
ref={inputRef}
disabled={inputDisabled} rows={1}
className={`w-full resize-none border-none bg-transparent outline-none max-h-[160px]`}
placeholder={(inputDisabled ? "当前处于回复中或不支持输入状态" : t('chat.inputPlaceholder'))}
onInput={handleTextAreaHeight}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) handleSend()
}}></textarea>
<div className="absolute right-0 bottom-0 w-[42px] duihua-input-btn cursor-pointer">
{/* <ShadTooltip content={t('chat.sendTooltip')}>
<button disabled={inputEmpty || inputDisabled} className=" disabled:text-gray-400" onClick={handleSend}><Send /></button>
</ShadTooltip> */}
<div className="duihua-input-btn-send">
<img src={duihuaSend} onClick={handleSend} alt=""/>
</div>
</div>
{/* {inputState.errorMsg && <div className="bg-gray-200 absolute top-0 left-0 w-full h-full text-center text-gray-400 align-middle pt-4">{inputState.errorMsg}</div>} */}
{inputState.errorMsg && <div className="absolute top-0 left-0 w-full h-full text-center align-middle pt-4" style={{background:"#2E1212",color:"#FF6060"}}>{inputState.errorMsg}</div>}
</div>
<p className="mb-2 text-center text-gray-400 text-sm">{appConfig.dialogTips}</p>
</div>
</div>
{(isRoom || isReport) && <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>}
{/* 源文件类型 */}
<ResouceModal chatId={chatId} open={!!souce} data={souce} setOpen={() => setSouce(null)}></ResouceModal>
{/* 表单 */}
{isForm && formShow && <ChatReportForm flow={flow} onStart={sendReport} />}
{/* 踩 反馈 */}
<ThumbsMessage ref={thumbRef}></ThumbsMessage>
<NpcInfo isNpcInfo={isNpcInfo} setIsNpcInfo={setIsNpcInfo}></NpcInfo>
<DuiHua isDuiHua={isDuiHua} setIsDuiHua={setIsDuiHua}></DuiHua>
<PopUp isPopUp={isPopUp} setIsPopUp={setIsPopUp}></PopUp>
<Quote isQuote={isQuote} setIsQuote={setIsQuote}></Quote>
</div>
});
/**
* 输入框状态
* 分析 flow状态
* return 该技能含有表单、有报表、群聊
* @returns
*/
const useInputState = ({ flow, chatId, chating, messages, isForm, isReport }) => {
const { tabsState } = useContext(TabsContext);
const [inputState, setInputState] = useState({
lock: false,
errorMsg: ''
})
// 输入问答
const inputRef = useRef(null)
useEffect(() => {
!chating && setTimeout(() => {
// 对话结束自动聚焦
inputRef.current?.focus()
}, 1000);
}, [chating])
// input 滚动
const [inputEmpty, setInputEmpty] = useState(true)
useEffect(() => {
setInputEmpty(true)
if (inputRef.current) inputRef.current.value = ''
}, [chatId])
// 获取上传file input
const fileInputs = useMemo(() => {
return tabsState[flow.id]?.formKeysData?.input_keys?.filter((input: any) => input.type === 'file')
}, [tabsState, flow])
const handleTextAreaHeight = (e) => {
const textarea = e.target
textarea.style.height = 'auto'
textarea.style.height = textarea.scrollHeight + 'px'
setInputEmpty(textarea.value.trim() === '')
}
// input disabled
const inputDisabled = useMemo(() => {
return inputState.lock
// 表单 && 没回话或只有一个引导词
|| (isForm && (messages.length === 0 || (messages.length === 1 && messages[0].id === 9999)))
|| isReport
}, [inputState, fileInputs, isReport])
// 表单收起
const [formShow, setFormShow] = useState(true)
return {
inputState, inputEmpty, inputDisabled, inputRef,
formShow, setFormShow,
setInputState, setInputEmpty, handleTextAreaHeight
}
}
/**
* flow state
* 分析 flow状态
* return 该技能含有表单、有报表、群聊
* @returns
*/
const useFlowState = (flow: FlowType) => {
const flowSate = useMemo(() => {
// 是否群聊
const isRoom = !!flow.data?.nodes.find(node => node.data.type === "AutoGenChain")
// 是否展示表单
const isForm = !!flow.data?.nodes.find(node => ["VariableNode", "InputFileNode"].includes(node.data.type))
// 是否报表
const isReport = !!flow.data?.nodes.find(node => "Report" === node.data.type)
return { isRoom, isForm, isReport }
}, [flow])
// propmt类型补充自定义字段
const checkPrompt = async (_flow) => {
const params = _flow.data.nodes.map(node => {
const temps = []
const temp = node.data.node.template
Object.keys(temp).map(key => {
const { type, value } = temp[key]
if (type === 'prompt' && !!value) !temps.length && temps.push({ name: key, template: value, data: node.data })
})
return temps
}).flat()
const promises = params.map(param => {
return postValidatePrompt(param.name, param.template, param.data.node).then(res => {
if (res) param.data.node = res.frontend_node
})
})
return Promise.all(promises)
}
return { ...flowSate, checkPrompt }
}
/**
* 消息列表模块
* 翻页、追加、历史
* @returns
*/
const useMessages = (chatId, flow) => {
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
const lastIdRef = useRef(0)
// 控制开启自动随消息滚动(临时方案)
const changeHistoryByScroll = useRef(false)
const loadIdRef = useRef('') // 记录最后一个加载的 chatId
// 获取聊天记录
const loadHistory = async (lastId?: number) => {
loadIdRef.current = chatId
const res = await getChatHistory(flow.id, chatId, lastId ? 10 : 30, lastId)
const hisData = res.map(item => {
// let count = 0
let { message, files, is_bot, intermediate_steps, ...other } = item
try {
message = message && message[0] === '{' ? JSON.parse(message.replace(/([\t\n"])/g, '\\$1').replace(/'/g, '"')) : message || ''
} catch (e) {
// 未考虑的情况暂不处理
}
return {
...other,
chatKey: typeof message === 'string' ? undefined : Object.keys(message)[0],
end: true,
files: files ? JSON.parse(files) : [],
isSend: !is_bot,
message,
thought: intermediate_steps,
noAccess: true
}
})
lastIdRef.current = hisData[hisData.length - 1]?.id || lastIdRef.current || 0 // 记录最后一个id
let historyData = []
if (lastId) {
historyData = [...hisData.reverse(), ...chatHistory]
} else if (loadIdRef.current === chatId) { // 保证同一会话
historyData = hisData.reverse()
}
setChatHistory(historyData)
const pageSize = historyData.length < 30 ? 30 : 10 // 先偷懒
if (hisData.length < pageSize) initGuide()
return historyData
}
const loadLock = useRef(false)
const currentIdRef = useRef(0)
const loadNextPage = async () => {
if (loadLock.current) return
if (currentIdRef.current === lastIdRef.current) return // 最后一个相同表示聊天记录已到顶
loadLock.current = true
currentIdRef.current = lastIdRef.current
changeHistoryByScroll.current = true
await loadHistory(currentIdRef.current)
loadLock.current = false
// 滚动 hack TODO 滚动翻页设计
setTimeout(() => {
changeHistoryByScroll.current = false
}, 500);
}
const initGuide = () => {
const guideMsg = {
"category": "system",
"chat_id": chatId,
"end": true,
"create_time": "",
"extra": "{}",
"files": [],
"flow_id": flow.id,
"id": 9999,
"thought": flow.guide_word,
"is_bot": true,
"liked": 0,
"message": '',
"receiver": null,
"remark": null,
"sender": "",
"solved": 0,
isSend: false,
"source": 0,
"type": "end",
"update_time": "",
noAccess: true,
"user_id": 0
}
flow.guide_word && setChatHistory((chatHistory) =>
chatHistory[0]?.id === 9999 ? chatHistory : [guideMsg, ...chatHistory]
)
}
// 消息滚动
const messagesRef = useRef(null);
useEffect(() => {
if (messagesRef.current && !changeHistoryByScroll.current) { // 滚动加载不触发
messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
}
}, [chatHistory, changeHistoryByScroll]);
// 消息滚动加载
useEffect(() => {
function handleScroll() {
if (messagesRef.current.scrollTop <= 30) {
loadNextPage()
}
}
messagesRef.current?.addEventListener('scroll', handleScroll);
return () => messagesRef.current?.removeEventListener('scroll', handleScroll)
}, [messagesRef.current, chatHistory, chatId]);
return {
messages: chatHistory, messagesRef, loadHistory, setChatHistory, initGuide, changeHistoryByScroll
}
}
/**
* websocket 通信
* 建立连接、重连、断开、接收、发送
* @returns
*/
const useWebsocket = (chatId, flow, setChatHistory, queryString, version) => {
const ws = useRef<WebSocket | null>(null);
// 接收ws状态
const [begin, setBegin] = useState(false)
const { setErrorData } = useContext(alertContext);
const { t } = useTranslation()
const { appConfig } = useContext(locationContext)
const chatIdRef = useRef(chatId);
useEffect(() => {
chatIdRef.current = chatId;
}, [chatId])
function heartbeat() {
if (!ws.current) return;
if (ws.current.readyState !== 1) return;
ws.current.send("heartbeat");
setTimeout(heartbeat, 30000);
}
function getWebSocketUrl(flowId, isDevelopment = false) {
const token = localStorage.getItem("ws_token") || '';
const isSecureProtocol = window.location.protocol === "https:";
const webSocketProtocol = isSecureProtocol ? "wss" : "ws";
const host = appConfig.websocketHost || window.location.host // isDevelopment ? "localhost:7860" : window.location.host;
const chatEndpoint = version === 'v1' ? `/api/v1/chat/${flowId}?type=L1&chat_id=${chatId}&t=${token}`
: `/api/v2/chat/ws/${flowId}?type=L1&chat_id=${chatId}${queryString}&t=${token}`
return `${webSocketProtocol}://${host}${chatEndpoint}`;
}
const newChatStart = useRef(false) // 处理当前会话上下文丢失,阻止上一次打字机效果
// 自动重连次数
const tryReLinkCount = useRef(0)
const reConnect = (params) => {
if (tryReLinkCount.current <= 3) {
connectWS(params)
tryReLinkCount.current++
} else {
console.warn('超过最大重试次数 :>> ');
}
}
useEffect(() => {
tryReLinkCount.current = 0
newChatStart.current = true
}, [chatId])
function connectWS(params) {
const { setInputState, setIsStop, changeHistoryByScroll } = params
if (ws.current) return Promise.resolve('ok');
// 连接断开重链接
return new Promise((res, rej) => {
try {
const urlWs = getWebSocketUrl(
flow.id,
process.env.NODE_ENV === "development"
);
const newWs = new WebSocket(urlWs);
newWs.onopen = () => {
console.log("WebSocket connection established!");
res('ok')
// heartbeat()
};
newWs.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.chat_id !== chatIdRef.current) return
console.log('newChatStart.current :>> ', newChatStart.current);
const errorMsg = data.category === 'error' ? data.intermediate_steps : ''
if (newChatStart.current) {
if (data.type === 'close') {
newChatStart.current = false
return setInputState({ lock: false, errorMsg })
} else {
return setInputState({ lock: true, errorMsg })
}
}
// 异常类型处理,提示
if (errorMsg) return setInputState({ lock: true, errorMsg })
handleWsMessage({ data, setIsStop, setInputState, changeHistoryByScroll });
// get chat history
// 群聊@自己时开启input
if (data.type === 'end' && data.receiver?.is_self) {
setInputState({ lock: false, errorMsg: '' })
}
};
newWs.onclose = (event) => {
ws.current = null
handleOnClose({ event, setIsStop, setInputState });
// reConnect(params)
};
newWs.onerror = (ev) => {
ws.current = null
console.error('链接异常error', ev);
setIsStop(true)
setErrorData({
title: `${t('chat.networkError')}:`,
list: [
t('chat.networkErrorList1'),
t('chat.networkErrorList2'),
t('chat.networkErrorList3')
],
});
reConnect(params)
};
ws.current = newWs;
console.log('newWs :>> ', newWs);
} catch (error) {
console.error('创建链接异常', error);
rej(error)
}
})
}
var isStream = false;
function handleWsMessage({ data, setIsStop, setInputState, changeHistoryByScroll }) {
if (Array.isArray(data) && data.length) return
if (data.type === "begin") {
setBegin(true)
setIsStop(false)
changeHistoryByScroll.current = false
}
if (data.type === "close") {
setBegin(false)
setIsStop(true)
setInputState({ lock: false, errorMsg: '' });
changeHistoryByScroll.current = true
}
if (data.type === "start") {
setChatHistory((old) => {
let newChat = cloneDeep(old);
newChat.push({
isSend: false,
message: '',
chatKey: '',
thought: data.intermediate_steps || '',
category: data.category || '',
files: [],
end: false
})
return newChat
});
isStream = true;
}
if (data.type === "stream" && isStream) {
updateLastMessage({ str: data.message, thought: data.intermediate_steps });
}
if (data.type === "end") {
updateLastMessage({
...data,
str: data.message,
files: data.files || null,
end: true,
thought: data.intermediate_steps || '',
cate: data.category || '',
messageId: data.message_id,
noAccess: false,
liked: 0
});
isStream = false;
}
}
function updateLastMessage({ str, thought = '', end = false, files = [], cate = '', messageId = 0, source = false, noAccess = false, ...data }: {
str: string;
messageId?: number
thought?: string;
cate?: string;
end?: boolean;
files?: Array<any>;
source?: boolean
noAccess?: boolean
}) {
setChatHistory((old) => {
const newChats = [...old]
// console.log('newchats :>> ', newChats);
let chatsLen = newChats.length
const prevChat = newChats[chatsLen - 2]
// hack 过滤重复最后消息
if (end
&& str
&& chatsLen > 1
&& str === prevChat.message
// && data.sender === prevChat.sender
&& !prevChat.thought) {
newChats.splice(chatsLen - 2, 1) // 删上一条
chatsLen = newChats.length
}
// 更新
const lastChat = newChats[chatsLen - 1]
const newLastChat = {
...newChats[chatsLen - 1],
...data,
id: messageId,
message: lastChat.message + str,
thought: lastChat.thought + (thought ? `${thought}\n` : ''),
files,
category: cate,
source,
noAccess,
end
}
newChats[chatsLen - 1] = newLastChat
// start - end 之间没有内容删除load
if (end && !(newLastChat.files.length || newLastChat.thought || newLastChat.message)) {
newChats.pop()
}
return newChats;
});
}
// 发送ws
async function sendAll(data: sendAllProps) {
try {
if (ws.current) {
if (JSON.stringify(data.inputs) !== '{}') {
newChatStart.current = false
}
ws.current.send(JSON.stringify(data));
}
} catch (error) {
setErrorData({
title: "There was an error sending the message",
list: [error.message],
});
}
}
// 处理主动断开
function handleOnClose({ event, setIsStop, setInputState }) {
console.error('链接手动断开 event :>> ', event);
setIsStop(true)
setBegin(false)
if ([1005, 1008].includes(event.code)) {
console.warn('即将废弃 :>> ');
setInputState({ lock: true, errorMsg: event.reason });
} else {
if (event.reason) {
setErrorData({ title: event.reason });
// setChatHistory((old) => {
// let newChat = cloneDeep(old);
// if (newChat.length) {
// newChat[newChat.length - 1].end = true;
// }
// newChat.push({ end: true, message: `${t('chat.connectionbreakTip')}${event.reason}`, isSend: false, chatKey: '', files: [] });
// return newChat
// })
}
setInputState({ lock: false, errorMsg: '' });
}
}
useEffect(() => {
// destory
return () => {
// close prev connection
if (ws.current) {
switch (ws.current.readyState) {
case WebSocket.OPEN:
console.warn('前端主动关闭1')
ws.current.close()
; break;
case WebSocket.CONNECTING:
ws.current.onopen = () => {
console.warn('前端主动关闭2')
ws.current.close()
};
}
ws.current = null
}
}
}, [])
// 检测并重连
const checkReLinkWs = async (reConnect) => {
if (ws.current) return true
// 重连
// 上一条加loading
setChatHistory((old) => {
let newChat = [...old];
newChat[newChat.length - 1].category = 'loading';
return newChat;
});
await reConnect()
// 链接成功
// 上一条去loading
setChatHistory((old) => {
let newChat = [...old];
newChat[newChat.length - 1].category = '';
return newChat;
});
}
const handleStop = () => {
try {
if (ws) {
ws.current.send(JSON.stringify({
"action": "stop"
}));
}
} catch (error) {
setErrorData({
title: "There was an error stop the message",
list: [error.message],
});
}
}
return { begin, stop: handleStop, checkReLinkWs, sendAll, connectWS }
}
/**
* build flow
* 校验每个节点展示进度及结果返回input_keys;end_of_stream断开链接
* 主要校验节点并设置更新setTabsState的 formKeysData
* @returns
*/
const useBuild = (flow: FlowType, chatId: string) => {
const { setErrorData } = useContext(alertContext);
const { setTabsState } = useContext(TabsContext);
const { t } = useTranslation()
// SSE 服务端推送
async function streamNodeData(flow: FlowType, chatId: string) {
// Step 1: Make a POST request to send the flow data and receive a unique session ID
const { flowId } = await postBuildInit(flow, chatId);
// Step 2: Use the session ID to establish an SSE connection using EventSource
let validationResults = [];
let finished = false;
let buildEnd = false
const apiUrl = `/api/v1/build/stream/${flowId}?chat_id=${chatId}`;
const eventSource = new EventSource(apiUrl);
eventSource.onmessage = (event) => {
// If the event is parseable, return
if (!event.data) {
return;
}
const parsedData = JSON.parse(event.data);
// if the event is the end of the stream, close the connection
if (parsedData.end_of_stream) {
eventSource.close(); // 结束关闭链接
buildEnd = true
return;
} else if (parsedData.log) {
// If the event is a log, log it
// setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys) {
setTabsState((old) => {
return {
...old,
[flowId]: {
...old[flowId],
formKeysData: parsedData,
},
};
});
} else {
// setProgress(parsedData.progress);
validationResults.push(parsedData.valid);
}
};
eventSource.onerror = (error: any) => {
console.error("EventSource failed:", error);
eventSource.close();
if (error.data) {
const parsedData = JSON.parse(error.data);
setErrorData({ title: parsedData.error });
}
};
// Step 3: Wait for the stream to finish
while (!finished) {
await new Promise((resolve) => setTimeout(resolve, 100));
finished = buildEnd // validationResults.length === flow.data.nodes.length;
}
// Step 4: Return true if all nodes are valid, false otherwise
return validationResults.every((result) => result);
}
// 延时器
async function enforceMinimumLoadingTime(
startTime: number,
minimumLoadingTime: number
) {
const elapsedTime = Date.now() - startTime;
const remainingTime = minimumLoadingTime - elapsedTime;
if (remainingTime > 0) {
return new Promise((resolve) => setTimeout(resolve, remainingTime));
}
}
async function handleBuild() {
try {
const errors = flow.data.nodes.flatMap((n: NodeType) => validateNode(n, flow.data.edges))
if (errors.length > 0) {
setErrorData({
title: t('chat.buildError'),
list: errors,
});
return;
}
const minimumLoadingTime = 200; // in milliseconds
const startTime = Date.now();
await streamNodeData(flow, chatId);
await enforceMinimumLoadingTime(startTime, minimumLoadingTime); // 至少等200ms, 再继续(强制最小load时间)
} catch (error) {
console.error("Error:", error);
} finally {
}
}
return handleBuild
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,153 @@
// 嵌iframe、适配移动端
import { useEffect, useMemo, useState, useRef, useContext } from "react";
import { useLocation, useParams } from "react-router-dom";
import { getFlowApi, readOnlineFlows } from "../../../controllers/API/flow";
import { FlowType } from "../../../types/flow";
import { generateUUID } from "../../../utils";
import ChatPanneM from "./ChatPanneM";
import { useTranslation } from "react-i18next";
import { deleteChatApi, getChatsApi } from "../../../controllers/API";
import { captureAndAlertRequestErrorHoc } from "../../../controllers/request";
import { useDebounce, useTable } from "../../../util/hook";
import { TabsContext } from "../../../contexts/tabsContext";
import SkillTemps from "../../SkillPage/components/SkillTemps";
import titIconL from "../../../assets/chatM/tit-icon-l.png";
import titIconR from "../../../assets/chatM/tit-icon-r.png";
export default function chatShare() {
const { id: flowId } = useParams()
const location = useLocation();
const searchParams = new URLSearchParams(location.search);
const libId = searchParams.get('lib')
const tweak = searchParams.get('tweak')
const { t } = useTranslation();
const [open, setOpen] = useState(false)
// 对话列表
const { chatList, chatsRef, addChat, deleteChat } = useChatList()
const queryString = useMemo(() => {
const params = [];
if (libId) params.push(`knowledge_id=${libId}`);
if (tweak) params.push(`tweak=${tweak}`);
return params.length > 0 ? `&${params.join('&')}` : '';
}, [libId, tweak])
const chatIdRef = useRef('')
const handlerSelectFlow = async (node: FlowType) => {
// 会话ID
chatIdRef.current = generateUUID(32)
setOpen(false)
// add list
addChat({
"flow_name": node.name,
"flow_description": node.description,
"flow_id": node.id,
"chat_id": chatIdRef.current,
"create_time": "-",
"update_time": "-"
})
const flow = await getFlowApi(node.id)
setFlow(flow)
setChatId(chatIdRef.current)
// setFace(false)
}
//
const { flow: initFlow } = useContext(TabsContext);
const [flow, setFlow] = useState<FlowType>(null)
const {
data: onlineFlows,
loading,
search,
} = useTable<FlowType>({}, (param) =>
readOnlineFlows(param.page, param.keyword).then((res) => {
return res;
})
);
const [chatId, setChatId] = useState<string>('')
console.log(flowId,libId,tweak)
useEffect(() => {
flowId && getFlowApi(flowId).then(node => {
// 会话ID
setFlow(node)
setChatId(generateUUID(32))
})
}, [flowId])
// select chat
const handleSelectChat = useDebounce(async (chat) => {
if (chat.chat_id === chatId) return
const flow = initFlow?.id === chat.flow_id ? initFlow : await getFlowApi(chat.flow_id)
// if (!flow) {
// setInputState({ lock: true, errorCode: '1004' })
// clearHistory()
// return setFace(false)
// }
setFlow(flow)
setChatId(chat.chat_id)
}, 100, false)
const wsUrl = useMemo(() => {
const params = [];
if (libId) params.push(`knowledge_id=${libId}`);
if (tweak) params.push(`tweak=${tweak}`);
const paramStr = params.length > 0 ? `${params.join('&')}` : '';
return `/api/v2/chat/ws/${flowId}?type=L1&${paramStr}`
}, [libId, tweak])
const [data] = useState<any>({ id: flowId, chatId: generateUUID(32), type: 'flow' })
if (!flowId) return <div></div>
return <div className="chatShareM">
{/* <div className="chatShareM-tit">
<img src={titIconL} alt=""/>
<p>对话名称</p>
<img src={titIconR} alt=""/>
</div> */}
{/* {flow
? <div className="flex-1 chat-box h-screen relative">
{flow && <ChatPanneM version='v2' queryString={queryString} chatId={chatId} flow={flow} />}
</div>
:<div className="flex-1 chat-box h-screen overflow-hidden relative">
<p className="text-center mt-[100px] text-sm text-gray-600">{t('chat.selectChat')}</p>
</div>} */}
<ChatPanneM customWsHost={wsUrl} data={data} />
</div>
// flow ? <ChatPanne version='v2' queryString={queryString} chatId={chatId} flow={flow} /> : null
};
/**
* 本地对话列表
*/
const useChatList = () => {
const [chatList, setChatList] = useState([])
const chatsRef = useRef(null)
useEffect(() => {
getChatsApi().then(setChatList)
}, [])
return {
chatList,
chatsRef,
addChat: (chat) => {
const newList = [chat, ...chatList]
// localStorage.setItem(ITEM_KEY, JSON.stringify(newList))
setChatList(newList)
setTimeout(() => {
chatsRef.current.scrollTop = 1
}, 0);
},
deleteChat: (id: string) => {
// api
captureAndAlertRequestErrorHoc(deleteChatApi(id).then(res => {
setChatList(oldList => oldList.filter(item => item.chat_id !== id))
}))
}
}
}

View File

@@ -73,21 +73,35 @@ export default function chatShare() {
setChatId(generateUUID(32))
})
}, [flowId])
// select chat
const handleSelectChat = useDebounce(async (chat) => {
if (chat.chat_id === chatId) return
// select chat
const handleSelectChat = useDebounce(async (chat) => {
if (chat.chat_id === chatId) return
const flow = initFlow?.id === chat.flow_id ? initFlow : await getFlowApi(chat.flow_id)
const flow = initFlow?.id === chat.flow_id ? initFlow : await getFlowApi(chat.flow_id)
// if (!flow) {
// setInputState({ lock: true, errorCode: '1004' })
// clearHistory()
// return setFace(false)
// }
// if (!flow) {
// setInputState({ lock: true, errorCode: '1004' })
// clearHistory()
// return setFace(false)
// }
setFlow(flow)
setChatId(chat.chat_id)
}, 100, false)
const wsUrl = useMemo(() => {
const params = [];
if (libId) params.push(`knowledge_id=${libId}`);
if (tweak) params.push(`tweak=${tweak}`);
const paramStr = params.length > 0 ? `${params.join('&')}` : '';
return `/api/v2/chat/ws/${flowId}?type=L1&${paramStr}`
}, [libId, tweak])
const [data] = useState<any>({ id: flowId, chatId: generateUUID(32), type: 'flow' })
setFlow(flow)
setChatId(chat.chat_id)
}, 100, false)
if (!flowId) return <div></div>
return <div className="chatShareM">
@@ -96,13 +110,14 @@ export default function chatShare() {
<p>对话名称</p>
<img src={titIconR} alt=""/>
</div> */}
{flow
{/* {flow
? <div className="flex-1 chat-box h-screen relative">
{flow && <ChatPanneM version='v2' queryString={queryString} chatId={chatId} flow={flow} />}
</div>
:<div className="flex-1 chat-box h-screen overflow-hidden relative">
<p className="text-center mt-[100px] text-sm text-gray-600">{t('chat.selectChat')}</p>
</div>}
</div>} */}
<ChatPanneM customWsHost={wsUrl} data={data} />
</div>
// flow ? <ChatPanne version='v2' queryString={queryString} chatId={chatId} flow={flow} /> : null
};

View File

@@ -46,7 +46,7 @@ export default function FilesPage() {
})
)
// loadData();
setTimeout(() => reload(), 5000);
// setTimeout(() => reload(), 5000);
const [hasPermission, setHasPermission] = useState(true)
const { appConfig } = useContext(locationContext)

View File

@@ -13,7 +13,7 @@ const SelectComp = ({ value, onChange = (id) => { }, data, disabled = false }) =
}
return <Select value={value} onValueChange={handleChange} disabled={disabled}>
<SelectTrigger className="w-[120px] h-6">
<SelectTrigger className="w-[61px] h-[27px] SelectTrigger">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -41,7 +41,7 @@ export default function CardSelectVersion(
<TooltipTrigger>
<SelectComp {...props} value={value} onChange={setValue} />
</TooltipTrigger>
<TooltipContent>
<TooltipContent className="bg-[#E6B71E] text-[#000]">
<p>{t('skills.chooseOnline')}</p>
</TooltipContent>
</Tooltip>

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { LoadIcon } from "../../../components/bs-icons/loading";
import { Button } from "../../../components/bs-ui/button";
@@ -7,6 +7,10 @@ import { Input, Textarea } from "../../../components/bs-ui/input";
import { createAssistantsApi } from "../../../controllers/API/assistant";
import { captureAndAlertRequestErrorHoc } from "../../../controllers/request";
import { useTranslation } from "react-i18next";
import { TitleIconBg } from "@/components/bs-comp/cardComponent";
import { uploadFileWithProgress, uploadNpcHeaderLibFileWithProgress } from "../../../modals/UploadModal/upload";
import huifumoren from "../../../assets/npc/huifumoren.png";
import npcIcon from "../../../assets/npc/npcIcon.png";
export default function CreateAssistant() {
@@ -67,7 +71,13 @@ export default function CreateAssistant() {
setErrors(formErrors);
return isValid;
};
const [avatar_img, setAvatar_img] = useState("")
const [avatar_color, setAvatar_color] = useState("")
const randomNum = Math.floor(Math.random()*(4-0+1)+0).toString();
useEffect(() => {
if (avatar_color != "") return
setAvatar_color(randomNum);
}, [avatar_color]);
// Handle form submission
const navigate = useNavigate()
const handleSubmit = async (e) => {
@@ -77,7 +87,7 @@ export default function CreateAssistant() {
if (isValid) {
console.log('Form data:', formData);
setLoading(true)
const res = await captureAndAlertRequestErrorHoc(createAssistantsApi(formData.name, formData.roleAndTasks))
const res = await captureAndAlertRequestErrorHoc(createAssistantsApi(formData.name, formData.roleAndTasks, avatar_img, avatar_color))
if (res) {
window.assistantCreate = true // 标记新建助手
navigate('/assistant/' + res.id)
@@ -85,12 +95,46 @@ export default function CreateAssistant() {
setLoading(false)
}
};
const handleButtonClick = () => {
// Create a file input element
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.style.display = "none"; // Hidden from view
input.multiple = false; // Allow only one file selection
input.onchange = (e: Event) => {
setLoading(true);
// Get the selected file
const file = (e.target as HTMLInputElement).files?.[0];
// Check if the file type is correct
// Upload the file
uploadNpcHeaderLibFileWithProgress(file, (progress) => { }).then(res => {
setLoading(false);
setAvatar_img(res);
})
};
// Trigger the file selection dialog
input.click();
};
return <DialogContent className="sm:max-w-[625px] bg-[#000000]">
return <DialogContent className="sm:max-w-[625px] bg-[#262626]">
<DialogHeader>
<DialogTitle className="text-[#fff]">NPC</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-8 py-6">
<div>
<label htmlFor="name" className="bisheng-label text-[#999999]"><span className="bisheng-tip text-[#FF6060]">* </span>NPC头像</label>
{/* <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 : npcIcon} alt="" /></TitleIconBg> */}
<div className="flex items-center ml-[7px] mt-[6px]">
<TitleIconBg className="w-[41px] h-[41px] min-w-[41px]" img={avatar_img} id={avatar_color} ><img onClick={handleButtonClick} src={avatar_img ? avatar_img : npcIcon} alt="" /></TitleIconBg>
<div className="flex items-center justify-center ml-[20px] w-[95px] h-[27px] bg-[#333333] cursor-pointer" style={{borderRadius:"14px"}} onClick={() => setAvatar_img("")}>
<img src={huifumoren} className="w-[12px] h-[11px]" alt="" />
<span className="ml-[5px] text-[#999999] text-[12px] mt-[1px]"></span>
</div>
</div>
</div>
<div className="">
<label htmlFor="name" className="bisheng-label text-[#999999]"><span className="bisheng-tip text-[#FF6060]">* </span>NPC名称</label>
<Input id="name" name="name" placeholder="给NPC取一个名字" className="mt-2 npcInput" value={formData.name} onChange={handleChange} />

View File

@@ -14,6 +14,7 @@ import { captureAndAlertRequestErrorHoc } from "@/controllers/request"
import { PlusIcon } from "@radix-ui/react-icons"
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import borderR from "../../../assets/npc/border-r.png";
const TestDialog = forwardRef((props: any, ref) => {
const {t} = useTranslation()
@@ -116,7 +117,7 @@ const TestDialog = forwardRef((props: any, ref) => {
<Button className="bg-[#ffd025] hover:bg-[#ffd025] text-[#333]" onClick={handleTest} disabled={loading}>{t('test.test')}</Button>
<div className="">
<label htmlFor="desc" className="bisheng-label text-[#fff]">{t('test.result')}</label>
<Textarea id="desc" name="desc" value={result} placeholder={t('test.outResultPlaceholder')} readOnly className="mt-2 npcInput2 text-[#fff]" />
<Textarea id="desc" name="desc" value={result} placeholder={t('test.outResultPlaceholder')} readOnly className="mt-2 npcInput2 text-[#fff] overflow-y-auto no-scrollbar" />
</div>
</div>}
</DialogContent>
@@ -159,6 +160,7 @@ const EditTool = forwardRef((props: any, ref) => {
setShow(bln)
}
const [delShow, setDelShow] = useState(false)
const [delShow1, setDelShow1] = useState(false)
const schemaUrl = useRef('')
const [formState, setFormState] = useState({ ...formData });
@@ -281,18 +283,24 @@ const EditTool = forwardRef((props: any, ref) => {
// 删除工具
const handleDelete = () => {
bsConfirm({
title: t('prompt'),
desc: t('skills.deleteSure'),
onOk(next) {
// api
captureAndAlertRequestErrorHoc(deleteTool(fromDataRef.current.id)).then(res => {
if (res === false) return
props.onReload()
setEditShow(false)
next()
})
}
// bsConfirm({
// title: t('prompt'),
// desc: t('skills.deleteSure'),
// onOk(next) {
// // api
// captureAndAlertRequestErrorHoc(deleteTool(fromDataRef.current.id)).then(res => {
// if (res === false) return
// props.onReload()
// setEditShow(false)
// next()
// })
// }
// })
captureAndAlertRequestErrorHoc(deleteTool(fromDataRef.current.id)).then(res => {
if (res === false) return
props.onReload()
setEditShow(false)
next()
})
}
@@ -305,93 +313,205 @@ const EditTool = forwardRef((props: any, ref) => {
return <div>
<Sheet open={editShow} onOpenChange={setEditShow}>
<SheetContent className="w-[800px] bg-[#000] sm:max-w-[800px] p-4">
<SheetHeader>
<SheetTitle>{delShow ? t('edit') : t('create')}</SheetTitle>
</SheetHeader>
<div className="mt-4 overflow-y-auto h-screen pb-40">
{/* name */}
<label htmlFor="open" className="px-6 text-[#fff]"></label>
<div className="px-6 mb-4" >
<Input
id="toolName"
name="toolName"
className="mt-2 npcInput2"
placeholder="输入工具名称"
value={formState.toolName}
onChange={handleInputChange}
/>
</div>
{/* schema */}
<div className="px-6 flex items-center justify-between">
<label htmlFor="open" className="text-[#fff]">OpenAPI Schema</label>
<div className="flex gap-2">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="text-[#fff] bg-[#1a1a1a]"><PlusIcon /> URL导入</Button>
</PopoverTrigger>
<PopoverContent className="w-80" align="end">
<div className="flex items-center gap-4 ">
<Input
id="schemaUrl"
name="schemaUrl"
placeholder="https://"
onChange={(e) => schemaUrl.current = e.target.value}
/>
<PopoverClose>
<Button size="sm" className="w-16 text-[#fff] bg-[#1a1a1a]" onClick={handleImportSchema}>{t('skills.import')}</Button>
</PopoverClose>
<SheetContent className="flex w-[800px] bg-[#1a1a1a] sm:max-w-[800px]">
<div className="xinDuiHua-boxR">
{/* <img src={borderR} className="w-[30px] h-[100%]" alt="" /> */}
</div>
<div className="w-[800px] pt-[10px]">
<SheetHeader>
<SheetTitle>{delShow ? t('edit') : t('create')}</SheetTitle>
</SheetHeader>
<div className="mt-4 overflow-y-auto h-screen pb-40">
{/* name */}
<label htmlFor="open" className="px-6 text-[#fff]"></label>
<div className="px-6 mb-4" >
<Input
id="toolName"
name="toolName"
className="mt-2 npcInput2"
placeholder="输入工具名称"
value={formState.toolName}
onChange={handleInputChange}
/>
</div>
{/* schema */}
<div className="px-6 flex items-center justify-between">
<label htmlFor="open" className="text-[#fff]">OpenAPI Schema</label>
<div className="flex gap-2">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="text-[#fff] bg-[#1a1a1a]"><PlusIcon /> URL导入</Button>
</PopoverTrigger>
<PopoverContent className="w-80" align="end">
<div className="flex items-center gap-4 ">
<Input
id="schemaUrl"
name="schemaUrl"
placeholder="https://"
onChange={(e) => schemaUrl.current = e.target.value}
/>
<PopoverClose>
<Button size="sm" className="w-16 text-[#fff] bg-[#1a1a1a]" onClick={handleImportSchema}>{t('skills.import')}</Button>
</PopoverClose>
</div>
</PopoverContent>
</Popover>
<Select value="1" onValueChange={(k) => handleSelectTemplate(k)}>
<SelectTrigger >
<span>{t('tools1.examples')}</span>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="json">{t('tools1.weatherJson')}</SelectItem>
<SelectItem value="yaml">{t('tools1.petShopYaml')}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
<div className="px-6 mb-4" >
<Textarea
id="schemaContent"
name="schemaContent"
placeholder={t('tools1.enterOpenAPISchema')}
className="mt-2 min-h-52 npcInput"
value={formState.schemaContent}
onChange={handleInputChange}
onBlur={() => handleSelectTemplate()}
/>
</div>
<label htmlFor="open" className="px-6 text-[#fff]">{t('tools1.authenticationType')}</label>
<div className="px-6">
<div className="px-6 mb-4" >
<label htmlFor="open" className="bisheng-label text-[#999]">{t('tools1.authType')}</label>
<RadioGroup
id="authMethod"
name="authMethod"
defaultValue={formState.authMethod}
className="flex mt-2 gap-4"
onValueChange={(value) => setFormState(prevState => ({ ...prevState, authMethod: value }))}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="none" id="r1" />
<Label htmlFor="r1" className="text-[#fff]">{t('tools1.none')}</Label>
</div>
</PopoverContent>
</Popover>
<Select value="1" onValueChange={(k) => handleSelectTemplate(k)}>
<SelectTrigger >
<span>{t('tools1.examples')}</span>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="json">{t('tools1.weatherJson')}</SelectItem>
<SelectItem value="yaml">{t('tools1.petShopYaml')}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div className="flex items-center space-x-2">
<RadioGroupItem value="apikey" id="r2" />
<Label htmlFor="r2" className="text-[#fff]">{t('tools1.apiKey')}</Label>
</div>
</RadioGroup>
</div>
{formState.authMethod === "apikey" && (<>
<div className="px-6 mb-4">
<label className="bisheng-label text-[#fff]" htmlFor="apiKey">API Key</label>
<Input
id="apiKey"
name="apiKey"
className="mt-2"
value={formState.apiKey}
onChange={handleInputChange}
/>
</div>
<div className="px-6 mb-4" >
<label htmlFor="open" className="bisheng-label text-[#fff]">Auth Type</label>
<RadioGroup
id="authType"
name="authType"
defaultValue={formState.authType}
className="flex mt-2 gap-4"
onValueChange={(value) => setFormState(prevState => ({ ...prevState, authType: value }))}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="basic" id="r4" />
<Label htmlFor="r4">Basic</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="bearer" id="r5" />
<Label htmlFor="r5">Bearer</Label>
</div>
{/* <div className="flex items-center space-x-2">
<RadioGroupItem value="custom" id="r6" />
<Label htmlFor="r6">Custom</Label>
</div> */}
</RadioGroup>
</div>
</>)}
{/* {formState.authMethod === "custom" && (
<div className="px-6 mb-4">
<label htmlFor="customHeader">Custom Header Name</label>
<Input
id="customHeader"
name="customHeader"
className="mt-2"
value={formState.customHeader}
onChange={handleInputChange}
/>
</div>
)} */}
</div>
<label htmlFor="open" className="px-6 text-[#fff]">{t('tools1.availableTools')}</label>
<div className="px-6 mb-4" >
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px] text-[#fff]">{t('tools1.name')}</TableHead>
<TableHead className="text-[#fff]">{t('tools1.description')}</TableHead>
<TableHead className="text-[#fff]">{t('tools1.method')}</TableHead>
<TableHead className="text-[#fff]">{t('tools1.path')}</TableHead>
<TableHead className="text-[#fff]">{t('operations')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{
tableData.length ? tableData.map((item, index) =>
<TableRow key={index}>
<TableCell>{item.name}</TableCell>
<TableCell>{item.desc}</TableCell>
<TableCell>{item.extra.method}</TableCell>
<TableCell>{item.extra.path}</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => testDialogRef.current.open(item, fromDataRef.current)}
>{t('test.test')}</Button>
</TableCell>
</TableRow>
) :
<TableRow>
<TableCell colSpan={5}>{t('tools1.none')}</TableCell>
</TableRow>
}
</TableBody>
</Table>
</div>
</div>
<div className="px-6 mb-4" >
<Textarea
id="schemaContent"
name="schemaContent"
placeholder={t('tools1.enterOpenAPISchema')}
className="mt-2 min-h-52 npcInput"
value={formState.schemaContent}
onChange={handleInputChange}
onBlur={() => handleSelectTemplate()}
/>
</div>
<label htmlFor="open" className="px-6 text-[#fff]">{t('tools1.authenticationType')}</label>
<label htmlFor="open" className="px-6">{t('tools1.authenticationType')}</label>
<div className="px-6">
<div className="px-6 mb-4" >
<label htmlFor="open" className="bisheng-label text-[#999]">{t('tools1.authType')}</label>
<label htmlFor="open" className="bisheng-label">{t('tools1.authType')}</label>
<RadioGroup
id="authMethod"
name="authMethod"
defaultValue={formState.authMethod}
defaultValue="none"
className="flex mt-2 gap-4"
onValueChange={(value) => setFormState(prevState => ({ ...prevState, authMethod: value }))}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="none" id="r1" />
<Label htmlFor="r1" className="text-[#fff]">{t('tools1.none')}</Label>
<Label htmlFor="r1">{t('tools1.none')}</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="apikey" id="r2" />
<Label htmlFor="r2" className="text-[#fff]">{t('tools1.apiKey')}</Label>
<Label htmlFor="r2">{t('tools1.apiKey')}</Label>
</div>
</RadioGroup>
</div>
{formState.authMethod === "apikey" && (<>
{formState.authMethod === "apikey" && (
<div className="px-6 mb-4">
<label className="bisheng-label text-[#fff]" htmlFor="apiKey">API Key</label>
<label htmlFor="apiKey">{t('tools1.apiKey')}</label>
<Input
id="apiKey"
name="apiKey"
@@ -400,129 +520,33 @@ const EditTool = forwardRef((props: any, ref) => {
onChange={handleInputChange}
/>
</div>
<div className="px-6 mb-4" >
<label htmlFor="open" className="bisheng-label text-[#fff]">Auth Type</label>
<RadioGroup
id="authType"
name="authType"
defaultValue={formState.authType}
className="flex mt-2 gap-4"
onValueChange={(value) => setFormState(prevState => ({ ...prevState, authType: value }))}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="basic" id="r4" />
<Label htmlFor="r4">Basic</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="bearer" id="r5" />
<Label htmlFor="r5">Bearer</Label>
</div>
{/* <div className="flex items-center space-x-2">
<RadioGroupItem value="custom" id="r6" />
<Label htmlFor="r6">Custom</Label>
</div> */}
</RadioGroup>
</div>
</>)}
{/* {formState.authMethod === "custom" && (
<div className="px-6 mb-4">
<label htmlFor="customHeader">Custom Header Name</label>
<Input
id="customHeader"
name="customHeader"
className="mt-2"
value={formState.customHeader}
onChange={handleInputChange}
/>
</div>
)} */}
</div>
<label htmlFor="open" className="px-6 text-[#fff]">{t('tools1.availableTools')}</label>
<div className="px-6 mb-4" >
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px] text-[#fff]">{t('tools1.name')}</TableHead>
<TableHead className="text-[#fff]">{t('tools1.description')}</TableHead>
<TableHead className="text-[#fff]">{t('tools1.method')}</TableHead>
<TableHead className="text-[#fff]">{t('tools1.path')}</TableHead>
<TableHead className="text-[#fff]">{t('operations')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{
tableData.length ? tableData.map((item, index) =>
<TableRow key={index}>
<TableCell>{item.name}</TableCell>
<TableCell>{item.desc}</TableCell>
<TableCell>{item.extra.method}</TableCell>
<TableCell>{item.extra.path}</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => testDialogRef.current.open(item, fromDataRef.current)}
>{t('test.test')}</Button>
</TableCell>
</TableRow>
) :
<TableRow>
<TableCell colSpan={5}>{t('tools1.none')}</TableCell>
</TableRow>
}
</TableBody>
</Table>
)}
</div>
<SheetFooter className="absolute bottom-0 right-0 w-full px-6 py-4">
{delShow && <Button
size="sm"
variant="destructive"
className="absolute left-[50px]"
onClick={() => setDelShow1(true)}
>{t('tools1.delete')}</Button>}
<Button size="sm" variant="outline" className="baogao-btn baogao-btn2" onClick={() => setEditShow(false)}>{t('tools1.cancel')}</Button>
<Button size="sm" onClick={handleSave} className="baogao-btn baogao-btn2">{t('tools1.save')}</Button>
</SheetFooter>
</div>
<label htmlFor="open" className="px-6">{t('tools1.authenticationType')}</label>
<div className="px-6">
<div className="px-6 mb-4" >
<label htmlFor="open" className="bisheng-label">{t('tools1.authType')}</label>
<RadioGroup
id="authMethod"
name="authMethod"
defaultValue="none"
className="flex mt-2 gap-4"
onValueChange={(value) => setFormState(prevState => ({ ...prevState, authMethod: value }))}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="none" id="r1" />
<Label htmlFor="r1">{t('tools1.none')}</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="apikey" id="r2" />
<Label htmlFor="r2">{t('tools1.apiKey')}</Label>
</div>
</RadioGroup>
</div>
{formState.authMethod === "apikey" && (
<div className="px-6 mb-4">
<label htmlFor="apiKey">{t('tools1.apiKey')}</label>
<Input
id="apiKey"
name="apiKey"
className="mt-2"
value={formState.apiKey}
onChange={handleInputChange}
/>
</div>
)}
</div>
<SheetFooter className="absolute bottom-0 right-0 w-full px-6 py-4">
{delShow && <Button
size="sm"
variant="destructive"
className="absolute left-6"
onClick={handleDelete}
>{t('tools1.delete')}</Button>}
<Button size="sm" variant="outline" className="baogao-btn baogao-btn2" onClick={() => setEditShow(false)}>{t('tools1.cancel')}</Button>
<Button size="sm" onClick={handleSave} className="baogao-btn baogao-btn2">{t('tools1.save')}</Button>
</SheetFooter>
</SheetContent>
</Sheet >
{/* test dialog */}
<TestDialog ref={testDialogRef} />
<dialog className={`modal ${delShow1 && 'modal-open'}`}>
<form method="dialog" className="modal-box w-[400px] bg-[#262626] shadow-lg">
<h3 className="text-[16px] font-bold text-center" style={{color:"#FFFFFF"}}>{t('prompt')}</h3>
<p className="text-[12px] text-center mt-[18px]" style={{color:"#FFFFFF"}}> </p>
<div className="flex justify-center mt-[27px]">
<Button className="baogao-btn" variant="outline" onClick={() => setDelShow1(false)}>{t('cancel')}</Button>
<Button className="baogao-btn ml-[27px]" variant="destructive" onClick={handleDelete}>{t('delete')}</Button>
</div>
</form>
</dialog>
</div>
})

View File

@@ -18,6 +18,8 @@ import robot2 from "../../../assets/robot2.png";
import robot3 from "../../../assets/robot3.png";
import zidingyi1 from "../../../assets/npc/zidingyi1.png";
import zidingyi2 from "../../../assets/npc/zidingyi2.png";
import { TitleIconBg } from "@/components/bs-comp/cardComponent";
import nengliIcon from "../../../assets/npc/nengliIcon.png";
export default function SkillTemps({ flows, isTemp = false,
title = 'skills.skillTemplate',
@@ -49,18 +51,20 @@ export default function SkillTemps({ flows, isTemp = false,
<span>
<div>
{/* <img src={robot} className="w-[160px]" alt=""/> */}
{(item.id == "06b1d374-ba97-46e6-8782-c56dec8dcc17" || item.id == "ed8e21f6-9757-43d0-b076-8c6e81bb0580") && <img src={robot2} className="w-[160px]" alt=""/>}
{/* {(item.id == "06b1d374-ba97-46e6-8782-c56dec8dcc17" || item.id == "ed8e21f6-9757-43d0-b076-8c6e81bb0580") && <img src={robot2} className="w-[160px]" alt=""/>}
{item.id == "ca214b41-2b73-4585-b172-bf1e546cf6ec" && <img src={robot3} className="w-[160px]" 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-[160px]" 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-[160px]" alt=""/>} */}
<TitleIconBg className="w-[160px] h-[160px]" img={item.avatar_img} id={item.avatar_color ? item.avatar_color : item.id} ><img src={item.avatar_img ? item.avatar_img : nengliIcon} alt="" /></TitleIconBg>
</div>
</span>
</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 == "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=""/>}
{(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 : nengliIcon} alt="" /></TitleIconBg>
<div>
<p>{item.name}</p>
<div>

View File

@@ -178,7 +178,7 @@ export default function AutoPromptDialog({ onOpenChange }) {
<Textarea ref={areaRef} className="h-full npcInput" defaultValue={assistantState.prompt}
placeholder={t('build.prompt')}
></Textarea>
<Button className="group-hover:flex hidden h-6 absolute bottom-4 right-4" disabled={LoadType.Prompt <= loading} size="sm" onClick={handleUsePropmt}>{t('build.use')}</Button>
<Button className="group-hover:flex hidden h-6 absolute bottom-4 right-4 baogao-btn2" disabled={LoadType.Prompt <= loading} size="sm" onClick={handleUsePropmt}>{t('build.use')}</Button>
</div>
</div>
{/* 自动配置 */}
@@ -190,7 +190,7 @@ export default function AutoPromptDialog({ onOpenChange }) {
{/* 开场白 */}
<div className="group relative pb-12 mt-4 px-4 py-2 rounded-md bg-[#1a1a1a]">
<div className="text-md mb-2 font-medium leading-none flex text-[#fff]">{t('build.openingRemarks')}{LoadType.GuideWord === loading && <LoadIcon className="ml-2 text-gray-600" />}</div>
<Textarea ref={guideAreaRef} className="bg-transparent border-none bg-gray-50 npcInput1"></Textarea>
<Textarea ref={guideAreaRef} className="bg-transparent border-none bg-gray-50 npcInput1 overflow-y-auto no-scrollbar"></Textarea>
<Button className="group-hover:flex hidden h-6 absolute bottom-4 right-4 baogao-btn2" disabled={LoadType.GuideWord <= loading} size="sm" onClick={handleUseGuide}>{t('build.use')}</Button>
</div>
{/* 引导词 */}
@@ -198,7 +198,7 @@ export default function AutoPromptDialog({ onOpenChange }) {
<div className="text-md mb-2 font-medium leading-none flex text-[#fff]">{t('build.guidingQuestions')}{LoadType.GuideQuestion === loading && <LoadIcon className="ml-2 text-gray-600" />}</div>
{
question.map(qs => (
<p key={qs} className="text-sm text-[#666666] bg-[#262626] px-2 py-1 rounded-xl mb-2">{qs}</p>
<p key={qs} className="text-sm text-[#999999] bg-[#262626] px-2 py-1 rounded-xl mb-2">{qs}</p>
))
}
<Button className="group-hover:flex hidden h-6 absolute bottom-4 right-4 baogao-btn2" disabled={LoadType.GuideQuestion <= loading} size="sm" onClick={handleUserQuestion}>{t('build.use')}</Button>

View File

@@ -1,20 +1,23 @@
import { TitleIconBg } from "@/components/bs-comp/cardComponent";
import { Button } from "@/components/bs-ui/button";
import { DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/bs-ui/dialog";
import { Input, Textarea } from "@/components/bs-ui/input";
import { useToast } from "@/components/bs-ui/toast/use-toast";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import npcIcon from "../../../../assets/npc/npcIcon.png";
import huifumoren from "../../../../assets/npc/huifumoren.png";
import { uploadNpcHeaderLibFileWithProgress } from "@/modals/UploadModal/upload";
export default function EditAssistantDialog({ name, desc, onSave }) {
export default function EditAssistantDialog({ name, desc, avatar_img, avatar_color, onSave }) {
const { t } = useTranslation()
// State for form fields
const [formData, setFormData] = useState({ name: '', desc: '' });
const [formData, setFormData] = useState({ name: '', desc: '', avatar_img: '', avatar_color: '' });
useEffect(() => {
setFormData({ name, desc })
}, [name, desc])
// console.log(formData, name, desc);
setFormData({ name, desc, avatar_img, avatar_color })
}, [name, desc, avatar_img, avatar_color])
console.log(formData, name, desc, avatar_img, avatar_color);
// State for errors
const [errors, setErrors] = useState<any>({});
@@ -76,27 +79,62 @@ export default function EditAssistantDialog({ name, desc, onSave }) {
};
return <DialogContent className="sm:max-w-[625px]">
const handleButtonClick = () => {
// Create a file input element
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.style.display = "none"; // Hidden from view
input.multiple = false; // Allow only one file selection
input.onchange = (e: Event) => {
// setLoading(true);
// Get the selected file
const file = (e.target as HTMLInputElement).files?.[0];
// Check if the file type is correct
// Upload the file
uploadNpcHeaderLibFileWithProgress(file, (progress) => { }).then(res => {
// setLoading(false);
setFormData({ name, desc, avatar_img: res, avatar_color })
})
};
// Trigger the file selection dialog
input.click();
};
return <DialogContent className="sm:max-w-[625px] bg-[#262626]">
<DialogHeader>
<DialogTitle>{t('build.editAssistant')}</DialogTitle>
<DialogTitle className="text-[#fff]">{t('build.editAssistant')}</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-8 py-6">
<div className="">
<label htmlFor="name" className="bisheng-label">{t('build.assistantName')}<span className="bisheng-tip">*</span></label>
<Input id="name" name="name" placeholder={t('build.enterName')} className="mt-2" value={formData.name} onChange={handleChange} />
{errors.name && <p className="bisheng-tip mt-1">{errors.name}</p>}
<div>
<label htmlFor="name" className="bisheng-label text-[#999999]"><span className="bisheng-tip text-[#FF6060]">* </span>NPC头像</label>
{/* <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 : npcIcon} alt="" /></TitleIconBg> */}
<div className="flex items-center ml-[7px] mt-[6px]">
<TitleIconBg className="w-[41px] h-[41px] min-w-[41px]" img={formData.avatar_img} id={formData.avatar_color} ><img onClick={handleButtonClick} src={formData.avatar_img ? formData.avatar_img : npcIcon} alt="" /></TitleIconBg>
<div className="flex items-center justify-center ml-[20px] w-[95px] h-[27px] bg-[#333333] cursor-pointer" style={{borderRadius:"14px"}} onClick={() => setFormData({ name, desc, avatar_img: '', avatar_color })}>
<img src={huifumoren} className="w-[12px] h-[11px]" alt="" />
<span className="ml-[5px] text-[#999999] text-[12px] mt-[1px]"></span>
</div>
</div>
</div>
<div className="">
<label htmlFor="desc" className="bisheng-label">{t('build.assistantDesc')}</label>
<Textarea id="desc" name="desc" placeholder={t('build.enterDesc')} maxLength={1200} className="mt-2" value={formData.desc} onChange={handleChange} />
<label htmlFor="name" className="bisheng-label text-[#999999]">{t('build.assistantName')}<span className="bisheng-tip text-[#FF6060]">*</span></label>
<Input id="name" name="name" placeholder={t('build.enterName')} className="mt-2 npcInput" value={formData.name} onChange={handleChange} />
{errors.name && <p className="bisheng-tip mt-1 text-[#999999]">{errors.name}</p>}
</div>
<div className="">
<label htmlFor="desc" className="bisheng-label text-[#999999]">{t('build.assistantDesc')}</label>
<Textarea id="desc" name="desc" placeholder={t('build.enterDesc')} maxLength={1200} className="mt-2 npcInput no-scrollbar" value={formData.desc} onChange={handleChange} />
{errors.desc && <p className="bisheng-tip mt-1">{errors.desc}</p>}
</div>
</div>
<DialogFooter>
<DialogClose>
<Button variant="outline" className="px-11" type="button">{t('build.cancel')}</Button>
<Button variant="outline" className="px-11 baogao-btn baogao-btn2" type="button">{t('build.cancel')}</Button>
</DialogClose>
<Button type="submit" className="px-11" onClick={handleSubmit}>{t('build.confirm')}</Button>
<Button type="submit" className="px-11 baogao-btn baogao-btn2" onClick={handleSubmit}>{t('build.confirm')}</Button>
</DialogFooter>
</DialogContent>
};

View File

@@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import EditAssistantDialog from "./EditAssistantDialog";
import { useTranslation } from "react-i18next";
import npcIcon from "../../../../assets/npc/npcIcon.png";
export default function Header({ onSave, onLine }) {
const { t } = useTranslation()
@@ -30,11 +31,11 @@ export default function Header({ onSave, onLine }) {
setEditShow(false)
needSaveRef.current = true
}
return <div className="flex justify-between items-center border-b px-4">
<div className="flex items-center gap-2 py-4">
<Button variant="outline" size="icon" onClick={() => navigate(-1)}><ChevronLeftIcon className="h-4 w-4" /></Button>
<TitleIconBg id={assistantState.id} className="ml-4"><AssistantIcon /></TitleIconBg>
{/* <TitleIconBg id={assistantState.id} className="ml-4"><AssistantIcon /></TitleIconBg> */}
<TitleIconBg className="w-[40px] h-[40px]" img={assistantState.avatar_img} id={assistantState.avatar_color ? assistantState.avatar_color : assistantState.id} ><img src={assistantState.avatar_img ? assistantState.avatar_img : npcIcon} alt="" /></TitleIconBg>
<span className="bisheng-title text-[#fff]">{assistantState.name}</span>
{/* edit dialog */}
<Dialog open={editShow} onOpenChange={setEditShow}>
@@ -45,6 +46,8 @@ export default function Header({ onSave, onLine }) {
editShow && <EditAssistantDialog
name={assistantState.name}
desc={assistantState.desc}
avatar_img={assistantState.avatar_img}
avatar_color={assistantState.avatar_color ? assistantState.avatar_color : assistantState.id}
onSave={handleEditSave}></EditAssistantDialog>
}
</Dialog>

View File

@@ -86,7 +86,7 @@ export default function Setting() {
</label>
<Textarea
name="open"
className="mt-2 min-h-[34px] npcInput2"
className="mt-2 min-h-[34px] npcInput2 overflow-y-auto no-scrollbar"
style={{ height: 56 }}
placeholder={t("build.assistantMessageFormat")}
value={assistantState.guide_word}
@@ -180,7 +180,7 @@ export default function Setting() {
</AccordionItem>
</Accordion>
<h1 className="border-b bg-[#1a1a1a] text-[#999] indent-4 text-sm leading-8">
{t("build.abilities")}
</h1>
<Accordion
type="multiple"

View File

@@ -5,6 +5,7 @@ import { AssistantIcon } from "@/components/bs-icons/assistant";
import { useAssistantStore } from "@/store/assistantStore";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import npcIcon from "../../../../assets/npc/npcIcon.png";
export default function TestChat({ assisId, guideQuestion }) {
const token = localStorage.getItem("ws_token") || '';
@@ -40,7 +41,8 @@ export default function TestChat({ assisId, guideQuestion }) {
return <div className="relative h-full bs-chat-bg">
<div className="absolute flex w-full left-0 top-0 gap-2 px-4 py-2 items-center z-10 bg-[#000] shadow-sm">
<TitleIconBg className="" id={assistantState.id}><AssistantIcon /></TitleIconBg>
{/* <TitleIconBg className="" id={assistantState.id}><AssistantIcon /></TitleIconBg> */}
<TitleIconBg className="w-[24px] h-[24px]" img={assistantState.avatar_img} id={assistantState.avatar_color ? assistantState.avatar_color : assistantState.id} ><img src={assistantState.avatar_img ? assistantState.avatar_img : npcIcon} alt="" /></TitleIconBg>
<span className="text-sm text-[#fff]">{t('build.debugPreview')}</span>
</div>
<ChatComponent

View File

@@ -61,24 +61,19 @@ export default function editAssistant() {
// 上线助手
const handleOnline = async () => {
message({
title: t('prompt'),
variant: 'success',
description: t('skills.onlineSuccessful')
if (!handleCheck()) return
await handleSave()
await captureAndAlertRequestErrorHoc(changeAssistantStatusApi(assistantState.id, 1)).then(res => {
if (res === false) return
message({
title: t('prompt'),
variant: 'success',
description: t('skills.onlineSuccessful')
})
})
// if (!handleCheck()) return
// await handleSave()
// await captureAndAlertRequestErrorHoc(changeAssistantStatusApi(assistantState.id, 1)).then(res => {
// if (res === false) return
// message({
// title: t('prompt'),
// variant: 'success',
// description: t('skills.onlineSuccessful')
// })
// })
// setTimeout(() => {
// navigate('/build')
// }, 1200);
setTimeout(() => {
navigate('/build')
}, 1200);
}
// 校验助手数据
@@ -112,7 +107,7 @@ export default function editAssistant() {
<div className="flex h-[calc(100vh-70px)]">
<div className="w-[60%]">
<div className="text-md font-medium leading-none p-4 shadow-sm text-[#fff] bg-[#000]">{t('build.assistantConfiguration')}</div>
<div className="flex h-[calc(100vh-120px)]">
<div className="flex h-[calc(100vh-106px)]">
<Prompt></Prompt>
<Setting></Setting>
</div>

View File

@@ -141,7 +141,7 @@ export default function SkillPage() {
<img src={create} className="w-[14px]" alt=""/>
</div>}
<div className="w-[27px] h-[27px] create" onClick={() => {setIsShareLink(!isShareLink);setIsShareLinkData("http://npcall.ai:3003/chat/" + item.id)}}>
<div className="w-[27px] h-[27px] create" onClick={() => {setIsShareLink(!isShareLink);setIsShareLinkData(location.origin + "/chat/" + item.id)}}>
<img src={share} className="w-[14px]" alt=""/>
</div>
</div>

View File

@@ -22,6 +22,7 @@ import nengliIcon from "../../assets/npc/nengliIcon.png";
import robot from "../../assets/robot.png";
import { uploadFileWithProgress, uploadNpcHeaderLibFileWithProgress } from "../../modals/UploadModal/upload";
import { TitleIconBg, gradients } from "@/components/bs-comp/cardComponent";
import { useToast } from "@/components/bs-ui/toast/use-toast";
export default function l2Edit() {
const { t } = useTranslation()
@@ -32,16 +33,19 @@ export default function l2Edit() {
const flow = useMemo(() => {
return id ? nextFlow : null
}, [nextFlow])
console.log(flow,'flow')
const [isL2, setIsL2] = useState(false)
const [loading, setLoading] = useState(false)
const nameRef = useRef(null)
const descRef = useRef(null)
const guideRef = useRef(null)
const [logo, setLogo] = useState("")
const randomNum = Math.floor(Math.random()*(4-0+1)+0);
const [avatar_img, setAvatar_img] = useState("")
const [avatar_color, setAvatar_color] = useState("")
const randomNum = Math.floor(Math.random()*(4-0+1)+0).toString();
useEffect(() => {
if(!id){
setAvatar_color(randomNum);
}
// 无id不再请求
if (!id) return
// 已有flow 数据时,不再请求
@@ -50,7 +54,12 @@ export default function l2Edit() {
nameRef.current.value = flow.name
descRef.current.value = flow.description
guideRef.current.value = flow.guide_word
setLogo(flow.logo)
setAvatar_img(flow.avatar_img);
if(flow.avatar_color){
setAvatar_color(flow.avatar_color);
}else{
setAvatar_color(id);
}
return
}
// 无flow从db获取
@@ -61,14 +70,19 @@ export default function l2Edit() {
nameRef.current.value = _flow.name
descRef.current.value = _flow.description
guideRef.current.value = _flow.guide_word
setLogo(_flow.logo)
setAvatar_img(_flow.avatar_img);
if(_flow.avatar_color){
setAvatar_color(_flow.avatar_color);
}else{
setAvatar_color(id);
}
})
}, [id])
// 校验
const { user } = useContext(userContext);
const [error, setError] = useState({ name: false, desc: false }) // 表单error信息展示
// error信息展示
const { message } = useToast()
const isParamError = (name, desc, showErrorConfirm = false) => {
const errorlist = [];
if (!name) errorlist.push(t('skills.skillNameRequired'));
@@ -78,9 +92,10 @@ export default function l2Edit() {
const nameErrors = errorlist.length;
if (!desc) errorlist.push(t('skills.skillDescRequired'));
if (desc.length > 200) errorlist.push(t('skills.skillDescTooLong'));
if (errorlist.length && showErrorConfirm) setErrorData({
title: t('skills.errorTitle'),
list: errorlist,
if (errorlist.length && showErrorConfirm) message({
title: t('prompt'),
variant: 'error',
description: errorlist
});
setError({ name: !!nameErrors, desc: errorlist.length > nameErrors });
return !!errorlist.length;
@@ -92,11 +107,8 @@ export default function l2Edit() {
const name = nameRef.current.value
const guideWords = guideRef.current.value
const description = descRef.current.value
const avatar_img = logo
const avatar_color = gradients[randomNum]
if (isParamError(name, description, true)) return
setLoading(true)
await captureAndAlertRequestErrorHoc(createCustomFlowApi({
name,
description,
@@ -124,7 +136,7 @@ export default function l2Edit() {
setLoading(true)
formRef.current?.save()
await saveFlow({...flow, name, description, guide_word: guideWords}, true)
await saveFlow({...flow, name, description, guide_word: guideWords, avatar_img, avatar_color})
setLoading(false)
navigate('/flow/' + id, { replace: true })
}
@@ -133,14 +145,11 @@ export default function l2Edit() {
const name = nameRef.current.value
const description = descRef.current.value
const guideWords = guideRef.current.value
const avatar_img = logo
const avatar_color = gradients[randomNum]
// const logo = flow.logo
if (isParamError(name, description)) return
if (isParamError(name, description, true)) return
setLoading(true)
formRef.current?.save()
console.log(flow,name,description,guideWords,logo)
await saveFlow({...flow, name, description, guide_word: guideWords, avatar_img, avatar_color}, true)
await saveFlow({...flow, name, description, guide_word: guideWords, avatar_img, avatar_color})
setLoading(false)
setSuccessData({ title: t('success') });
setTimeout(() => /^\/skill\/[\w\d-]+/.test(location.pathname) && navigate(-1), 2000);
@@ -179,9 +188,10 @@ export default function l2Edit() {
uploadNpcHeaderLibFileWithProgress(file, (progress) => { }).then(res => {
// isSSO ? uploadFileWithProgress(file, (progress) => { }).then(res => {
setLoading(false);
if (typeof res === 'string') return setErrorData({ title: "Error", list: [res] })
const { file_path } = res;
setLogo(file_path);
// console.log(res)
// if (typeof res === 'string') return setErrorData({ title: "Error", list: [res] })
// const { file_path } = res;
setAvatar_img(res);
// logo = file_path;
// setMyValue(file.name);
// onChange(file.name);
@@ -220,12 +230,11 @@ export default function l2Edit() {
// Trigger the file selection dialog
input.click();
};
return <div className="relative box-border">
<div className="p-6 pb-48 h-screen overflow-y-auto">
<div className="flex justify-between w-full">
<ShadTooltip content={t('back')} side="right">
<button className="extra-side-bar-buttons w-[36px]" onClick={() => window.history.length < 3 ? navigate('/skills') : navigate(-1)}>
<button className="extra-side-bar-buttons w-[36px]" onClick={() => window.history.length < 3 ? navigate('/build/skills') : navigate(-1)}>
<ArrowLeft strokeWidth={1.5} className="side-bar-button-size" />
</button>
</ShadTooltip>
@@ -246,9 +255,11 @@ export default function l2Edit() {
<div className="pt-[20px] pr-[14px] pl-[14px]">
<p></p>
<div className="flex items-center ml-[7px] mt-[10px]">
{!logo ? <TitleIconBg className="w-[41px] h-[41px] min-w-[41px]" id={randomNum} ><img onClick={handleButtonClick} src={nengliIcon} alt="" /></TitleIconBg> : <img src={logo} className="w-[41px] h-[41px]" onClick={handleButtonClick} alt="" />}
<div className="flex items-center justify-center ml-[20px] w-[95px] h-[27px] bg-[#333333] cursor-pointer" style={{borderRadius:"14px"}} onClick={() => setLogo(robot)}>
{/* {flow && avatar_img ? <img src={flow.avatar_img} className="w-[41px] h-[41px] cursor-pointer" style={{borderRadius:"7px"}} onClick={handleButtonClick} alt="" /> : <TitleIconBg className="w-[41px] h-[41px] min-w-[41px]" img={avatar_img} id={avatar_color} ><img onClick={handleButtonClick} src={nengliIcon} alt="" /></TitleIconBg>} */}
{/* {flow && !flow.avatar_img && flow.avatar_color ? <TitleIconBg className="w-[41px] h-[41px] min-w-[41px]" id={randomNum} ><img onClick={handleButtonClick} src={nengliIcon} alt="" /></TitleIconBg> : <img src={logo} className="w-[41px] h-[41px]" onClick={handleButtonClick} alt="" />} */}
<TitleIconBg className="w-[41px] h-[41px] min-w-[41px]" img={avatar_img} id={avatar_color} ><img onClick={handleButtonClick} src={flow && avatar_img ? avatar_img : nengliIcon} alt="" /></TitleIconBg>
<div className="flex items-center justify-center ml-[20px] w-[95px] h-[27px] bg-[#333333] cursor-pointer" style={{borderRadius:"14px"}} onClick={() => setAvatar_img("")}>
<img src={huifumoren} className="w-[12px] h-[11px]" alt="" />
<span className="ml-[5px] text-[#999999] text-[12px] mt-[1px]"></span>
</div>

View File

@@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next";
import CardComponent from "../../components/bs-comp/cardComponent";
import CardComponent, { TitleIconBg } from "../../components/bs-comp/cardComponent";
import { Dialog, DialogTrigger } from "../../components/bs-ui/dialog";
import { SearchInput } from "../../components/bs-ui/input";
import AutoPagination from "../../components/bs-ui/pagination/autoPagination";
@@ -30,6 +30,8 @@ import { updataOnlineState } from "@/controllers/API/flow";
import { Switch } from "@/components/ui/switch";
import { userContext } from "@/contexts/userContext";
import ShareLink from "./externalUse/shareLink";
import npcIcon from "../../assets/npc/npcIcon.png";
import { Button } from "@/components/ui/button";
export default function Assistants() {
const { t } = useTranslation()
@@ -49,15 +51,19 @@ export default function Assistants() {
const inputRef = useRef(null);
const handleDelete = (data) => {
bsConfirm({
desc: t('确认删除该助手?'),
okTxt: t('delete'),
onOk(next) {
deleteAssistantApi(data.id).then(() => reload())
next()
}
})
const handleDelete = () => {
// bsConfirm({
// desc: t('确认删除该NPC'),
// okTxt: t('delete'),
// onOk(next) {
// deleteAssistantApi(data.id).then(() => reload())
// next()
// }
// })
deleteAssistantApi(idRef.current.id).then(res => {
reload();
close();
});
}
const handleCheckedChange = (checked, data) => {
@@ -69,6 +75,14 @@ export default function Assistants() {
})
}
const checkSassUrl = (url: string) => {
console.log(location.origin)
// return url;
// return url.replace(/https?:\/\/[^\/]+/, '')
// location.origin === SASS_HOST ? url.replace(/https?:\/\/[^\/]+/, '') : url;
}
const { delShow, idRef, close, delConfirm } = useDelete();
const render = (item: any) => {
const [openSwitch, setOpenSwitch] = useState(item.status === 1);
const handleChange = (bln) => {
@@ -94,8 +108,8 @@ export default function Assistants() {
<img src={zidingyijia} alt="" />
<span>NPC</span>
</div>
<p>NPCNPC </p>
<div className="flex justify-end mb-[7px] mr-[14px] mt-0">
<p>NPCNPC可以调用多个能力和工</p>
<div className="absolute flex justify-end bottom-[7px] right-[14px] mt-0">
<img src={zidingyi1} className="w-[68px]" alt="" />
</div>
</div>
@@ -120,17 +134,13 @@ export default function Assistants() {
{openSwitch && <span>
<span>
<div>
{(item.id == "06b1d374-ba97-46e6-8782-c56dec8dcc17" || item.id == "ed8e21f6-9757-43d0-b076-8c6e81bb0580") && <img src={robot2} className="w-[160px]" alt=""/>}
{item.id == "ca214b41-2b73-4585-b172-bf1e546cf6ec" && <img src={robot3} className="w-[160px]" 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-[160px]" alt=""/>}
<TitleIconBg className="w-[160px] h-[160px]" img={item.avatar_img} id={item.avatar_color ? item.avatar_color : item.id} ><img src={item.avatar_img ? item.avatar_img : npcIcon} alt="" /></TitleIconBg>
</div>
</span>
</span>}
</div>
<div>
{(item.id == "06b1d374-ba97-46e6-8782-c56dec8dcc17" || item.id == "ed8e21f6-9757-43d0-b076-8c6e81bb0580") && <img src={robot2} className="w-[40px]" alt=""/>}
{item.id == "ca214b41-2b73-4585-b172-bf1e546cf6ec" && <img src={robot3} className="w-[40px]" 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-[40px]" 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 : npcIcon} alt="" /></TitleIconBg>
{/* <img src={robot} className="w-[40px]" alt=""/> */}
<p className="ml-[14px]">{item.name}</p>
</div>
@@ -142,12 +152,7 @@ export default function Assistants() {
</div>}
<div className="w-[27px] h-[27px] create mr-[14px]" onClick={() => {setIsShareLink(!isShareLink);setIsShareLinkData("http://npcall.ai:3003/chat/" + item.id)}}>
<img src={share} className="w-[14px]" alt=""/>
</div>
<div className="w-[27px] h-[27px] create" onClick={() => handleDelete(item)}>
<div className="w-[27px] h-[27px] create" onClick={() => delConfirm(item)}>
<img src={del} className="w-[14px]" alt=""/>
</div>
</div>
@@ -229,5 +234,33 @@ export default function Assistants() {
</Dialog>
{/* 分享链接 */}
<ShareLink isShareLink={isShareLink} setIsShareLink={setIsShareLink} data={isShareLinkData}></ShareLink>
<dialog className={`modal ${delShow && 'modal-open'}`}>
<form method="dialog" className="modal-box w-[400px] bg-[#262626] shadow-lg">
<h3 className="text-[16px] font-bold text-center" style={{color:"#FFFFFF"}}>{t('prompt')}</h3>
<p className="text-[12px] text-center mt-[18px]" style={{color:"#FFFFFF"}}>NPC </p>
<div className="flex justify-center mt-[27px]">
<Button className="baogao-btn" variant="outline" onClick={close}>{t('cancel')}</Button>
<Button className="baogao-btn ml-[27px]" variant="destructive" onClick={handleDelete}>{t('delete')}</Button>
</div>
</form>
</dialog>
</div>
};
};
const useDelete = () => {
const [delShow, setDelShow] = useState(false)
const idRef = useRef<any>(null)
return {
delShow,
idRef,
close: () => {
setDelShow(false)
},
delConfirm: (id) => {
idRef.current = id
setDelShow(true)
}
}
}

View File

@@ -4,7 +4,7 @@ import { useToast } from "@/components/bs-ui/toast/use-toast";
import { useContext, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import CardComponent from "../../components/bs-comp/cardComponent";
import CardComponent, { TitleIconBg } from "../../components/bs-comp/cardComponent";
import { MoveOneIcon } from "../../components/bs-icons/moveOne";
import { Button } from "../../components/bs-ui/button";
import { SearchInput } from "../../components/bs-ui/input";
@@ -28,7 +28,7 @@ import share from "../../assets/npc/share.png";
import moban from "../../assets/npc/moban.png";
import sousuo from "../../assets/npc/sousuo.png";
import del from "../../assets/npc/del.png";
import zidingyi1 from "../../assets/npc/zidingyi1.png";
import zidingyi1 from "../../assets/npc/nengliIcon1.png";
import zidingyijia from "../../assets/npc/zidingyijia.png";
import ShareLink from "./externalUse/shareLink";
import { Switch } from "@/components/ui/switch";
@@ -36,6 +36,7 @@ import { Flexbox } from 'react-layout-kit';
import Templates from "./temps";
import { Size } from "recharts/types/util/types";
import SkillTemps from "./components/SkillTemps";
import nengliIcon from "../../assets/npc/nengliIcon.png";
export default function Skills() {
const { t } = useTranslation()
@@ -72,14 +73,18 @@ export default function Skills() {
}
const handleDelete = (data) => {
bsConfirm({
desc: t('skills.confirmDeleteSkill'),
okTxt: t('delete'),
onOk(next) {
captureAndAlertRequestErrorHoc(deleteFlowFromDatabase(data.id).then(reload));
next()
}
})
// bsConfirm({
// desc: t('skills.confirmDeleteSkill'),
// okTxt: t('delete'),
// onOk(next) {
// captureAndAlertRequestErrorHoc(deleteFlowFromDatabase(data.id).then(reload));
// next()
// }
// })
captureAndAlertRequestErrorHoc(deleteFlowFromDatabase(idRef.current.id).then(res => {
reload();
close();
}));
}
const handleSetting = (data) => {
@@ -107,6 +112,7 @@ export default function Skills() {
window.SearchSkillsPage = { no: pageNo, key: inputRef.current.value };
nvaigate("/skill/" + id)
}
const { delShow, idRef, close, delConfirm } = useDelete();
// 模板管理
if (isTempsPage) return <Templates onBack={() => setIsTempPage(false)} onChange={loadTemps}></Templates>
@@ -132,27 +138,25 @@ export default function Skills() {
<img src={zidingyijia} alt="" />
<span></span>
</div>
<p> 使</p>
<img src={zidingyi1} alt="" />
<p>使</p>
<div className="absolute flex justify-end bottom-[7px] right-[14px] mt-0">
<img src={zidingyi1} className="w-[68px]" alt="" />
</div>
</div>:
<div className={`selectNpcFlexbox`}>
<div className="npcInfoItemBg">
{openSwitch && <span>
<span>
<div>
{(item.id == "06b1d374-ba97-46e6-8782-c56dec8dcc17" || item.id == "ed8e21f6-9757-43d0-b076-8c6e81bb0580") && <img src={robot2} className="w-[160px]" alt=""/>}
{item.id == "ca214b41-2b73-4585-b172-bf1e546cf6ec" && <img src={robot3} className="w-[160px]" 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-[160px]" alt=""/>}
<TitleIconBg className="w-[160px] h-[160px]" img={item.avatar_img} id={item.avatar_color ? item.avatar_color : item.id} ><img src={item.avatar_img ? item.avatar_img : nengliIcon} alt="" /></TitleIconBg>
</div>
</span>
</span>}
</div>
<div>
{(item.id == "06b1d374-ba97-46e6-8782-c56dec8dcc17" || item.id == "ed8e21f6-9757-43d0-b076-8c6e81bb0580") && <img src={robot2} className="w-[40px]" alt=""/>}
{item.id == "ca214b41-2b73-4585-b172-bf1e546cf6ec" && <img src={robot3} className="w-[40px]" 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-[40px]" alt=""/>}
<div>
<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 : nengliIcon} alt="" /></TitleIconBg>
{/* <img src={robot} className="w-[40px]" alt=""/> */}
<p className="ml-[14px]">{item.name}</p>
<p className="ml-[14px] yichu">{item.name}</p>
</div>
<p>{item.description}</p>
<div>
@@ -166,16 +170,23 @@ export default function Skills() {
<img src={create} className="w-[14px]" alt=""/>
</div>}
<div className="w-[27px] h-[27px] create mr-[14px]" onClick={() => {setIsShareLink(!isShareLink);setIsShareLinkData("http://npcall.ai:3003/chat/" + item.id)}}>
{openSwitch && <div className="w-[27px] h-[27px] create mr-[14px]" onClick={() => {setIsShareLink(!isShareLink);setIsShareLinkData(location.origin + "/chat/" + item.id)}}>
<img src={share} className="w-[14px]" alt=""/>
</div>
</div>}
<div className="w-[27px] h-[27px] create" onClick={() => handleDelete(item)}>
<div className="w-[27px] h-[27px] create" onClick={() => delConfirm(item)}>
<img src={del} className="w-[14px]" alt=""/>
</div>
</div>
<Switch checked={openSwitch} onCheckedChange={handleChange} />
</div>
<div className="absolute right-[7px] top-[7px]">
<CardSelectVersion
showPop={item.status !== 2}
data={item}
></CardSelectVersion>
</div>
</div>}
</Flexbox>
)
@@ -260,6 +271,17 @@ export default function Skills() {
</div>
{/* 分享链接 */}
<ShareLink isShareLink={isShareLink} setIsShareLink={setIsShareLink} data={isShareLinkData}></ShareLink>
<dialog className={`modal ${delShow && 'modal-open'}`}>
<form method="dialog" className="modal-box w-[400px] bg-[#262626] shadow-lg">
<h3 className="text-[16px] font-bold text-center" style={{color:"#FFFFFF"}}>{t('prompt')}</h3>
<p className="text-[12px] text-center mt-[18px]" style={{color:"#FFFFFF"}}> </p>
<div className="flex justify-center mt-[27px]">
<Button className="baogao-btn" variant="outline" onClick={close}>{t('cancel')}</Button>
<Button className="baogao-btn ml-[27px]" variant="destructive" onClick={handleDelete}>{t('delete')}</Button>
</div>
</form>
</dialog>
</div>
};
@@ -291,4 +313,21 @@ const useTemps = () => {
}, []);
return [temps, loadTemps];
};
};
const useDelete = () => {
const [delShow, setDelShow] = useState(false)
const idRef = useRef<any>(null)
return {
delShow,
idRef,
close: () => {
setDelShow(false)
},
delConfirm: (id) => {
idRef.current = id
setDelShow(true)
}
}
}

View File

@@ -73,7 +73,8 @@ export default function tabTools({ select = null, onSelect }) {
>
{type === "" ? <img src={gongjuIcon1} className="w-[14px]" alt="" /> : <img src={gongjuIcon} className="w-[14px]" alt="" />}
{/* <PersonIcon /> */}
<span className="ml-[8px] text-[#999999]"></span>
<span className={`ml-[8px] text-[#999999] ${type === "" && "text-[#FFD025]"
}`}></span>
</div>
<div
className={`flex cursor-pointer items-center w-[237px] h-[40px] ml-[14px] border-radius-7 pl-[14px] ${type === "edit" && "bg-[#2A271D] text-[#FFD54C]"
@@ -82,7 +83,8 @@ export default function tabTools({ select = null, onSelect }) {
>
{type === "edit" ? <img src={gongjuIcon1} className="w-[14px]" alt="" /> : <img src={gongjuIcon} className="w-[14px]" alt="" />}
{/* <StarFilledIcon /> */}
<span className="ml-[8px] text-[#999999]"></span>
<span className={`ml-[8px] text-[#999999] ${type === "edit" && "text-[#FFD025]"
}`}></span>
</div>
</div>
</div>

View File

@@ -2,8 +2,8 @@ import { Navigate, createBrowserRouter } from "react-router-dom";
import MainLayout from "./layout/MainLayout";
import FileLibPage from "./pages/FileLibPage";
// import FileLibPage from "./pages/Knowledge";
// import FilesPage from "./pages/FileLibPage/files";
import FilesPage from "./pages/Knowledge/knowledge";
import FilesPage from "./pages/FileLibPage/files";
// import FilesPage from "./pages/Knowledge/knowledge";
import FlowPage from "./pages/FlowPage";
import ModelPage from "./pages/ModelPage";
import Doc from "./pages/ModelPage/doc";

View File

@@ -33,7 +33,7 @@ const assistantReducer = (state: State, action: Action, data: Partial<AssistantD
const assistantTemp = {
id: 3,
id: "",
name: "",
desc: "",
logo: "",
@@ -49,6 +49,8 @@ const assistantTemp = {
tool_list: [],
flow_list: [],
knowledge_list: [],
avatar_img: "",
avatar_color: "",
}
export const useAssistantStore = create<State & Actions>((set) => ({

View File

@@ -84,6 +84,15 @@
background-size: 30px 100%;
background-repeat: no-repeat;
}
.xinDuiHua-boxR{
width: 30px;
height: 100%;
background-image: url('../assets/npc/border-r.png');
background-size: 30px 100%;
background-repeat: no-repeat;
background-color: rgba(0, 0, 0, 0.8);
opacity: 90%;
}
.xinDuiHua{
display: flex;
padding-top: 21px;
@@ -183,10 +192,24 @@
}
}
.duihua-chat{
width: calc(100% - 288px);
height: 100%;
background-image: url('../assets/chat/duihua-bg.png');
background-size: 30px 100%;
background-repeat: no-repeat;
// background-image: url('../assets/chat/duihua-bg.png');
// background-size: 30px 100%;
// background-repeat: no-repeat;
}
.chatShare{
.duihua-chat{
width: 100%!important;
height: 100%;
}
}
.chatShareM{
.duihua-chat{
width: 100%!important;
height: 100%;
}
}
.duihua-chat-top{
width: 100%;
@@ -195,6 +218,19 @@
justify-content: space-between;
align-items: center;
background: rgba(13, 13, 13, 0.5);
backdrop-filter: blur(10px);
// opacity: 0.8;
// &::before{
// content: "";
// width: 100%;
// height: 100%;
// left: 0;
// top: 0;
// position: absolute;
// background: rgba(13, 13, 13, 0.5);
// backdrop-filter: blur(20px);
// opacity: 0.8;
// }
>div:nth-of-type(1){
display: flex;
align-items: center;
@@ -612,9 +648,8 @@
border: 1px solid #FFD025;
}
.selectNpcFlexbox{
height: 127px;
display: block;
/* padding: 14px; */
padding-bottom: 14px;
>div:nth-of-type(2){
display: flex;
align-items: self-start;
@@ -2602,7 +2637,8 @@
}
}
.selectNpcFlexboxZiDing{
height: 176px;
height: 180px;
position: relative;
>div{
margin-left: 14px;
margin-top: 27px;
@@ -2635,12 +2671,14 @@
}
.selectNpcFlexbox{
width: 100%;
height: 176px;
height: 100%;
min-height: 180px;
// height: 190px;
// display: flex;
// flex-direction: column;
// justify-content:space-between;
/* padding: 14px; */
position: relative;
// position: relative;
padding-bottom: 55px;
>div:nth-of-type(2){
display: flex;
@@ -2659,12 +2697,13 @@
font-weight: 300;
font-size: 12px;
color: #CCCCCC;
margin-top: 14px;
margin-top: 8px;
}
>div:nth-of-type(3){
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 14px;
position: absolute;
bottom: 14px;
@@ -3353,7 +3392,7 @@
}
}
.selectNpcFlexbox{
// height: 127px;
height: 127px;
padding-bottom: 14px;
display: block;
/* padding: 14px; */
@@ -3921,23 +3960,20 @@
outline: 1px solid #997e1f!important;
}
}
.bs-chat-bg{
.questionTextarea{
width: 95%!important;
margin-left: 2.5%;
height: 40px!important;
background: #1A1A1A!important;
box-shadow: 0px 2px 7px 0px rgba(0,1,51,0.15)!important;
border-radius: 20px!important;
padding: 10px 0;
padding-left: 14px;
font-family: PingFang SC;
font-weight: 400;
font-size: 11px;
color: #FFFFFF;
}
.questionTextarea{
width: 95%!important;
margin-left: 2.5%;
height: 40px!important;
background: #1A1A1A!important;
box-shadow: 0px 2px 7px 0px rgba(0,1,51,0.15)!important;
border-radius: 20px!important;
padding: 10px 0;
padding-left: 14px;
font-family: PingFang SC;
font-weight: 400;
font-size: 11px;
color: #FFFFFF;
}
.skillSheet{
height: 100%;
padding: 0 27px 30px 27px;
@@ -4090,4 +4126,29 @@
}
}
}
::-webkit-scrollbar {
width: 0;
}
::-webkit-scrollbar-horizontal {
display: none;
}
::-webkit-scrollbar-thumb {
display: none;
}
.SelectTrigger{
background: rgba(255, 255, 255, 0.05);
border-radius: 14px!important;
border: none;
&:focus{
// border: 1px solid #997e1f!important;
border: none!important;
outline: none!important;
}
}
.yichu{
width: calc(100% - 110px); /* 定义容器宽度 */
white-space: nowrap; /* 确保文本在一行内显示 */
overflow: hidden; /* 隐藏溢出的内容 */
text-overflow: ellipsis; /* 使用省略号表示文本溢出 */
}

View File

@@ -33,6 +33,8 @@ export interface AssistantDetail {
flow_list?: FlowType[];
/** 知识库ID列表为None则不更新 */
knowledge_list?: { id: number, name: string, index_name: string }[];
avatar_img?: string;
avatar_color?: string;
}