LAPTOP-1LHD2FP6\joeny 5 місяців тому
батько
коміт
2455fbc0df

+ 3 - 0
package.json

@@ -13,6 +13,9 @@
     "ant-design-vue": "^4.0.0-rc.6",
     "axios": "^1.7.9",
     "dayjs": "^1.11.13",
+    "docx": "^9.1.0",
+    "file-saver": "^2.0.5",
+    "html2pdf.js": "^0.10.2",
     "less": "^4.2.1",
     "less-loader": "^12.2.0",
     "markdown-it": "^14.1.0",

+ 3 - 0
src/assets/svg/book.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="64 64 896 896" width="16" height="16" fill="currentColor">
+  <path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32zm-260 72h96v209.9L621.5 312 572 347.4V136zm220 752H232V136h280v296.9c0 3.3 1 6.6 3 9.3a15.9 15.9 0 0 0 22.3 3.7l83.8-59.9 81.4 59.4c2.7 2 6 3.1 9.3 3.1 8.8 0 16-7.2 16-16V136h64v752z"/>
+</svg> 

+ 3 - 0
src/assets/svg/export.svg

@@ -0,0 +1,3 @@
+<svg viewBox="64 64 896 896" width="16" height="16" fill="currentColor">
+  <path d="M888.3 757.4h-53.8c-4.2 0-7.7 3.5-7.7 7.7v61.8H197.1V197.1h629.8v61.8c0 4.2 3.5 7.7 7.7 7.7h53.8c4.2 0 7.7-3.5 7.7-7.7V158.7c0-17-13.7-30.7-30.7-30.7H158.7c-17 0-30.7 13.7-30.7 30.7v706.6c0 17 13.7 30.7 30.7 30.7h706.6c17 0 30.7-13.7 30.7-30.7V765.1c0-4.2-3.5-7.7-7.7-7.7zm18.6-251.7L765 393.7c-5.3-4.2-13-.4-13 6.3v76H438c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h314v76c0 6.7 7.8 10.5 13 6.3l141.9-112a8 8 0 000-12.6z"/>
+</svg> 

+ 3 - 0
src/assets/svg/markdown.svg

@@ -0,0 +1,3 @@
+<svg viewBox="64 64 896 896" width="16" height="16" fill="currentColor">
+  <path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494zM429 481.2c-1.9-4.4-6.2-7.2-11-7.2h-35c-6.6 0-12 5.4-12 12v272c0 6.6 5.4 12 12 12h27.1c6.6 0 12-5.4 12-12V582.1l66.8 150.2c1.9 4.3 6.2 7.1 11 7.1H524c4.7 0 9-2.8 11-7.1l66.8-150.6V758c0 6.6 5.4 12 12 12h27.1c6.6 0 12-5.4 12-12V486c0-6.6-5.4-12-12-12h-34.7c-4.8 0-9.1 2.8-11 7.2l-83.1 191-83.1-191z"/>
+</svg> 

+ 3 - 0
src/assets/svg/pdf.svg

@@ -0,0 +1,3 @@
+<svg viewBox="64 64 896 896" width="16" height="16" fill="currentColor">
+  <path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494z"/>
+</svg> 

+ 3 - 0
src/assets/svg/setting.svg

@@ -0,0 +1,3 @@
+<svg viewBox="64 64 896 896" width="16" height="16" fill="currentColor">
+  <path d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 0 0 9.3-35.2l-.9-2.6a443.74 443.74 0 0 0-79.7-137.9l-1.8-2.1a32.12 32.12 0 0 0-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 0 0-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 0 0-25.8 25.7l-15.8 85.4a351.86 351.86 0 0 0-99 57.4l-81.9-29.1a32 32 0 0 0-35.1 9.5l-1.8 2.1a446.02 446.02 0 0 0-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 0 0-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0 0 35.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0 0 25.8 25.7l2.7.5a449.4 449.4 0 0 0 159 0l2.7-.5a32.05 32.05 0 0 0 25.8-25.7l15.7-85a350 350 0 0 0 99.7-57.6l81.3 28.9a32 32 0 0 0 35.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 0 1-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 0 1-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 0 1 512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 0 1 400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 0 1 624 502c0 29.9-11.7 58-32.8 79.2z"/>
+</svg> 

+ 4 - 0
src/assets/svg/voice.svg

