123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- import {
- Bubble,
- Conversations,
- Prompts,
- Sender,
- Suggestion,
- XProvider,
- Welcome,
- Attachments,
- AttachmentsProps,
- } from "@ant-design/x";
- import { Conversation } from "@ant-design/x/lib/conversations";
- import { useChat } from "@/hooks/useChat";
- import {
- Card,
- Divider,
- Flex,
- message,
- Button,
- Space,
- Spin,
- Typography,
- Modal,
- Input,
- } from "antd";
- import { useEffect, useRef, useState } from "react";
- import {
- BulbOutlined,
- SmileOutlined,
- UserOutlined,
- EditOutlined,
- DeleteOutlined,
- PlusOutlined,
- CloudUploadOutlined,
- LinkOutlined,
- CopyOutlined,
- RedoOutlined,
- ReadOutlined,
- } 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 { ChangeSessionName, DeleteSession } from "@/api/ai";
- type AssistantProps = {
- agent?: AgentItem;
- };
- // bubbles角色配置
- const roles: GetProp<typeof Bubble.List, "roles"> = {
- 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 typeof content === "string" ? (
- <Typography>
- <MarkdownViewer content={content} />
- </Typography>
- ) : (
- content
- );
- },
- header: "易码工坊AI助手",
- },
- user: {
- placement: "end",
- avatar: { icon: <UserOutlined />, style: { background: "#87d068" } },
- },
- };
- export default (props: AssistantProps) => {
- const [senderVal, setSenderVal] = useState("");
- const {
- messages,
- setMessages,
- activeConversation,
- changeConversation,
- conversationList,
- onRequest,
- cancel,
- loading,
- loadingSession,
- addConversation,
- setConversationList,
- } = useChat({
- app_name: props.agent?.key || "",
- onSuccess: (msg) => {
- setMessages((messages) => {
- const arr = [...messages];
- const query = arr[messages.length - 2].content as string;
- arr[messages.length - 1].status = "done";
- arr[messages.length - 1].footer = (
- <BubbleFooter
- content={arr[messages.length - 1].content as string}
- query={query}
- />
- );
- return arr;
- });
- },
- onUpdate: (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;
- arr[messages.length - 1].status = "error";
- return arr;
- });
- },
- onError: (error) => {
- message.error(error.message);
- setMessages((messages) => {
- const arr = [...messages];
- arr[messages.length - 1].content = (
- <Typography.Text type="danger">{error.message}</Typography.Text>
- );
- arr[messages.length - 1].status = "error";
- arr[messages.length - 1].loading = false;
- return arr;
- });
- },
- });
- const handleChangeConversationName = (conversation: Conversation) => {
- let new_name: string;
- Modal.info({
- title: "修改对话名称",
- okText: "提交",
- closable: true,
- content: (
- <div>
- <Input
- type="text"
- defaultValue={conversation.label + ""}
- onChange={(e) => {
- new_name = e.target.value;
- }}
- />
- </div>
- ),
- onOk: () => {
- return ChangeSessionName({
- app_name: props.agent?.key || "",
- session_id: conversation.key,
- new_name,
- }).then(() => {
- message.success("修改成功");
- setConversationList((list) =>
- list?.map((item) =>
- item.key === conversation.key
- ? { ...item, label: new_name }
- : item
- )
- );
- });
- },
- });
- };
- const handleDeleteConversation = (conversation: Conversation) => {
- Modal.confirm({
- title: "删除对话",
- content: "是否删除对话?",
- okText: "删除",
- cancelText: "取消",
- onOk: () => {
- return DeleteSession({
- app_name: props.agent?.key || "",
- session_id: conversation.key,
- }).then(() => {
- message.success("删除成功");
- setConversationList((list) =>
- list?.filter((item) => item.key !== conversation.key)
- );
- addConversation();
- });
- },
- });
- };
- const menuConfig: ConversationsProps["menu"] = (conversation) => {
- if (conversation.key === "1") return undefined;
- return {
- items: [
- {
- label: "修改对话名称",
- key: "edit",
- icon: <EditOutlined />,
- },
- {
- label: "删除对话",
- key: "del",
- icon: <DeleteOutlined />,
- danger: true,
- },
- ],
- onClick: (menuInfo) => {
- // 修改对话名称
- if (menuInfo.key === "edit") {
- handleChangeConversationName(conversation);
- }
- // 删除对话
- if (menuInfo.key === "del") {
- handleDeleteConversation(conversation);
- }
- },
- };
- };
- 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 [contentHeight, setContentHeight] = useState(0);
- const setHeight = () => {
- setContentHeight(contentRef.current?.clientHeight || 0);
- };
- useEffect(() => {
- setHeight();
- window.addEventListener("resize", setHeight);
- return () => {
- window.removeEventListener("resize", setHeight);
- };
- }, []);
- // 附件组件
- const senderHeader = (
- <Sender.Header
- title="附件"
- styles={{
- content: {
- padding: 0,
- },
- }}
- open={openAttachment}
- onOpenChange={setOpenAttachment}
- forceRender
- >
- <Attachments
- ref={attachmentsRef}
- beforeUpload={() => false}
- items={attachmentItems}
- onChange={({ fileList }) => setAttachmentItems(fileList)}
- placeholder={(type) =>
- type === "drop"
- ? {
- title: "拖拽文件到这里",
- }
- : {
- icon: <CloudUploadOutlined />,
- title: "文件列表",
- description: "点击或者拖拽文件到这里上传",
- }
- }
- getDropContainer={() => senderRef.current?.nativeElement}
- />
- </Sender.Header>
- );
- // 底部组件
- 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 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) => {
- 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 pl-12px">(已停止思考)</div>
- <BubbleFooter
- content={arr[messages.length - 1].content as string}
- query={arr[messages.length - 2].content as string}
- />
- </div>
- );
- return arr;
- });
- };
- return (
- <>
- <Card
- className="w-full h-full"
- styles={{
- body: {
- height: "calc(100% - 48px)",
- },
- }}
- title={
- <span className="flex items-center">
- <img
- className="w-20px h-20px rounded-8px"
- src={props.agent?.icon}
- />
- <span className="ml-4px">{props.agent?.name}</span>
- </span>
- }
- >
- <XProvider direction="ltr">
- <Flex style={{ height: "100%" }} gap={12}>
- <Spin spinning={loadingSession}>
- <div className="w-200px">
- <div className="w-full px-12px">
- <Button
- type="primary"
- className="w-full"
- icon={<PlusOutlined />}
- onClick={addConversation}
- >
- 新对话
- </Button>
- </div>
- <Conversations
- style={{ width: 200 }}
- activeKey={activeConversation}
- onActiveChange={changeConversation}
- menu={menuConfig}
- items={conversationList}
- />
- </div>
- </Spin>
- <Divider type="vertical" style={{ height: "100%" }} />
- <Flex vertical style={{ flex: 1 }} gap={8}>
- <div
- className="flex-1"
- ref={contentRef}
- style={{ height: contentHeight }}
- >
- {!messages.length ? (
- <>
- <div className="mt-20 mb-10">
- <Welcome
- icon={
- <img
- src={props.agent?.icon}
- className="rounded-8px"
- />
- }
- title={`你好!我是易码工坊${props.agent?.name || "AI"}助手`}
- description={props.agent?.description}
- />
- </div>
- <Prompts
- title={
- props.agent?.promptsItems ? "✨ 你可以这样问我:" : ""
- }
- items={props.agent?.promptsItems || []}
- wrap
- onItemClick={handlePromptItem}
- />
- </>
- ) : (
- <Bubble.List
- style={{ maxHeight: contentHeight }}
- autoScroll
- roles={roles}
- items={messages}
- />
- )}
- </div>
- <Prompts
- items={[
- {
- key: "1",
- icon: <BulbOutlined style={{ color: "#FFD700" }} />,
- label: "写需求",
- },
- {
- key: "2",
- icon: <SmileOutlined style={{ color: "#52C41A" }} />,
- label: "生成代码",
- },
- {
- key: "3",
- icon: <ReadOutlined style={{ color: "#52C41A" }} />,
- label: "问题解答",
- },
- ]}
- onItemClick={handlePromptItem}
- />
- <Suggestion
- items={[{ label: "写一个应用介绍", value: "report" }]}
- onSelect={submitMessage}
- >
- {({ onTrigger, onKeyDown }) => {
- return (
- <Sender
- ref={senderRef}
- header={senderHeader}
- loading={loading}
- prefix={
- <Button
- type="text"
- icon={<LinkOutlined />}
- onClick={() => {
- setOpenAttachment(!openAttachment);
- }}
- />
- }
- value={senderVal}
- onPasteFile={(file) => {
- attachmentsRef.current?.upload(file);
- setOpenAttachment(true);
- }}
- onChange={(nextVal) => {
- if (nextVal === "/") {
- onTrigger();
- } else if (!nextVal) {
- onTrigger(false);
- }
- setSenderVal(nextVal);
- }}
- onKeyDown={onKeyDown}
- placeholder="输入/获取快捷提示"
- onSubmit={submitMessage}
- onCancel={handleStop}
- />
- );
- }}
- </Suggestion>
- </Flex>
- </Flex>
- </XProvider>
- </Card>
- </>
- );
- };
|