|
@@ -1,9 +1,397 @@
|
|
|
-import React from 'react'
|
|
|
+import {
|
|
|
+ Bubble,
|
|
|
+ Prompts,
|
|
|
+ Sender,
|
|
|
+ Suggestion,
|
|
|
+ XProvider,
|
|
|
+ Welcome,
|
|
|
+ Attachments,
|
|
|
+ AttachmentsProps,
|
|
|
+} from "@ant-design/x";
|
|
|
+import { useChat } from "@/hooks/useChat";
|
|
|
+
|
|
|
+import {
|
|
|
+ Card,
|
|
|
+ Flex,
|
|
|
+ message,
|
|
|
+ Button,
|
|
|
+ Space,
|
|
|
+ Spin,
|
|
|
+ Typography,
|
|
|
+} from "antd";
|
|
|
+import { useEffect, useRef, useState } from "react";
|
|
|
+import {
|
|
|
+ BulbOutlined,
|
|
|
+ SmileOutlined,
|
|
|
+ UserOutlined,
|
|
|
+ CloudUploadOutlined,
|
|
|
+ LinkOutlined,
|
|
|
+ CopyOutlined,
|
|
|
+ RedoOutlined,
|
|
|
+ ReadOutlined,
|
|
|
+} from "@ant-design/icons";
|
|
|
+import type { GetProp, GetRef } from "antd";
|
|
|
+import MarkdownViewer from "@/components/ai/MarkdownViewer";
|
|
|
+
|
|
|
+// bubbles角色配置
|
|
|
+const roles: GetProp<typeof Bubble.List, "roles"> = {
|
|
|
+ assistant: {
|
|
|
+ placement: "start",
|
|
|
+ avatar: {
|
|
|
+ icon: <i className="iconfont icon-AI1" />,
|
|
|
+ style: { background: "#3450ec" },
|
|
|
+ },
|
|
|
+ // typing: { step: 5, interval: 20 },
|
|
|
+ loadingRender: () => (
|
|
|
+ <Space>
|
|
|
+ <Spin size="small" />
|
|
|
+ 思考中...
|
|
|
+ </Space>
|
|
|
+ ),
|
|
|
+ messageRender: (content) => {
|
|
|
+ return typeof content === "string" ? (
|
|
|
+ <Typography className={content?.includes("```") ? "w-full" : ""}>
|
|
|
+ <MarkdownViewer content={content} />
|
|
|
+ </Typography>
|
|
|
+ ) : (
|
|
|
+ content
|
|
|
+ );
|
|
|
+ },
|
|
|
+ header: "易码工坊AI助手",
|
|
|
+ },
|
|
|
+ user: {
|
|
|
+ placement: "end",
|
|
|
+ avatar: { icon: <UserOutlined />, style: { background: "#87d068" } },
|
|
|
+ messageRender: (content) => {
|
|
|
+ return <div style={{ whiteSpace: "pre-wrap" }}>{content}</div>;
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+
|
|
|
+export default () => {
|
|
|
+ const [senderVal, setSenderVal] = useState("");
|
|
|
+ const {
|
|
|
+ messages,
|
|
|
+ setMessages,
|
|
|
+ activeConversation,
|
|
|
+ changeConversation,
|
|
|
+ conversationList,
|
|
|
+ onRequest,
|
|
|
+ cancel,
|
|
|
+ loading,
|
|
|
+ loadingSession,
|
|
|
+ addConversation,
|
|
|
+ setConversationList
|
|
|
+ } = useChat({
|
|
|
+ app_name: "normal_ask",
|
|
|
+ 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 [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();
|
|
|
+ const resizeObserver = new ResizeObserver(() => {
|
|
|
+ setHeight();
|
|
|
+ });
|
|
|
+ resizeObserver.observe(contentRef.current!);
|
|
|
+ return () => {
|
|
|
+ resizeObserver.disconnect();
|
|
|
+ };
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 附件组件
|
|
|
+ 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;
|
|
|
+ });
|
|
|
+ };
|
|
|
|
|
|
-export default function index() {
|
|
|
return (
|
|
|
- <div>
|
|
|
-
|
|
|
- </div>
|
|
|
- )
|
|
|
-}
|
|
|
+ <>
|
|
|
+ <Card
|
|
|
+ className="w-full h-full"
|
|
|
+ styles={{
|
|
|
+ body: {
|
|
|
+ height: "calc(100vh - 48px)",
|
|
|
+ },
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <XProvider direction="ltr">
|
|
|
+ <Flex style={{ height: "100%" }} gap={12}>
|
|
|
+ <Flex
|
|
|
+ vertical
|
|
|
+ style={{ flex: 1, overflow: "hidden", height: "100%" }}
|
|
|
+ gap={8}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ className="flex-1 overflow-hidden"
|
|
|
+ ref={contentRef}
|
|
|
+ style={{ height: contentHeight }}
|
|
|
+ >
|
|
|
+ {!messages.length ? (
|
|
|
+ <>
|
|
|
+ <div className="mt-20 mb-10">
|
|
|
+ <Welcome
|
|
|
+ icon={
|
|
|
+ <img
|
|
|
+ src={''}
|
|
|
+ className="rounded-8px"
|
|
|
+ />
|
|
|
+ }
|
|
|
+ title={`你好!我是AI易搭`}
|
|
|
+ description={`我可以帮你一句话生成应用`}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Prompts
|
|
|
+ title={
|
|
|
+ "✨ 你可以这样问我:"
|
|
|
+ }
|
|
|
+ items={[]}
|
|
|
+ wrap
|
|
|
+ onItemClick={handlePromptItem}
|
|
|
+ />
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <Bubble.List
|
|
|
+ style={{ maxHeight: contentHeight, padding: '0 20px' }}
|
|
|
+ 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>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+};
|