@@ -0,0 +1,4 @@
+<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor">
+  <path d="M842 454c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 140.3-113.7 254-254 254S258 594.3 258 454c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 168.7 126.6 307.9 290 327.6V884H326.7c-13.7 0-24.7 14.3-24.7 32v36c0 4.4 2.8 8 6.2 8h407.6c3.4 0 6.2-3.6 6.2-8v-36c0-17.7-11-32-24.7-32H548V782.1c165.3-18 294-158 294-328.1z" />
+  <path d="M512 624c93.9 0 170-75.2 170-168V232c0-92.8-76.1-168-170-168s-170 75.2-170 168v224c0 92.8 76.1 168 170 168zm-94-392c0-50.6 41.9-92 94-92s94 41.4 94 92v224c0 50.6-41.9 92-94 92s-94-41.4-94-92V232z" />
+</svg> 

+ 164 - 0
src/components/ExportButton/index.vue

@@ -0,0 +1,164 @@
+<template>
+  <div class="export-button">
+    <a-dropdown :trigger="['click']">
+      <a-button type="text" class="export-trigger">
+        <img src="../../assets/svg/export.svg" alt="export" class="export-icon" />
+      </a-button>
+      <template #overlay>
+        <a-menu>
+          <a-menu-item key="pdf" @click="exportAs('pdf')">
+            <img src="../../assets/svg/pdf.svg" alt="pdf" class="menu-icon" />
+            <span>导出为 PDF</span>
+          </a-menu-item>
+          <a-menu-item key="markdown" @click="exportAs('markdown')">
+            <img src="../../assets/svg/markdown.svg" alt="markdown" class="menu-icon" />
+            <span>导出为 Markdown</span>
+          </a-menu-item>
+          <a-menu-item key="word" @click="exportAs('word')">
+            <img src="../../assets/svg/word.svg" alt="word" class="menu-icon" />
+            <span>导出为 Word</span>
+          </a-menu-item>
+        </a-menu>
+      </template>
+    </a-dropdown>
+  </div>
+</template>
+
+<script setup>
+import { defineProps } from 'vue';
+import { message } from 'ant-design-vue';
+import html2pdf from 'html2pdf.js';
+import { saveAs } from 'file-saver';
+import { Document, Packer, Paragraph, TextRun } from 'docx';
+
+const props = defineProps({
+  conversation: {
+    type: Object,
+    required: true
+  }
+});
+
+const exportAs = async (format) => {
+  try {
+    switch (format) {
+      case 'pdf':
+        await exportToPdf();
+        break;
+      case 'markdown':
+        exportToMarkdown();
+        break;
+      case 'word':
+        exportToWord();
+        break;
+    }
+    message.success(`导出${format.toUpperCase()}成功`);
+  } catch (error) {
+    console.error('Export failed:', error);
+    message.error('导出失败');
+  }
+};
+
+const exportToPdf = async () => {
+  const content = document.createElement('div');
+  content.innerHTML = formatConversationForPdf();
+  
+  const opt = {
+    margin: 1,
+    filename: `对话记录_${new Date().toLocaleDateString()}.pdf`,
+    image: { type: 'jpeg', quality: 0.98 },
+    html2canvas: { scale: 2 },
+    jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }
+  };
+  
+  await html2pdf().set(opt).from(content).save();
+};
+
+const exportToMarkdown = () => {
+  const markdown = formatConversationForMarkdown();
+  const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' });
+  saveAs(blob, `对话记录_${new Date().toLocaleDateString()}.md`);
+};
+
+const exportToWord = () => {
+  const doc = new Document({
+    sections: [{
+      properties: {},
+      children: formatConversationForWord()
+    }]
+  });
+
+  Packer.toBlob(doc).then(blob => {
+    saveAs(blob, `对话记录_${new Date().toLocaleDateString()}.docx`);
+  });
+};
+
+const formatConversationForPdf = () => {
+  return props.conversation.messages.map(msg => `
+    <div style="margin-bottom: 20px;">
+      <div style="font-weight: bold; margin-bottom: 5px;">
+        ${msg.role === 'user' ? '用户' : 'AI助手'}:
+      </div>
+      <div style="white-space: pre-wrap;">${msg.content}</div>
+    </div>
+  `).join('');
+};
+
+const formatConversationForMarkdown = () => {
+  return props.conversation.messages.map(msg => 
+    `### ${msg.role === 'user' ? '用户' : 'AI助手'}\n\n${msg.content}\n\n`
+  ).join('---\n\n');
+};
+
+const formatConversationForWord = () => {
+  return props.conversation.messages.flatMap(msg => [
+    new Paragraph({
+      children: [
+        new TextRun({
+          text: `${msg.role === 'user' ? '用户' : 'AI助手'}:`,
+          bold: true
+        })
+      ]
+    }),
+    new Paragraph({
+      children: [
+        new TextRun(msg.content)
+      ]
+    }),
+    new Paragraph({
+      children: [new TextRun('\n')]
+    })
+  ]);
+};
+</script>
+
+<style scoped>
+.export-button {
+  display: inline-block;
+}
+
+.export-trigger {
+  padding: 4px;
+  height: 32px;
+  width: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.export-icon {
+  width: 16px;
+  height: 16px;
+}
+
+.menu-icon {
+  width: 14px;
+  height: 14px;
+  margin-right: 8px;
+}
+
+:deep(.ant-dropdown-menu-item) {
+  display: flex;
+  align-items: center;
+  padding: 8px 12px;
+}
+</style> 

+ 203 - 0
src/components/KnowledgeConfig/index.vue

@@ -0,0 +1,203 @@
+<template>
+  <div class="knowledge-config">
+    <div class="config-header" @click="togglePanel">
+      <img src="../../assets/svg/book.svg" alt="knowledge" class="config-icon" />
+      <span>知识库配置</span>
+      <img
+        :src="isPanelOpen ? '../../assets/svg/up.svg' : '../../assets/svg/down.svg'"
+        alt="toggle"
+        class="toggle-icon"
+      />
+    </div>
+    
+    <div v-show="isPanelOpen" class="config-panel">
+      <div class="knowledge-list">
+        <div class="knowledge-group" v-for="group in knowledgeGroups" :key="group.label">
+          <div class="group-header">{{ group.label }}</div>
+          <div class="checkbox-list">
+            <a-checkbox-group v-model:value="selectedKnowledge[group.key]">
+              <div v-for="item in group.items" :key="item.value" class="checkbox-item">
+                <a-checkbox :value="item.value">
+                  {{ item.label }}
+                  <span class="item-desc">{{ item.description }}</span>
+                </a-checkbox>
+              </div>
+            </a-checkbox-group>
+          </div>
+        </div>
+      </div>
+
+      <div class="action-buttons">
+        <a-button type="primary" :loading="saving" @click="saveConfig">
+          保存配置
+        </a-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { message } from 'ant-design-vue';
+
+const KNOWLEDGE_GROUPS = [
+  {
+    label: '产品文档',
+    key: 'product',
+    items: [
+      { label: 'Ant Design', value: 'antd', description: '企业级 UI 设计语言' },
+      { label: 'Vue.js', value: 'vue', description: '渐进式 JavaScript 框架' },
+      { label: 'React', value: 'react', description: '用于构建用户界面的库' }
+    ]
+  },
+  {
+    label: '技术文档',
+    key: 'tech',
+    items: [
+      { label: 'JavaScript', value: 'js', description: '编程语言基础' },
+      { label: 'TypeScript', value: 'ts', description: '类型系统扩展' },
+      { label: 'Node.js', value: 'node', description: '服务端运行时' }
+    ]
+  },
+  {
+    label: '行业资料',
+    key: 'industry',
+    items: [
+      { label: '前端开发', value: 'frontend', description: 'Web 开发技术' },
+      { label: '后端架构', value: 'backend', description: '服务器端开发' },
+      { label: '人工智能', value: 'ai', description: 'AI 与机器学习' }
+    ]
+  }
+];
+
+const isPanelOpen = ref(false);
+const saving = ref(false);
+const knowledgeGroups = ref(KNOWLEDGE_GROUPS);
+
+// 用于存储每个分组的选中状态
+const selectedKnowledge = reactive({
+  product: [],
+  tech: [],
+  industry: []
+});
+
+const togglePanel = () => {
+  isPanelOpen.value = !isPanelOpen.value;
+};
+
+const saveConfig = async () => {
+  saving.value = true;
+  try {
+    // 保存配置到 localStorage
+    localStorage.setItem('knowledgeConfig', JSON.stringify(selectedKnowledge));
+    message.success('知识库配置保存成功');
+  } catch (error) {
+    message.error('知识库配置保存失败');
+  } finally {
+    saving.value = false;
+  }
+};
+
+// 初始化配置
+const initConfig = () => {
+  const savedConfig = localStorage.getItem('knowledgeConfig');
+  if (savedConfig) {
+    const config = JSON.parse(savedConfig);
+    Object.keys(config).forEach(key => {
+      selectedKnowledge[key] = config[key];
+    });
+  }
+};
+
+// 组件挂载时加载配置
+initConfig();
+</script>
+
+<style scoped>
+.knowledge-config {
+  border-top: 1px solid rgba(0, 0, 0, 0.06);
+  background: #fff;
+}
+
+.config-header {
+  padding: 12px 20px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  transition: background-color 0.2s;
+}
+
+.config-header:hover {
+  background: rgba(0, 0, 0, 0.02);
+}
+
+.config-icon {
+  width: 16px;
+  height: 16px;
+}
+
+.toggle-icon {
+  width: 12px;
+  height: 12px;
+  margin-left: auto;
+}
+
+.config-panel {
+  padding: 12px 20px;
+  border-top: 1px solid rgba(0, 0, 0, 0.06);
+}
+
+.knowledge-list {
+  margin-bottom: 16px;
+}
+
+.knowledge-group {
+  margin-bottom: 16px;
+}
+
+.knowledge-group:last-child {
+  margin-bottom: 0;
+}
+
+.group-header {
+  font-size: 13px;
+  color: rgba(0, 0, 0, 0.85);
+  margin-bottom: 8px;
+}
+
+.checkbox-list {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.checkbox-item {
+  display: flex;
+  align-items: flex-start;
+}
+
+.item-desc {
+  margin-left: 8px;
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.action-buttons {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 16px;
+}
+
+:deep(.ant-checkbox-wrapper) {
+  font-size: 14px;
+}
+
+:deep(.ant-checkbox-group) {
+  width: 100%;
+}
+
+:deep(.ant-checkbox-wrapper + .ant-checkbox-wrapper) {
+  margin-left: 0;
+}
+</style> 

+ 207 - 214
src/components/ModelConfig/index.vue

@@ -1,27 +1,26 @@
 <template>
   <div class="model-config">
-    <div class="config-trigger" @click="showDrawer">
-      <SettingOutlined class="trigger-icon" />
+    <div class="config-header" @click="togglePanel">
+      <img src="../../assets/svg/setting.svg" alt="setting" class="config-icon" />
       <span>模型配置</span>
+      <img
+        :src="isPanelOpen ? '../../assets/svg/up.svg' : '../../assets/svg/down.svg'"
+        alt="toggle"
+        class="toggle-icon"
+      />
     </div>
 
-    <a-drawer
-      title="AI模型配置"
-      placement="left"
-      :width="400"
-      :visible="visible"
-      @close="closeDrawer"
-      class="model-drawer"
-    >
+    <div v-show="isPanelOpen" class="config-panel">
       <div class="model-list">
         <div v-for="(model, index) in modelList" :key="index" class="model-item">
           <div class="model-header">
             <span class="model-title">模型 {{index + 1}}</span>
-            <a-button type="link" danger @click="removeModel(index)">删除</a-button>
+            <a-button type="link" danger size="small" @click="removeModel(index)">删除</a-button>
           </div>
           
-          <a-form layout="vertical">
-            <a-form-item label="选择模型">
+          <div class="model-form">
+            <div class="form-item">
+              <label>选择模型</label>
               <a-select
                 v-model:value="model.type"
                 placeholder="请选择模型"
@@ -37,41 +36,45 @@
                   </a-select-option>
                 </a-select-opt-group>
               </a-select>
-            </a-form-item>
+            </div>
 
             <template v-if="model.type">
-              <a-form-item label="API Key">
+              <div class="form-item">
+                <label>API Key</label>
                 <a-input-password
                   v-model:value="model.key"
                   :placeholder="getKeyPlaceholder(model.type)"
                 />
-              </a-form-item>
+              </div>
               
               <template v-if="model.type === 'ollama'">
-                <a-form-item label="服务器地址">
+                <div class="form-item">
+                  <label>服务器地址</label>
                   <a-input
                     v-model:value="model.endpoint"
                     placeholder="例如: http://localhost:11434"
                   />
-                </a-form-item>
+                </div>
               </template>
 
               <template v-if="model.type === 'custom'">
-                <a-form-item label="模型名称">
+                <div class="form-item">
+                  <label>模型名称</label>
                   <a-input
                     v-model:value="model.name"
                     placeholder="请输入自定义模型名称"
                   />
-                </a-form-item>
-                <a-form-item label="服务器地址">
+                </div>
+                <div class="form-item">
+                  <label>服务器地址</label>
                   <a-input
                     v-model:value="model.endpoint"
                     placeholder="请输入API地址"
                   />
-                </a-form-item>
+                </div>
               </template>
             </template>
-          </a-form>
+          </div>
         </div>
       </div>
 
@@ -84,19 +87,18 @@
         <PlusOutlined /> 添加模型
       </a-button>
 
-      <div class="drawer-footer">
-        <a-button style="margin-right: 8px" @click="closeDrawer">取消</a-button>
+      <div class="action-buttons">
         <a-button type="primary" :loading="saving" @click="saveConfig">
           保存配置
         </a-button>
       </div>
-    </a-drawer>
+    </div>
   </div>
 </template>
 
-<script>
-import { defineComponent, ref, reactive } from 'vue';
-import { SettingOutlined, PlusOutlined } from '@ant-design/icons-vue';
+<script setup>
+import { ref, reactive } from 'vue';
+import { PlusOutlined } from '@ant-design/icons-vue';
 import { message } from 'ant-design-vue';
 
 const MODEL_GROUPS = [
@@ -132,200 +134,191 @@ const MODEL_GROUPS = [
   }
 ];
 
-export default defineComponent({
-  name: 'ModelConfig',
-  components: {
-    SettingOutlined,
-    PlusOutlined
-  },
-  setup() {
-    const visible = ref(false);
-    const saving = ref(false);
-    const modelList = ref([
-      { type: '', key: '', endpoint: '', name: '' }
-    ]);
-
-    const modelGroups = ref(MODEL_GROUPS);
-
-    const getKeyPlaceholder = (type) => {
-      const placeholders = {
-        'chatgpt': '请输入 OpenAI API Key',
-        'gpt4': '请输入 OpenAI API Key',
-        'claude': '请输入 Anthropic API Key',
-        'deepseek': '请输入 DeepSeek API Key',
-        'qianwen': '请输入阿里云 API Key',
-        'wenxin': '请输入百度 API Key',
-        'spark': '请输入讯飞 API Key',
-        'chatglm': '请输入智谱 API Key',
-        'ollama': '可选:请输入访问密钥',
-        'custom': '请输入 API Key'
-      };
-      return placeholders[type] || '请输入 API Key';
-    };
-
-    const handleModelChange = (value, index) => {
-      const model = modelList.value[index];
-      const selectedOption = MODEL_GROUPS.flatMap(group => group.options)
-        .find(option => option.value === value);
-      
-      if (selectedOption) {
-        model.endpoint = selectedOption.endpoint;
-        model.name = selectedOption.label;
-      }
-    };
-
-    const showDrawer = () => {
-      visible.value = true;
-    };
-
-    const closeDrawer = () => {
-      visible.value = false;
-    };
-
-    const addModel = () => {
-      modelList.value.push({ type: '', key: '', endpoint: '', name: '' });
-    };
-
-    const removeModel = (index) => {
-      if (modelList.value.length === 1) {
-        message.warning('至少保留一个模型配置');
-        return;
-      }
-      modelList.value.splice(index, 1);
-    };
-
-    const validateModel = (model) => {
-      if (!model.type) return false;
-      if (!model.key) return false;
-      if (model.type === 'ollama' && !model.endpoint) return false;
-      if (model.type === 'custom' && (!model.name || !model.endpoint)) return false;
-      return true;
-    };
-
-    const saveConfig = async () => {
-      // 验证所有字段都已填写
-      const hasInvalidModel = modelList.value.some(model => !validateModel(model));
-      if (hasInvalidModel) {
-        message.error('请填写完整的模型信息');
-        return;
-      }
-
-      saving.value = true;
-      try {
-        // 这里添加保存配置的逻辑
-        localStorage.setItem('modelConfig', JSON.stringify(modelList.value));
-        message.success('配置保存成功');
-        closeDrawer();
-      } catch (error) {
-        message.error('配置保存失败');
-      } finally {
-        saving.value = false;
-      }
-    };
-
-    // 初始化时从localStorage加载配置
-    const initConfig = () => {
-      const savedConfig = localStorage.getItem('modelConfig');
-      if (savedConfig) {
-        modelList.value = JSON.parse(savedConfig);
-      }
-    };
-
-    // 组件挂载时加载配置
-    initConfig();
-
-    return {
-      visible,
-      modelList,
-      modelGroups,
-      saving,
-      showDrawer,
-      closeDrawer,
-      addModel,
-      removeModel,
-      saveConfig,
-      handleModelChange,
-      getKeyPlaceholder
-    };
+const isPanelOpen = ref(false);
+const saving = ref(false);
+const modelList = ref([
+  { type: '', key: '', endpoint: '', name: '' }
+]);
+
+const modelGroups = ref(MODEL_GROUPS);
+
+const getKeyPlaceholder = (type) => {
+  const placeholders = {
+    'chatgpt': '请输入 OpenAI API Key',
+    'gpt4': '请输入 OpenAI API Key',
+    'claude': '请输入 Anthropic API Key',
+    'deepseek': '请输入 DeepSeek API Key',
+    'qianwen': '请输入阿里云 API Key',
+    'wenxin': '请输入百度 API Key',
+    'spark': '请输入讯飞 API Key',
+    'chatglm': '请输入智谱 API Key',
+    'ollama': '可选:请输入访问密钥',
+    'custom': '请输入 API Key'
+  };
+  return placeholders[type] || '请输入 API Key';
+};
+
+const handleModelChange = (value, index) => {
+  const model = modelList.value[index];
+  const selectedOption = MODEL_GROUPS.flatMap(group => group.options)
+    .find(option => option.value === value);
+  
+  if (selectedOption) {
+    model.endpoint = selectedOption.endpoint;
+    model.name = selectedOption.label;
   }
-});
-</script>
+};
 
-<style lang="less" scoped>
-.model-config {
-  position: fixed;
-  left: 20px;
-  bottom: 20px;
-  z-index: 1000;
-
-  .config-trigger {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    padding: 8px 16px;
-    background: #fff;
-    border-radius: 20px;
-    cursor: pointer;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
-    transition: all 0.3s;
-
-    &:hover {
-      background: #f5f5f5;
-      transform: translateY(-2px);
-    }
-
-    .trigger-icon {
-      font-size: 16px;
-      color: #1677ff;
-    }
-
-    span {
-      font-size: 14px;
-      color: #1677ff;
-    }
-  }
-}
+const togglePanel = () => {
+  isPanelOpen.value = !isPanelOpen.value;
+};
+
+const addModel = () => {
+  modelList.value.push({ type: '', key: '', endpoint: '', name: '' });
+};
 
-.model-drawer {
-  .model-list {
-    .model-item {
-      margin-bottom: 24px;
-      padding: 16px;
-      background: #fafafa;
-      border-radius: 8px;
-
-      .model-header {
-        display: flex;
-        justify-content: space-between;
-        align-items: center;
-        margin-bottom: 16px;
-
-        .model-title {
-          font-weight: 500;
-          color: #1677ff;
-        }
-      }
-    }
+const removeModel = (index) => {
+  if (modelList.value.length === 1) {
+    message.warning('至少保留一个模型配置');
+    return;
+  }
+  modelList.value.splice(index, 1);
+};
+
+const validateModel = (model) => {
+  if (!model.type) return false;
+  if (!model.key) return false;
+  if (model.type === 'ollama' && !model.endpoint) return false;
+  if (model.type === 'custom' && (!model.name || !model.endpoint)) return false;
+  return true;
+};
+
+const saveConfig = async () => {
+  const hasInvalidModel = modelList.value.some(model => !validateModel(model));
+  if (hasInvalidModel) {
+    message.error('请填写完整的模型信息');
+    return;
   }
 
-  .add-model-btn {
-    margin-bottom: 24px;
+  saving.value = true;
+  try {
+    localStorage.setItem('modelConfig', JSON.stringify(modelList.value));
+    message.success('配置保存成功');
+  } catch (error) {
+    message.error('配置保存失败');
+  } finally {
+    saving.value = false;
   }
+};
 
-  .drawer-footer {
-    position: absolute;
-    bottom: 0;
-    width: 100%;
-    border-top: 1px solid #f0f0f0;
-    padding: 16px 24px;
-    text-align: right;
-    left: 0;
-    background: #fff;
+// 初始化时从localStorage加载配置
+const initConfig = () => {
+  const savedConfig = localStorage.getItem('modelConfig');
+  if (savedConfig) {
+    modelList.value = JSON.parse(savedConfig);
   }
+};
+
+// 组件挂载时加载配置
+initConfig();
+</script>
+
+<style scoped>
+.model-config {
+  border-top: 1px solid rgba(0, 0, 0, 0.06);
+  background: #fff;
+}
+
+.config-header {
+  padding: 12px 20px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  transition: background-color 0.2s;
+}
+
+.config-header:hover {
+  background: rgba(0, 0, 0, 0.02);
+}
+
+.config-icon {
+  width: 16px;
+  height: 16px;
+}
+
+.toggle-icon {
+  width: 12px;
+  height: 12px;
+  margin-left: auto;
+}
+
+.config-panel {
+  padding: 12px 20px;
+  border-top: 1px solid rgba(0, 0, 0, 0.06);
+}
+
+.model-list {
+  margin-bottom: 16px;
+}
+
+.model-item {
+  margin-bottom: 16px;
+  padding: 12px;
+  background: #fafafa;
+  border-radius: 4px;
+}
+
+.model-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+}
+
+.model-title {
+  font-size: 13px;
+  color: rgba(0, 0, 0, 0.85);
+}
+
+.model-form {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.form-item {
+  margin-bottom: 12px;
+}
+
+.form-item label {
+  display: block;
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+  margin-bottom: 8px;
+}
+
+.add-model-btn {
+  margin-bottom: 16px;
+}
+
+.action-buttons {
+  display: flex;
+  justify-content: flex-end;
+}
+
+:deep(.ant-select),
+:deep(.ant-input),
+:deep(.ant-input-password) {
+  font-size: 14px;
+}
+
+:deep(.ant-select-selector) {
+  border-radius: 4px !important;
 }
 
-:deep(.ant-drawer-body) {
-  padding: 24px;
-  padding-bottom: 80px;
+:deep(.ant-input),
+:deep(.ant-input-password) {
+  border-radius: 4px;
 }
 </style> 

+ 165 - 0
src/components/VoiceConfig/index.vue

@@ -0,0 +1,165 @@
+<template>
+  <div class="voice-config">
+    <div class="config-header" @click="togglePanel">
+      <img src="../../assets/svg/voice.svg" alt="voice" class="config-icon" />
+      <span>语音配置</span>
+      <img
+        :src="isPanelOpen ? '../../assets/svg/up.svg' : '../../assets/svg/down.svg'"
+        alt="toggle"
+        class="toggle-icon"
+      />
+    </div>
+    
+    <div v-show="isPanelOpen" class="config-panel">
+      <!-- 语音角色选择 -->
+      <div class="config-item">
+        <label>语音角色</label>
+        <select v-model="voiceConfig.role" @change="handleConfigChange">
+          <option value="female">女声</option>
+          <option value="male">男声</option>
+          <option value="child">童声</option>
+        </select>
+      </div>
+      
+      <!-- 语速调节 -->
+      <div class="config-item">
+        <label>语速</label>
+        <div class="slider-container">
+          <input
+            type="range"
+            v-model="voiceConfig.speed"
+            min="0.5"
+            max="2"
+            step="0.1"
+            @change="handleConfigChange"
+          />
+          <span class="slider-value">{{ voiceConfig.speed }}x</span>
+        </div>
+      </div>
+      
+      <!-- 语言选择 -->
+      <div class="config-item">
+        <label>语言</label>
+        <select v-model="voiceConfig.language" @change="handleConfigChange">
+          <option value="zh">中文</option>
+          <option value="en">英文</option>
+          <option value="ja">日语</option>
+        </select>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { useStore } from 'vuex';
+
+const store = useStore();
+const isPanelOpen = ref(false);
+
+const voiceConfig = reactive({
+  role: 'female',
+  speed: 1,
+  language: 'zh'
+});
+
+const togglePanel = () => {
+  isPanelOpen.value = !isPanelOpen.value;
+};
+
+const handleConfigChange = () => {
+  store.commit('updateVoiceConfig', { ...voiceConfig });
+};
+</script>
+
+<style scoped>
+.voice-config {
+  border-top: 1px solid rgba(0, 0, 0, 0.06);
+  background: #fff;
+}
+
+.config-header {
+  padding: 12px 20px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  transition: background-color 0.2s;
+}
+
+.config-header:hover {
+  background: rgba(0, 0, 0, 0.02);
+}
+
+.config-icon {
+  width: 16px;
+  height: 16px;
+}
+
+.toggle-icon {
+  width: 12px;
+  height: 12px;
+  margin-left: auto;
+}
+
+.config-panel {
+  padding: 12px 20px;
+  border-top: 1px solid rgba(0, 0, 0, 0.06);
+}
+
+.config-item {
+  margin-bottom: 16px;
+}
+
+.config-item:last-child {
+  margin-bottom: 0;
+}
+
+.config-item label {
+  display: block;
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+  margin-bottom: 8px;
+}
+
+select {
+  width: 100%;
+  padding: 6px 8px;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+  font-size: 14px;
+  color: rgba(0, 0, 0, 0.88);
+  background: #fff;
+}
+
+.slider-container {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+input[type="range"] {
+  flex: 1;
+  height: 4px;
+  -webkit-appearance: none;
+  background: #e5e6eb;
+  border-radius: 2px;
+  outline: none;
+}
+
+input[type="range"]::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  width: 14px;
+  height: 14px;
+  border-radius: 50%;
+  background: #1677ff;
+  cursor: pointer;
+  border: none;
+}
+
+.slider-value {
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+  min-width: 36px;
+}
+</style> 

+ 40 - 3
src/views/report.vue

@@ -33,8 +33,12 @@
         </div>
       </div>
 
-      <!-- 添加模型配置组件到底部 -->
-      <ModelConfig />
+      <!-- 添加模型配置和语音配置的容器 -->
+      <div class="config-container">
+        <KnowledgeConfig />
+        <ModelConfig />
+        <VoiceConfig />
+      </div>
     </div>
 
     <div class="chat">
@@ -172,10 +176,17 @@
                   <img v-else :src="playIcon" alt="播放" class="audio-icon" />
                 </button>
               </div>
+
+
+
+
+
+
             </div>
           </div>
         </div>
       </div>
+
       <!-- 输入框 -->
       <div class="input-container">
         <!-- 标签栏 -->
@@ -397,6 +408,9 @@ import { useStore } from "vuex";
 import MarkdownIt from "markdown-it";
 import ModelConfig from '../components/ModelConfig/index.vue';
 import SearchResults from '../components/SearchResults/index.vue';
+import VoiceConfig from '../components/VoiceConfig/index.vue';
+import KnowledgeConfig from '../components/KnowledgeConfig/index.vue';
+import ExportButton from '../components/ExportButton/index.vue';
 
 // 初始化 markdown-it
 const md = new MarkdownIt({
@@ -423,7 +437,7 @@ const store = useStore();
 const conversations = ref([
   {
     id: 1,
-    title: "What is Ant Design X?",
+    title: "当前会话",
     messages: [],
   },
 ]);
@@ -2047,6 +2061,12 @@ const renderMarkdown = (content) => {
 .upload-area.uploading {
   opacity: 0.7;
 }
+
+/* 添加配置容器的样式 */
+.config-container {
+  margin-top: auto;
+  border-top: 1px solid rgba(0, 0, 0, 0.06);
+}
 </style>
 <style lang="less" scoped>
 ::v-deep .message-content {
@@ -2119,4 +2139,21 @@ const renderMarkdown = (content) => {
     background: #f5f5f5;
   }
 }
+
+.message-actions {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 8px;
+  padding-top: 8px;
+  border-top: 1px solid rgba(0, 0, 0, 0.06);
+}
+
+.message-actions :deep(.export-button) {
+  opacity: 0.6;
+  transition: opacity 0.2s;
+}
+
+.message-actions :deep(.export-button:hover) {
+  opacity: 1;
+}
 </style>