|
@@ -5,66 +5,120 @@ import {
|
|
|
Sender,
|
|
|
Suggestion,
|
|
|
XProvider,
|
|
|
- useXAgent,
|
|
|
- useXChat,
|
|
|
Welcome,
|
|
|
Attachments,
|
|
|
AttachmentsProps,
|
|
|
} from "@ant-design/x";
|
|
|
|
|
|
-import { Card, Divider, Flex, App, Button } from "antd";
|
|
|
-import React from "react";
|
|
|
+import {
|
|
|
+ Card,
|
|
|
+ Divider,
|
|
|
+ Flex,
|
|
|
+ message,
|
|
|
+ Button,
|
|
|
+ Space,
|
|
|
+ Spin,
|
|
|
+ Typography,
|
|
|
+} from "antd";
|
|
|
+import {
|
|
|
+ CSSProperties,
|
|
|
+ ReactNode,
|
|
|
+ useEffect,
|
|
|
+ useMemo,
|
|
|
+ useRef,
|
|
|
+ useState,
|
|
|
+} from "react";
|
|
|
import {
|
|
|
BulbOutlined,
|
|
|
SmileOutlined,
|
|
|
UserOutlined,
|
|
|
EditOutlined,
|
|
|
DeleteOutlined,
|
|
|
- MessageOutlined,
|
|
|
PlusOutlined,
|
|
|
CloudUploadOutlined,
|
|
|
LinkOutlined,
|
|
|
+ CopyOutlined,
|
|
|
+ RedoOutlined,
|
|
|
} from "@ant-design/icons";
|
|
|
import type { GetProp, GetRef } from "antd";
|
|
|
import type { ConversationsProps } from "@ant-design/x";
|
|
|
import type { AgentItem } from "./data";
|
|
|
+import MarkdownViewer from "@/components/ai/MarkdownViewer";
|
|
|
+import { useModel } from "umi";
|
|
|
+import { useSessionStorageState } from "ahooks";
|
|
|
+
|
|
|
+// 消息类型
|
|
|
+type MessageItem = {
|
|
|
+ id: string;
|
|
|
+ content: string;
|
|
|
+ role: "user" | "assistant" | "system";
|
|
|
+ status: "loading" | "done" | "error" | "stop";
|
|
|
+ loading?: boolean;
|
|
|
+ footer?: ReactNode;
|
|
|
+};
|
|
|
+
|
|
|
+type AssistantProps = {
|
|
|
+ agent?: AgentItem;
|
|
|
+};
|
|
|
|
|
|
+// bubbles角色配置
|
|
|
const roles: GetProp<typeof Bubble.List, "roles"> = {
|
|
|
- assient: {
|
|
|
+ assistant: {
|
|
|
placement: "start",
|
|
|
avatar: {
|
|
|
icon: <i className="iconfont icon-AI1" />,
|
|
|
style: { background: "#fde3cf" },
|
|
|
},
|
|
|
+ typing: { step: 5, interval: 20 },
|
|
|
+ loadingRender: () => (
|
|
|
+ <Space>
|
|
|
+ <Spin size="small" />
|
|
|
+ 思考中...
|
|
|
+ </Space>
|
|
|
+ ),
|
|
|
+ messageRender: (content) => {
|
|
|
+ return content ? (
|
|
|
+ <Typography>
|
|
|
+ <MarkdownViewer content={content} />
|
|
|
+ </Typography>
|
|
|
+ ) : (
|
|
|
+ <></>
|
|
|
+ );
|
|
|
+ },
|
|
|
+ header: "易码工坊AI助手",
|
|
|
},
|
|
|
user: {
|
|
|
placement: "end",
|
|
|
avatar: { icon: <UserOutlined />, style: { background: "#87d068" } },
|
|
|
},
|
|
|
};
|
|
|
-type AssistantProps = {
|
|
|
- agent?: AgentItem;
|
|
|
-};
|
|
|
-export default (props: AssistantProps) => {
|
|
|
- const [value, setValue] = React.useState("");
|
|
|
- const { message } = App.useApp();
|
|
|
|
|
|
- const [agent] = useXAgent<{ role: string; content: string }>({
|
|
|
- baseURL: "http://localhost:3000/ai/chat",
|
|
|
- });
|
|
|
+const defaultConversation = {
|
|
|
+ // 会话id
|
|
|
+ key: "1",
|
|
|
+ label: "新的对话",
|
|
|
+};
|
|
|
|
|
|
- const { onRequest, messages } = useXChat({ agent });
|
|
|
+export default (props: AssistantProps) => {
|
|
|
+ const { agent, loading, cancel } = useModel("aiModel");
|
|
|
+ const [senderVal, setSenderVal] = useState("");
|
|
|
+ const [messages, setMessages] = useState<Array<MessageItem>>([]);
|
|
|
+ const [conversationList, setConversationList] = useState<
|
|
|
+ ConversationsProps["items"]
|
|
|
+ >([{ ...defaultConversation }]);
|
|
|
+ const [activeConversation, setActiveConversation] = useState("1");
|
|
|
|
|
|
+ const [currentAgent, setCurrentAgent] = useSessionStorageState("agent-map");
|
|
|
const menuConfig: ConversationsProps["menu"] = (conversation) => ({
|
|
|
items: [
|
|
|
{
|
|
|
label: "修改对话名称",
|
|
|
- key: "operation1",
|
|
|
+ key: "edit",
|
|
|
icon: <EditOutlined />,
|
|
|
},
|
|
|
{
|
|
|
label: "删除对话",
|
|
|
- key: "operation3",
|
|
|
+ key: "del",
|
|
|
icon: <DeleteOutlined />,
|
|
|
danger: true,
|
|
|
},
|
|
@@ -74,11 +128,31 @@ export default (props: AssistantProps) => {
|
|
|
},
|
|
|
});
|
|
|
|
|
|
- const [openAttachment, setOpenAttachment] = React.useState(false);
|
|
|
- const attachmentsRef = React.useRef<GetRef<typeof Attachments>>(null);
|
|
|
- const senderRef = React.useRef<GetRef<typeof Sender>>(null);
|
|
|
- const [attachmentItems, setAttachmentItems] = React.useState<GetProp<AttachmentsProps, 'items'>>([]);
|
|
|
+ const [openAttachment, setOpenAttachment] = useState(false);
|
|
|
+ const attachmentsRef = useRef<GetRef<typeof Attachments>>(null);
|
|
|
+ const senderRef = useRef<GetRef<typeof Sender>>(null);
|
|
|
+ const [attachmentItems, setAttachmentItems] = useState<
|
|
|
+ GetProp<AttachmentsProps, "items">
|
|
|
+ >([]);
|
|
|
+ const contentRef = useRef<HTMLDivElement>(null);
|
|
|
+
|
|
|
+ const contentStyle = useMemo((): CSSProperties => {
|
|
|
+ if (!contentRef.current) return {};
|
|
|
+ return {
|
|
|
+ maxHeight: contentRef.current?.clientHeight,
|
|
|
+ overflowY: "auto",
|
|
|
+ };
|
|
|
+ }, [contentRef.current]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ // 切换类型时清除回话 加载回话列表
|
|
|
+ if (agent) {
|
|
|
+ setMessages([]);
|
|
|
+ setActiveConversation("1");
|
|
|
+ }
|
|
|
+ }, [props.agent]);
|
|
|
|
|
|
+ // 附件组件
|
|
|
const senderHeader = (
|
|
|
<Sender.Header
|
|
|
title="附件"
|
|
@@ -93,7 +167,6 @@ export default (props: AssistantProps) => {
|
|
|
>
|
|
|
<Attachments
|
|
|
ref={attachmentsRef}
|
|
|
- // Mock not real upload file
|
|
|
beforeUpload={() => false}
|
|
|
items={attachmentItems}
|
|
|
onChange={({ fileList }) => setAttachmentItems(fileList)}
|
|
@@ -113,12 +186,170 @@ export default (props: AssistantProps) => {
|
|
|
</Sender.Header>
|
|
|
);
|
|
|
|
|
|
- const submitMessage = (message: string) => {
|
|
|
- // onRequest({ role: "user", content: message });
|
|
|
+ // 底部组件
|
|
|
+ const BubbleFooter = (props: { content: string; query: string }) => {
|
|
|
+ const handleCopy = () => {
|
|
|
+ navigator.clipboard.writeText(props.content);
|
|
|
+ message.success("复制成功");
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleRedo = () => {
|
|
|
+ submitMessage(props.query);
|
|
|
+ };
|
|
|
+ return (
|
|
|
+ <Space>
|
|
|
+ <Button
|
|
|
+ type="text"
|
|
|
+ size="small"
|
|
|
+ icon={<CopyOutlined />}
|
|
|
+ onClick={handleCopy}
|
|
|
+ >
|
|
|
+ 复制
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ type="text"
|
|
|
+ size="small"
|
|
|
+ icon={<RedoOutlined />}
|
|
|
+ onClick={handleRedo}
|
|
|
+ >
|
|
|
+ 重新生成
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ // 发起请求
|
|
|
+ const onRequest = (message: string) => {
|
|
|
+ agent.request(
|
|
|
+ {
|
|
|
+ app_name: "app1",
|
|
|
+ chat_name: activeConversation === "1" ? "新会话" : undefined,
|
|
|
+ chat_query: message,
|
|
|
+ conversation_id:
|
|
|
+ activeConversation === "1" ? undefined : activeConversation,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ onSuccess: (msg) => {
|
|
|
+ console.log("success", msg);
|
|
|
+ setMessages((messages) => {
|
|
|
+ const arr = [...messages];
|
|
|
+ const query = arr[messages.length - 2].content;
|
|
|
+ arr[messages.length - 1].status = "done";
|
|
|
+ arr[messages.length - 1].footer = (
|
|
|
+ <BubbleFooter
|
|
|
+ content={arr[messages.length - 1].content}
|
|
|
+ query={query}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ return arr;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ console.log("err:", error);
|
|
|
+
|
|
|
+ },
|
|
|
+ onUpdate: (msg) => {
|
|
|
+ console.log("update", msg);
|
|
|
+ setMessages((messages) => {
|
|
|
+ const arr = [...messages];
|
|
|
+ arr[messages.length - 1].content += msg.answer;
|
|
|
+ arr[messages.length - 1].id = msg.message_id;
|
|
|
+ arr[messages.length - 1].loading = false;
|
|
|
+ // 第一次提交后保存会话记录
|
|
|
+ if (
|
|
|
+ !conversationList?.find(
|
|
|
+ (item) => item.key === msg.conversation_id
|
|
|
+ )
|
|
|
+ ) {
|
|
|
+ setConversationList((list) => {
|
|
|
+ return list?.map((item) => {
|
|
|
+ return {
|
|
|
+ ...item,
|
|
|
+ key: msg.conversation_id,
|
|
|
+ label: message,
|
|
|
+ };
|
|
|
+ });
|
|
|
+ });
|
|
|
+ setActiveConversation(msg.conversation_id);
|
|
|
+ }
|
|
|
+
|
|
|
+ return arr;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ }
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ // 提交消息
|
|
|
+ const submitMessage = (msg: string) => {
|
|
|
+ setSenderVal("");
|
|
|
+
|
|
|
+ setMessages((arr) => {
|
|
|
+ const index = arr.length;
|
|
|
+ return [
|
|
|
+ ...arr,
|
|
|
+ { id: index + "", content: msg, status: "done", role: "user" },
|
|
|
+ {
|
|
|
+ id: index + 1 + "",
|
|
|
+ content: "",
|
|
|
+ status: "loading",
|
|
|
+ role: "assistant",
|
|
|
+ loading: true,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+ });
|
|
|
+ onRequest(msg);
|
|
|
};
|
|
|
|
|
|
+ // 点击提示词
|
|
|
const handlePromptItem = (item: any) => {
|
|
|
- // onRequest({ role: "user", content: item.data.description });
|
|
|
+ console.log(item);
|
|
|
+ const msg = item.data.description || item.data.label;
|
|
|
+ const index = messages.length;
|
|
|
+ setMessages([
|
|
|
+ ...messages,
|
|
|
+ { id: index + "", content: msg, status: "done", role: "user" },
|
|
|
+ {
|
|
|
+ id: index + 1 + "",
|
|
|
+ content: "",
|
|
|
+ status: "loading",
|
|
|
+ role: "assistant",
|
|
|
+ loading: true,
|
|
|
+ },
|
|
|
+ ]);
|
|
|
+ onRequest(msg);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 停止对话
|
|
|
+ const handleStop = () => {
|
|
|
+ cancel();
|
|
|
+ setMessages((messages) => {
|
|
|
+ const arr = [...messages];
|
|
|
+ arr[messages.length - 1].status = "stop";
|
|
|
+ arr[messages.length - 1].loading = false;
|
|
|
+ arr[messages.length - 1].footer = (
|
|
|
+ <div>
|
|
|
+ <div className="text-12px text-text-secondary">已停止思考</div>
|
|
|
+ <BubbleFooter content={arr[messages.length - 1].content} query={arr[messages.length - 2].content}/>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ return arr;
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // 新增会话
|
|
|
+ const handleAddConversation = () => {
|
|
|
+ setMessages([]);
|
|
|
+ // 还没产生对话时 直接清除当前对话
|
|
|
+ if (!conversationList?.find((item) => item.key === "1")) {
|
|
|
+ setConversationList([
|
|
|
+ {
|
|
|
+ ...defaultConversation,
|
|
|
+ },
|
|
|
+ ...(conversationList || []),
|
|
|
+ ]);
|
|
|
+ setActiveConversation("1");
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
return (
|
|
@@ -148,6 +379,7 @@ export default (props: AssistantProps) => {
|
|
|
type="primary"
|
|
|
className="w-full"
|
|
|
icon={<PlusOutlined />}
|
|
|
+ onClick={handleAddConversation}
|
|
|
>
|
|
|
新对话
|
|
|
</Button>
|
|
@@ -155,19 +387,15 @@ export default (props: AssistantProps) => {
|
|
|
<Conversations
|
|
|
style={{ width: 200 }}
|
|
|
defaultActiveKey="1"
|
|
|
+ activeKey={activeConversation}
|
|
|
+ onActiveChange={setActiveConversation}
|
|
|
menu={menuConfig}
|
|
|
- items={[
|
|
|
- {
|
|
|
- key: "1",
|
|
|
- label: "新的对话",
|
|
|
- icon: <MessageOutlined />,
|
|
|
- },
|
|
|
- ]}
|
|
|
+ items={conversationList}
|
|
|
/>
|
|
|
</div>
|
|
|
<Divider type="vertical" style={{ height: "100%" }} />
|
|
|
<Flex vertical style={{ flex: 1 }} gap={8}>
|
|
|
- <div className="flex-1">
|
|
|
+ <div className="flex-1" ref={contentRef} style={contentStyle}>
|
|
|
{!messages.length ? (
|
|
|
<>
|
|
|
<div className="mt-20 mb-10">
|
|
@@ -193,19 +421,7 @@ export default (props: AssistantProps) => {
|
|
|
/>
|
|
|
</>
|
|
|
) : (
|
|
|
- <Bubble.List
|
|
|
- autoScroll
|
|
|
- roles={roles}
|
|
|
- items={
|
|
|
- []
|
|
|
- // messages.map(({ id, message, status }) => ({
|
|
|
- // key: id,
|
|
|
- // loading: status === "loading",
|
|
|
- // role: status === "user" ? "local" : "ai",
|
|
|
- // content: message,
|
|
|
- // }))
|
|
|
- }
|
|
|
- />
|
|
|
+ <Bubble.List autoScroll roles={roles} items={messages} />
|
|
|
)}
|
|
|
</div>
|
|
|
<Prompts
|
|
@@ -213,12 +429,17 @@ export default (props: AssistantProps) => {
|
|
|
{
|
|
|
key: "1",
|
|
|
icon: <BulbOutlined style={{ color: "#FFD700" }} />,
|
|
|
- label: "一句话",
|
|
|
+ label: "写需求",
|
|
|
},
|
|
|
{
|
|
|
key: "2",
|
|
|
icon: <SmileOutlined style={{ color: "#52C41A" }} />,
|
|
|
- label: "自动生成",
|
|
|
+ label: "生成代码",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: "3",
|
|
|
+ icon: <SmileOutlined style={{ color: "#52C41A" }} />,
|
|
|
+ label: "问题解答",
|
|
|
},
|
|
|
]}
|
|
|
onItemClick={handlePromptItem}
|
|
@@ -233,6 +454,7 @@ export default (props: AssistantProps) => {
|
|
|
<Sender
|
|
|
ref={senderRef}
|
|
|
header={senderHeader}
|
|
|
+ loading={loading}
|
|
|
prefix={
|
|
|
<Button
|
|
|
type="text"
|
|
@@ -242,7 +464,7 @@ export default (props: AssistantProps) => {
|
|
|
}}
|
|
|
/>
|
|
|
}
|
|
|
- value={value}
|
|
|
+ value={senderVal}
|
|
|
onPasteFile={(file) => {
|
|
|
attachmentsRef.current?.upload(file);
|
|
|
setOpenAttachment(true);
|
|
@@ -253,11 +475,12 @@ export default (props: AssistantProps) => {
|
|
|
} else if (!nextVal) {
|
|
|
onTrigger(false);
|
|
|
}
|
|
|
- setValue(nextVal);
|
|
|
+ setSenderVal(nextVal);
|
|
|
}}
|
|
|
onKeyDown={onKeyDown}
|
|
|
placeholder="输入/获取快捷提示"
|
|
|
onSubmit={submitMessage}
|
|
|
+ onCancel={handleStop}
|
|
|
/>
|
|
|
);
|
|
|
}}
|