李富豪 3 mesiacov pred
rodič
commit
5cbffe5aba

+ 1 - 1
env/.env.development

@@ -2,4 +2,4 @@
 VITE_ENV = 'development'
 
 # Api地址
-VITE_API_URL = 'http://192.168.3.3:8091'
+VITE_API_URL = 'http://192.168.3.123:8091'

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 744 - 60
package-lock.json


+ 3 - 0
package.json

@@ -12,13 +12,16 @@
     "build:prod": "vite build --mode production"
   },
   "dependencies": {
+    "@ant-design/x": "^1.6.0",
     "antd": "^5.23.0",
+    "antd-style": "^3.7.1",
     "axios": "1.8.2",
     "dayjs": "^1.11.0",
     "mobx": "^6.13.0",
     "mobx-react": "^9.2.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
+    "react-markdown": "^10.1.0",
     "react-router-dom": "^7.1.0"
   },
   "devDependencies": {

+ 535 - 0
src/components/chat/index.tsx

@@ -0,0 +1,535 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { Button, GetProp, GetRef, Popover, Space, Spin, message } from 'antd';
+import {
+    CloseOutlined,
+    CloudUploadOutlined,
+    CommentOutlined,
+    OpenAIFilled,
+    PlusOutlined,
+} from '@ant-design/icons';
+import {
+    Attachments,
+    type AttachmentsProps,
+    Bubble,
+    Conversations,
+    Sender,
+    Suggestion,
+    Welcome,
+    useXAgent,
+    useXChat,
+} from '@ant-design/x';
+import type { Conversation } from '@ant-design/x/es/conversations';
+import { createStyles } from 'antd-style';
+import dayjs from 'dayjs';
+import ReactMarkdown from 'react-markdown';
+
+type BubbleDataType = {
+    role: string;
+    content: string;
+};
+
+const MOCK_SESSION_LIST = [
+    {
+        key: '5',
+        label: 'New session',
+        group: 'Today',
+    },
+    {
+        key: '4',
+        label: 'What has Ant Design X upgraded?',
+        group: 'Today',
+    },
+    {
+        key: '3',
+        label: 'New AGI Hybrid Interface',
+        group: 'Today',
+    },
+    {
+        key: '2',
+        label: 'How to quickly install and import components?',
+        group: 'Yesterday',
+    },
+    {
+        key: '1',
+        label: 'What is Ant Design X?',
+        group: 'Yesterday',
+    },
+];
+
+const MOCK_SUGGESTIONS = [
+    { label: 'Write a report', value: 'report' },
+    { label: 'Draw a picture', value: 'draw' },
+    {
+        label: 'Check some knowledge',
+        value: 'knowledge',
+        icon: <OpenAIFilled />,
+        children: [
+            { label: 'About React', value: 'react' },
+            { label: 'About Ant Design', value: 'antd' },
+        ],
+    },
+];
+
+const AGENT_PLACEHOLDER = 'Generating content, please wait...';
+
+const useCopilotStyle = createStyles(({ token, css }) => {
+    return {
+        copilotChat: css`
+      display: flex;
+      flex-direction: column;
+      background: ${token.colorBgContainer};
+      color: ${token.colorText};
+    `,
+        // chatHeader 样式
+        chatHeader: css`
+      height: 52px;
+      box-sizing: border-box;
+      border-bottom: 1px solid ${token.colorBorder};
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 0 10px 0 16px;
+    `,
+        headerTitle: css`
+      font-weight: 600;
+      font-size: 15px;
+    `,
+        headerButton: css`
+      font-size: 18px;
+    `,
+        conversations: css`
+      width: 300px;
+      .ant-conversations-list {
+        padding-inline-start: 0;
+      }
+    `,
+        // chatList 样式
+        chatList: css`
+      overflow: auto;
+      padding-block: 16px;
+      flex: 1;
+    `,
+        chatWelcome: css`
+      margin-inline: 16px;
+      padding: 12px 16px;
+      border-radius: 2px 12px 12px 12px;
+      background: ${token.colorBgTextHover};
+      margin-bottom: 16px;
+    `,
+        loadingMessage: css`
+      background-image: linear-gradient(90deg, #ff6b23 0%, #af3cb8 31%, #53b6ff 89%);
+      background-size: 100% 2px;
+      background-repeat: no-repeat;
+      background-position: bottom;
+    `,
+        // chatSend 样式
+        chatSend: css`
+      padding: 12px;
+    `,
+        sendAction: css`
+      display: flex;
+      align-items: center;
+      margin-bottom: 12px;
+      gap: 8px;
+    `,
+        speechButton: css`
+      font-size: 18px;
+      color: ${token.colorText} !important;
+    `,
+    };
+});
+
+interface CopilotProps {
+    copilotOpen: boolean;
+    setCopilotOpen: (open: boolean) => void;
+}
+
+const Copilot = (props: CopilotProps) => {
+    const { copilotOpen, setCopilotOpen } = props;
+    const { styles } = useCopilotStyle();
+    const attachmentsRef = useRef<GetRef<typeof Attachments>>(null);
+    const abortController = useRef<AbortController>(null);
+
+    // ==================== State ====================
+
+    const [messageHistory, setMessageHistory] = useState<Record<string, any>>({});
+
+    const [sessionList, setSessionList] = useState<Conversation[]>(MOCK_SESSION_LIST);
+    const [curSession, setCurSession] = useState(sessionList[0].key);
+
+    const [attachmentsOpen, setAttachmentsOpen] = useState(false);
+    const [files, setFiles] = useState<GetProp<AttachmentsProps, 'items'>>([]);
+
+    const [inputValue, setInputValue] = useState('');
+
+    // 在这里切换请求
+    const [agent] = useXAgent<BubbleDataType>({
+        baseURL: '/api/deepseek/api/chat',
+    });
+
+    const loading = agent.isRequesting();
+
+    const { messages, onRequest, setMessages } = useXChat({
+        agent,
+        requestFallback: (_, { error }) => {
+            if (error.name === 'AbortError') {
+                return {
+                    content: 'Request is aborted',
+                    role: 'assistant',
+                };
+            }
+            return {
+                content: 'Request failed, please try again!',
+                role: 'assistant',
+            };
+        },
+        transformMessage: (info) => {
+            const { originMessage, chunk } = info || {};
+            let currentContent = '';
+            let currentThink = '';
+            try {
+                if (chunk?.data && !chunk?.data.includes('DONE')) {
+                    const message = JSON.parse(chunk?.data);
+                    currentThink = message?.choices?.[0]?.delta?.reasoning_content || '';
+                    currentContent = message?.choices?.[0]?.delta?.content || '';
+                }
+            } catch (error) {
+                console.error(error);
+            }
+
+            let content = '';
+
+            if (!originMessage?.content && currentThink) {
+                content = `<think>${currentThink}`;
+            } else if (
+                originMessage?.content?.includes('<think>') &&
+                !originMessage?.content.includes('</think>') &&
+                currentContent
+            ) {
+                content = `${originMessage?.content}</think>${currentContent}`;
+            } else {
+                content = `${originMessage?.content || ''}${currentThink}${currentContent}`;
+            }
+
+            return {
+                content: content,
+                role: 'assistant',
+            };
+        },
+        resolveAbortController: (controller) => {
+            abortController.current = controller;
+        },
+    });
+    // ==================== Event ====================
+    const handleUserSubmit = (val: string) => {
+        onRequest({
+            message: { content: val, role: 'user' },
+            appId: '2942534530212696064',
+            // 进阶配置
+            request_id: undefined,
+            returnType: undefined,
+            knowledge_ids: undefined,
+            document_ids: undefined,
+        });
+
+        // session title mock
+        if (sessionList.find((i) => i.key === curSession)?.label === 'New session') {
+            setSessionList(
+                sessionList.map((i) => (i.key !== curSession ? i : { ...i, label: val?.slice(0, 20) })),
+            );
+        }
+    };
+
+    const onPasteFile = (_: File, files: FileList) => {
+        for (const file of files) {
+            attachmentsRef.current?.upload(file);
+        }
+        setAttachmentsOpen(true);
+    };
+
+    // ==================== Nodes ====================
+    const chatHeader = (
+        <div className={styles.chatHeader}>
+            <div className={styles.headerTitle}>✨ AI Copilot</div>
+            <Space size={0}>
+                <Button
+                    type="text"
+                    icon={<PlusOutlined />}
+                    onClick={() => {
+                        if (agent.isRequesting()) {
+                            message.error(
+                                'Message is Requesting, you can create a new conversation after request done or abort it right now...',
+                            );
+                            return;
+                        }
+
+                        if (messages?.length) {
+                            const timeNow = dayjs().valueOf().toString();
+                            abortController.current?.abort();
+                            // The abort execution will trigger an asynchronous requestFallback, which may lead to timing issues.
+                            // In future versions, the sessionId capability will be added to resolve this problem.
+                            setTimeout(() => {
+                                setSessionList([
+                                    { key: timeNow, label: 'New session', group: 'Today' },
+                                    ...sessionList,
+                                ]);
+                                setCurSession(timeNow);
+                                setMessages([]);
+                            }, 100);
+                        } else {
+                            message.error('It is now a new conversation.');
+                        }
+                    }}
+                    className={styles.headerButton}
+                />
+                <Popover
+                    placement="bottom"
+                    styles={{ body: { padding: 0, maxHeight: 600 } }}
+                    content={
+                        <Conversations
+                            items={sessionList?.map((i) =>
+                                i.key === curSession ? { ...i, label: `[current] ${i.label}` } : i,
+                            )}
+                            activeKey={curSession}
+                            groupable
+                            onActiveChange={async (val) => {
+                                abortController.current?.abort();
+                                // The abort execution will trigger an asynchronous requestFallback, which may lead to timing issues.
+                                // In future versions, the sessionId capability will be added to resolve this problem.
+                                setTimeout(() => {
+                                    setCurSession(val);
+                                    setMessages(messageHistory?.[val] || []);
+                                }, 100);
+                            }}
+                            styles={{ item: { padding: '0 8px' } }}
+                            className={styles.conversations}
+                        />
+                    }
+                >
+                    <Button type="text" icon={<CommentOutlined />} className={styles.headerButton} />
+                </Popover>
+                <Button
+                    type="text"
+                    icon={<CloseOutlined />}
+                    onClick={() => setCopilotOpen(false)}
+                    className={styles.headerButton}
+                />
+            </Space>
+        </div>
+    );
+    const chatList = (
+        <div className={styles.chatList}>
+            {messages?.length ? (
+                /** 消息列表 */
+                <Bubble.List
+                    style={{ height: '100%', paddingInline: 16 }}
+                    items={messages?.map((i) => ({
+                        ...i.message,
+                        classNames: {
+                            content: i.status === 'loading' ? styles.loadingMessage : '',
+                        },
+                        typing: i.status === 'loading' ? { step: 5, interval: 20, suffix: <>💗</> } : false,
+                        content: <ReactMarkdown>{i.message.content}</ReactMarkdown>,
+                    }))}
+                    roles={{
+                        assistant: {
+                            placement: 'start',
+                            // footer: (
+                            //     <div style={{ display: 'flex' }}>
+                            //         <Button type="text" size="small" icon={<ReloadOutlined />} />
+                            //         <Button type="text" size="small" icon={<CopyOutlined />} />
+                            //         <Button type="text" size="small" icon={<LikeOutlined />} />
+                            //         <Button type="text" size="small" icon={<DislikeOutlined />} />
+                            //     </div>
+                            // ),
+                            loadingRender: () => (
+                                <Space>
+                                    <Spin size="small" />
+                                    {AGENT_PLACEHOLDER}
+                                </Space>
+                            ),
+                        },
+                        user: { placement: 'end' },
+                    }}
+                />
+            ) : (
+                /** 没有消息时的 welcome */
+                <Welcome
+                    variant="borderless"
+                    title="预览调试"
+                    description="请输入问题进行预览调试"
+                    className={styles.chatWelcome}
+                />
+            )}
+        </div>
+    );
+    const sendHeader = (
+        <Sender.Header
+            title="Upload File"
+            styles={{ content: { padding: 0 } }}
+            open={attachmentsOpen}
+            onOpenChange={setAttachmentsOpen}
+            forceRender
+        >
+            <Attachments
+                ref={attachmentsRef}
+                beforeUpload={() => false}
+                items={files}
+                onChange={({ fileList }) => setFiles(fileList)}
+                placeholder={(type) =>
+                    type === 'drop'
+                        ? { title: 'Drop file here' }
+                        : {
+                            icon: <CloudUploadOutlined />,
+                            title: 'Upload files',
+                            description: 'Click or drag files to this area to upload',
+                        }
+                }
+            />
+        </Sender.Header>
+    );
+    const chatSender = (
+        <div className={styles.chatSend}>
+            {/** 输入框 */}
+            <Suggestion items={MOCK_SUGGESTIONS} onSelect={(itemVal) => setInputValue(`[${itemVal}]:`)}>
+                {({ onTrigger, onKeyDown }) => (
+                    <Sender
+                        loading={loading}
+                        value={inputValue}
+                        onChange={(v) => {
+                            onTrigger(v === '/');
+                            setInputValue(v);
+                        }}
+                        onSubmit={() => {
+                            handleUserSubmit(inputValue);
+                            setInputValue('');
+                        }}
+                        onCancel={() => {
+                            abortController.current?.abort();
+                        }}
+                        allowSpeech
+                        placeholder="请输入"
+                        onKeyDown={onKeyDown}
+                        header={sendHeader}
+                        // prefix={
+                        //     <Button
+                        //         type="text"
+                        //         icon={<PaperClipOutlined style={{ fontSize: 18 }} />}
+                        //         onClick={() => setAttachmentsOpen(!attachmentsOpen)}
+                        //     />
+                        // }
+                        onPasteFile={onPasteFile}
+                        actions={(_, info) => {
+                            const { SendButton, LoadingButton, SpeechButton } = info.components;
+                            return (
+                                <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
+                                    {/* <SpeechButton className={styles.speechButton} /> */}
+                                    {loading ? <LoadingButton type="default" /> : <SendButton type="primary" />}
+                                </div>
+                            );
+                        }}
+                    />
+                )}
+            </Suggestion>
+        </div>
+    );
+
+    useEffect(() => {
+        // history mock
+        if (messages?.length) {
+            setMessageHistory((prev) => ({
+                ...prev,
+                [curSession]: messages,
+            }));
+        }
+    }, [messages]);
+
+    return (
+        <div className={styles.copilotChat} style={{ width: copilotOpen ? '100%' : 0 }}>
+            {/** 对话区 - header */}
+            {/* {chatHeader} */}
+            {/** 对话区 - 消息列表 */}
+            {chatList}
+            {/** 对话区 - 输入框 */}
+            {chatSender}
+        </div>
+    );
+};
+
+const useWorkareaStyle = createStyles(({ token, css }) => {
+    return {
+        copilotWrapper: css`
+      height: 100%;
+      display: flex;
+    `,
+        workarea: css`
+      flex: 1;
+      background: ${token.colorBgLayout};
+      display: flex;
+      flex-direction: column;
+    `,
+        workareaHeader: css`
+      box-sizing: border-box;
+      height: 52px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 0 48px 0 28px;
+      border-bottom: 1px solid ${token.colorBorder};
+    `,
+        headerTitle: css`
+      font-weight: 600;
+      font-size: 15px;
+      color: ${token.colorText};
+      display: flex;
+      align-items: center;
+      gap: 8px;
+    `,
+        headerButton: css`
+      background-image: linear-gradient(78deg, #8054f2 7%, #3895da 95%);
+      border-radius: 12px;
+      height: 24px;
+      width: 93px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: #fff;
+      cursor: pointer;
+      font-size: 12px;
+      font-weight: 600;
+      transition: all 0.3s;
+      &:hover {
+        opacity: 0.8;
+      }
+    `,
+        workareaBody: css`
+      flex: 1;
+      padding: 16px;
+      background: ${token.colorBgContainer};
+      border-radius: 16px;
+      min-height: 0;
+    `,
+        bodyContent: css`
+      overflow: auto;
+      height: 100%;
+      padding-right: 10px;
+    `,
+        bodyText: css`
+      color: ${token.colorText};
+      padding: 8px;
+    `,
+    };
+});
+
+const Chat: React.FC = () => {
+    const { styles: workareaStyles } = useWorkareaStyle();
+    const [copilotOpen, setCopilotOpen] = useState(true);
+
+    return (
+        <div className={workareaStyles.copilotWrapper}>
+            <Copilot copilotOpen={copilotOpen} setCopilotOpen={setCopilotOpen} />
+        </div>
+    );
+};
+
+export default Chat;

+ 7 - 3
src/pages/deepseek/questionAnswer/info/index.tsx

@@ -12,6 +12,7 @@ import { PlusCircleOutlined, MinusCircleOutlined, ArrowLeftOutlined, InfoCircleO
 import { apis } from '@/apis';
 import router from '@/router';
 import LocalStorage from '@/LocalStorage';
+import Chat from '@/components/chat';
 
 const { TextArea } = Input;
 const FormItem = Form.Item;
@@ -648,8 +649,8 @@ const QuestionAnswerInfo: React.FC = () => {
                                 <InfoCircleOutlined style={{ marginLeft: '8px', color: '#999', fontSize: '14px' }} />
                             </Tooltip>
                         </div>
-                        <Splitter style={{ border: '1px solid #f0f0f0', borderRadius: '6px' }}>
-                            <Splitter.Panel defaultSize="65%">
+                        <Splitter style={{ border: '1px solid #f0f0f0', borderRadius: '6px', height: 550 }}>
+                            <Splitter.Panel defaultSize="45%">
                                 <div className='prompt'>
                                     <div className='prompt-info'>
                                         <div className='prompt-info-text'>
@@ -701,7 +702,7 @@ const QuestionAnswerInfo: React.FC = () => {
                                     </div>
                                 </div>
                             </Splitter.Panel>
-                            <Splitter.Panel defaultSize="35%">
+                            <Splitter.Panel defaultSize="25%">
                                 <div className='flex-center-container'>
                                     <div className='half-width'>
                                         <div className='flex-center-top'>
@@ -974,6 +975,9 @@ const QuestionAnswerInfo: React.FC = () => {
                                     </div>
                                 </div>
                             </Splitter.Panel>
+                            <Splitter.Panel defaultSize="30%">
+                                <Chat />
+                            </Splitter.Panel>
                         </Splitter>
                     </div>
                 </Form>

+ 39 - 35
src/style/global.less

@@ -34,6 +34,10 @@ body {
     --border-radius: @border-radius-base;
 }
 
+p {
+    margin: 0;
+}
+
 a,
 a:hover,
 a:active {
@@ -95,40 +99,40 @@ ul li {
 
 // 全局按钮样式
 .ant-btn-primary {
-  background: #1890ff;
-  border: 1px solid #1890ff;
-  color: #ffffff;
-  transition: all 0.3s ease;
-  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-  
-  &:hover {
-    background: #40a9ff;
-    border-color: #40a9ff;
-    color: #ffffff;
-    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
-    transform: translateY(-1px);
-  }
-  
-  &:active {
-    background: #096dd9;
-    border-color: #096dd9;
-    color: #ffffff;
-    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
-    transform: translateY(0);
-  }
-  
-  &:focus {
     background: #1890ff;
-    border-color: #1890ff;
+    border: 1px solid #1890ff;
     color: #ffffff;
-    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
-  }
-  
-  &:disabled {
-    background: #f5f5f5;
-    border-color: #d9d9d9;
-    color: rgba(0, 0, 0, 0.25);
-    box-shadow: none;
-    transform: none;
-  }
-}
+    transition: all 0.3s ease;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
+    &:hover {
+        background: #40a9ff;
+        border-color: #40a9ff;
+        color: #ffffff;
+        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
+        transform: translateY(-1px);
+    }
+
+    &:active {
+        background: #096dd9;
+        border-color: #096dd9;
+        color: #ffffff;
+        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+        transform: translateY(0);
+    }
+
+    &:focus {
+        background: #1890ff;
+        border-color: #1890ff;
+        color: #ffffff;
+        box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+    }
+
+    &:disabled {
+        background: #f5f5f5;
+        border-color: #d9d9d9;
+        color: rgba(0, 0, 0, 0.25);
+        box-shadow: none;
+        transform: none;
+    }
+}

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov