Parcourir la source

feat: 添加AI搜索功能及相关国际化支持,更新配置和组件

张涛 il y a 1 an
Parent
commit
fca4bb6b88

Fichier diff supprimé car celui-ci est trop grand
+ 935 - 15
package-lock.json


+ 3 - 1
package.json

@@ -20,7 +20,7 @@
     "eslint": "9.16.0",
     "eslint-plugin-vue": "9.32.0",
     "jszip": "3.10.1",
-    "marked": "15.0.4",
+    "marked": "^15.0.4",
     "patch-package": "8.0.0",
     "rollup-plugin-external-globals": "0.13.0",
     "rollup-plugin-visualizer": "5.14.0",
@@ -33,12 +33,14 @@
     "vitepress": "1.5.0"
   },
   "dependencies": {
+    "@antv/g6": "^5.0.43",
     "@arco-design/web-vue": "2.56.3",
     "@microsoft/fetch-event-source": "2.0.1",
     "axios": "1.7.8",
     "clipboard": "2.0.11",
     "dayjs": "1.11.13",
     "docx-preview": "0.3.3",
+    "eventsource-parser": "^3.0.0",
     "filesize": "10.1.6",
     "highlight.js": "11.10.0",
     "js-cookie": "3.0.5",

+ 1 - 0
public/i18n/lang-en.json

@@ -13,6 +13,7 @@
     "会话": "Conversation",
     "知识库管理": "Knowledge Base Management",
     "模型管理": "Model Management",
+    "AI搜索": "AI Search",
     "Agent": "Agent",
     "智能体": "Intelligent",
     "权限管理": "Permission",

+ 1 - 0
public/i18n/lang-zh.json

@@ -13,6 +13,7 @@
     "会话": "会话",
     "知识库管理": "知识库管理",
     "模型管理": "模型管理",
+    "AI搜索": "AI搜索",
     "Agent": "Agent",
     "智能体": "智能体",
     "权限管理": "权限管理",

+ 19 - 0
src/apis/AISearch.js

@@ -0,0 +1,19 @@
+import request from '../base/ajax';
+
+export const conversationAskPath = '/rgflow/v1/conversation/ask';
+
+export function retrievalTest(data) {
+  return request({
+    url: `/rgflow/v1/chunk/retrieval_test`,
+    method: 'post',
+    data
+  });
+}
+
+export function relatedQuestions(data) {
+  return request({
+    url: `/rgflow/v1/conversation/related_questions`,
+    method: 'post',
+    data
+  });
+}

+ 48 - 43
src/modules/AISearch/ChunkList.vue

@@ -1,40 +1,45 @@
 <template>
   <div class="chunk-list">
-    <div v-for="(item, index) in paginatedChunks" :key="index">
-      <div class="card">
-        <div class="documentReference">
-          <a-popover trigger="hover">
-            <template #content>
-              <div v-html="highlightText(item.docReference)"></div>
-            </template>
-            <div v-html="highlightText(item.docReference)" class="highlightContent" />
-          </a-popover>
-          <div class="fileBtn" @click="fileClick(item.docId, item)">
-            <a-space>
-              <FileIcon :fileType="item.fileType" />
-              <div>{{ item.docName }}</div>
-            </a-space>
+    <a-spin dot v-if="!chunks.length" />
+    <div v-else>
+      <div v-for="(item, index) in chunks" :key="index">
+        <div class="card">
+          <div class="documentReference">
+            <a-popover trigger="hover">
+              <template #content>
+                <div class="popupMarkdown">
+                  <div v-html="highlightText(item.content_with_weight)" />
+                </div>
+              </template>
+              <div v-html="highlightText(item.highlight)" class="highlightContent" />
+            </a-popover>
+            <div class="fileBtn" @click="fileClick(item.docId, item)">
+              <a-space>
+                <FileIcon :name="item.docnm_kwd" />
+                <div>{{ item.docnm_kwd }}</div>
+              </a-space>
+            </div>
           </div>
         </div>
+        <a-divider :margin="12" v-if="index < chunks.length - 1" />
+      </div>
+      <div v-if="chunks.length > 1">
+        <a-divider />
+        <a-pagination
+          :current="currentPage"
+          :total="chunks.length"
+          :pageSize="pageSize"
+          show-total
+          show-page-size
+          @change="handlePageChange"
+        />
       </div>
-      <a-divider v-if="index < paginatedChunks.length - 1" />
-    </div>
-    <div v-if="paginatedChunks.length > 1">
-      <a-divider />
-      <a-pagination
-        :current="currentPage"
-        :total="chunks.length"
-        :pageSize="pageSize"
-        show-total
-        show-page-size
-        @change="handlePageChange"
-      />
     </div>
   </div>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue';
+import { ref, watch } from 'vue';
 import FileIcon from './FileIcon.vue';
 
 const props = defineProps({
@@ -58,12 +63,6 @@ const highlightText = (text) => {
 const currentPage = ref(1);
 const pageSize = ref(10);
 
-const paginatedChunks = computed(() => {
-  const start = (currentPage.value - 1) * pageSize.value;
-  const end = start + pageSize.value;
-  return props.chunks.slice(start, end);
-});
-
 const handlePageChange = (page) => {
   currentPage.value = page;
 };
@@ -71,21 +70,26 @@ const handlePageChange = (page) => {
 
 <style scoped>
 .chunk-list {
-  /* 样式代码 */
+  text-align: center;
 }
 .card {
   box-sizing: border-box;
-  padding: 12px;
-  color: rgba(0, 0, 0, 0.88);
+  text-align: left;
+  padding: 14px;
+  padding-bottom: 8px;
+  color: var(--color-text-1);
   font-size: 14px;
   list-style: none;
   position: relative;
-  background: #ffffff;
-  border: 1px solid #d9d9d9;
+  background: var(--color-bg-1);
+  border: 1px solid var(--color-neutral-3);
   border-radius: 8px;
 }
 .popupMarkdown {
-  /* 样式代码 */
+  color: var(--color-text-1);
+  width: 60vw;
+  max-height: 40vh;
+  overflow: auto;
 }
 .highlightContent {
   display: -webkit-box;
@@ -96,17 +100,18 @@ const handlePageChange = (page) => {
 }
 
 .fileBtn {
+  font-weight: 600;
   cursor: pointer;
 }
 .documentReference {
   display: flex;
   flex-direction: column;
-  gap: 10px;
+  gap: 16px;
 }
 
 /* 新增高亮样式 */
 :deep(.highlighted) {
-  color: red; /* 设置高亮文本为色 */
+  color: red; /* 设置高亮文本为链接颜色 */
 }
 
 :deep(.arco-pagination) {
@@ -115,10 +120,10 @@ const handlePageChange = (page) => {
 :deep(.arco-pagination-item-active) {
   border-radius: 8px;
   font-weight: 600;
-  background-color: #fff;
+  background-color: var(--color-bg-1);
 }
 :deep(.arco-select-view-single) {
   border-radius: 8px;
-  background-color: #fff;
+  background-color: var(--color-bg-1);
 }
 </style>

+ 7 - 2
src/modules/AISearch/FileIcon.vue

@@ -6,11 +6,15 @@
 import { computed } from 'vue';
 
 const props = defineProps({
-  fileType: String
+  name: String
 });
 
+const getExtension = (name) => {
+  return name?.slice(name.lastIndexOf('.') + 1).toLowerCase() ?? '';
+};
+
 const iconSrc = computed(() => {
-  return new URL(`../../assets/AISearch/${props.fileType}.svg`, import.meta.url).href;
+  return new URL(`../../assets/AISearch/${getExtension(props.name)}.svg`, import.meta.url).href;
 });
 </script>
 
@@ -18,5 +22,6 @@ const iconSrc = computed(() => {
 .file-icon {
   width: 24px;
   height: 24px;
+  background-color: var(--color-bg-1);
 }
 </style>

+ 61 - 0
src/modules/AISearch/MarkdownContent.vue

@@ -0,0 +1,61 @@
+<template>
+  <div class="markdown-content">
+    <div class="header">
+      <img src="../../assets/logo.png" alt="Logo" class="logo" />
+      <h3>智能回答</h3>
+    </div>
+    <div v-if="content" v-html="htmlContent" class="content"></div>
+
+    <a-skeleton v-else class="content" :animation="true">
+      <a-space direction="vertical" :style="{ width: '100%' }" size="large">
+        <a-skeleton-line :rows="3" />
+      </a-space>
+    </a-skeleton>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+import { marked } from 'marked';
+
+const props = defineProps({
+  content: String
+});
+
+const htmlContent = computed(() => {
+  return marked(props.content || '');
+});
+</script>
+
+<style scoped>
+.markdown-content {
+  padding: 0 16px;
+  border-radius: 8px;
+  background-color: var(--color-bg-1);
+  font-size: 14px;
+  color: var(--color-text-1);
+  box-shadow: 0 2px 8px #00000014;
+}
+
+.header {
+  display: flex;
+  align-items: center;
+  background-color: #e6f4ff23;
+  color: var(--color-text-1);
+  font-weight: 600;
+  font-size: 16px;
+  background: transparent;
+  border-bottom: 1px solid var(--color-neutral-3);
+  border-radius: 8px 8px 0 0;
+}
+
+.logo {
+  width: 32px;
+  height: 32px;
+  margin-right: 12px;
+}
+
+.content {
+  padding: 16px 0;
+}
+</style>

+ 516 - 0
src/modules/AISearch/MindMapping.vue

@@ -0,0 +1,516 @@
+<template>
+  <div ref="container" class="mindmap-container"></div>
+</template>
+
+<script setup>
+import MindMappingData from './MindMappingData.json';
+import { onMounted, ref } from 'vue';
+import { Rect, Text } from '@antv/g';
+import {
+  Badge,
+  BaseBehavior,
+  BaseNode,
+  BaseTransform,
+  CommonEvent,
+  CubicHorizontal,
+  ExtensionCategory,
+  Graph,
+  GraphEvent,
+  idOf,
+  NodeEvent,
+  positionOf,
+  register,
+  treeToGraphData
+} from '@antv/g6';
+
+const container = ref(null);
+
+// 定义样式
+const RootNodeStyle = {
+  fill: '#EFF0F0',
+  labelFill: '#262626',
+  labelFontSize: 24,
+  labelFontWeight: 600,
+  labelOffsetY: 8,
+  labelPlacement: 'center',
+  ports: [{ placement: 'right' }, { placement: 'left' }],
+  radius: 8
+};
+
+const NodeStyle = {
+  fill: 'transparent',
+  labelPlacement: 'center',
+  labelFontSize: 16,
+  ports: [{ placement: 'right-bottom' }, { placement: 'left-bottom' }]
+};
+
+const TreeEvent = {
+  COLLAPSE_EXPAND: 'collapse-expand',
+  ADD_CHILD: 'add-child'
+};
+
+let textShape;
+
+const measureText = (text) => {
+  if (!textShape) textShape = new Text({ style: text });
+  textShape.attr(text);
+  return textShape.getBBox().width;
+};
+
+const getNodeWidth = (nodeId, isRoot) => {
+  const padding = isRoot ? 40 : 30;
+  const nodeStyle = isRoot ? RootNodeStyle : NodeStyle;
+  return measureText({ text: nodeId, fontSize: nodeStyle.labelFontSize, fontFamily: 'Gill Sans' }) + padding;
+};
+
+const getNodeSize = (nodeId, isRoot) => {
+  const width = getNodeWidth(nodeId, isRoot);
+  const height = isRoot ? 48 : 32;
+  return [width, height];
+};
+
+// 自定义节点类
+class MindmapNode extends BaseNode {
+  static defaultStyleProps = {
+    showIcon: false
+  };
+
+  constructor(options) {
+    Object.assign(options.style, MindmapNode.defaultStyleProps);
+    super(options);
+  }
+
+  get childrenData() {
+    return this.context.model.getChildrenData(this.id);
+  }
+
+  get rootId() {
+    return idOf(this.context.model.getRootsData()[0]);
+  }
+
+  isShowCollapse(attributes) {
+    const { collapsed, showIcon } = attributes;
+    return !collapsed && showIcon && this.childrenData.length > 0;
+  }
+
+  drawCircleShape(attributes, container) {
+    const { color = '#000', radius = 10 } = attributes; // 默认颜色和半径
+    const [width, height] = this.getSize(attributes);
+    const circleStyle = {
+      fill: color,
+      stroke: '#fff',
+      lineWidth: 1,
+      r: radius,
+      x: width / 2, // 圆心水平位置
+      y: height / 2 // 圆心垂直位置
+    };
+    this.upsert('circle', 'circle', circleStyle, container);
+  }
+
+  getCollapseStyle(attributes) {
+    const { showIcon, color, direction } = attributes;
+    if (!this.isShowCollapse(attributes)) return false;
+    const [width, height] = this.getSize(attributes);
+
+    return {
+      backgroundFill: color,
+      backgroundHeight: 12,
+      backgroundWidth: 12,
+      cursor: 'pointer',
+      fill: '#fff',
+      fontFamily: 'iconfont',
+      fontSize: 8,
+      text: '\ue6e4',
+      textAlign: 'center',
+      transform: direction === 'left' ? [['rotate', 90]] : [['rotate', -90]],
+      visibility: showIcon ? 'visible' : 'hidden',
+      x: direction === 'left' ? -6 : width + 6,
+      y: height
+    };
+  }
+
+  drawCollapseShape(attributes, container) {
+    const iconStyle = this.getCollapseStyle(attributes);
+    const btn = this.upsert('collapse-expand', Badge, iconStyle, container);
+
+    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {
+      event.stopPropagation();
+      this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {
+        id: this.id,
+        collapsed: !attributes.collapsed
+      });
+    });
+  }
+
+  getCountStyle(attributes) {
+    const { collapsed, color, direction } = attributes;
+    const count = this.context.model.getDescendantsData(this.id).length;
+    if (!collapsed || count === 0) return false;
+    const [width, height] = this.getSize(attributes);
+    return {
+      backgroundFill: color,
+      backgroundHeight: 12,
+      backgroundWidth: 12,
+      cursor: 'pointer',
+      fill: '#fff',
+      fontSize: 8,
+      text: count.toString(),
+      textAlign: 'center',
+      x: direction === 'left' ? -6 : width + 6,
+      y: height
+    };
+  }
+
+  drawCountShape(attributes, container) {
+    const countStyle = this.getCountStyle(attributes);
+    const btn = this.upsert('count', Badge, countStyle, container);
+
+    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {
+      event.stopPropagation();
+      this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {
+        id: this.id,
+        collapsed: false
+      });
+    });
+  }
+
+  getAddStyle(attributes) {
+    const { collapsed, showIcon, direction } = attributes;
+    if (collapsed || !showIcon) return false;
+    const [width, height] = this.getSize(attributes);
+    const color = '#ddd';
+
+    const offsetX = this.isShowCollapse(attributes) ? 24 : 12;
+    const isRoot = this.id === this.rootId;
+
+    return {
+      backgroundFill: '#fff',
+      backgroundHeight: 12,
+      backgroundLineWidth: 1,
+      backgroundStroke: color,
+      backgroundWidth: 12,
+      cursor: 'pointer',
+      fill: color,
+      fontFamily: 'iconfont',
+      fontSize: 8,
+      text: '\ue664',
+      textAlign: 'center',
+      x: isRoot ? width + 12 : direction === 'left' ? -offsetX : width + offsetX,
+      y: isRoot ? height / 2 : height
+    };
+  }
+
+  getAddBarStyle(attributes) {
+    const { collapsed, showIcon, direction, color = '#1783FF' } = attributes;
+    if (collapsed || !showIcon) return false;
+    const [width, height] = this.getSize(attributes);
+
+    const offsetX = this.isShowCollapse(attributes) ? 12 : 0;
+    const isRoot = this.id === this.rootId;
+
+    const HEIGHT = 2;
+    const WIDTH = 6;
+
+    return {
+      cursor: 'pointer',
+      fill:
+        direction === 'left'
+          ? `linear-gradient(180deg, #fff 20%, ${color})`
+          : `linear-gradient(0deg, #fff 20%, ${color})`,
+      height: HEIGHT,
+      width: WIDTH,
+      x: isRoot ? width : direction === 'left' ? -offsetX - WIDTH : width + offsetX,
+      y: isRoot ? height / 2 - HEIGHT / 2 : height - HEIGHT / 2,
+      zIndex: -1
+    };
+  }
+
+  drawAddShape(attributes, container) {
+    const addStyle = this.getAddStyle(attributes);
+    const addBarStyle = this.getAddBarStyle(attributes);
+    this.upsert('add-bar', Rect, addBarStyle, container);
+    const btn = this.upsert('add', Badge, addStyle, container);
+
+    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {
+      event.stopPropagation();
+      this.context.graph.emit(TreeEvent.ADD_CHILD, { id: this.id, direction: attributes.direction });
+    });
+  }
+
+  forwardEvent(target, type, listener) {
+    if (target && !Reflect.has(target, '__bind__')) {
+      Reflect.set(target, '__bind__', true);
+      target.addEventListener(type, listener);
+    }
+  }
+
+  getKeyStyle(attributes) {
+    const [width, height] = this.getSize(attributes);
+    const keyShape = super.getKeyStyle(attributes);
+    return { width, height, ...keyShape };
+  }
+
+  drawKeyShape(attributes, container) {
+    const keyStyle = this.getKeyStyle(attributes);
+    return this.upsert('key', Rect, keyStyle, container);
+  }
+
+  render(attributes = this.parsedAttributes, container = this) {
+    super.render(attributes, container);
+
+    this.drawCollapseShape(attributes, container);
+    this.drawAddShape(attributes, container);
+
+    this.drawCountShape(attributes, container);
+  }
+}
+
+// 自定义边类
+class MindmapEdge extends CubicHorizontal {
+  get rootId() {
+    return idOf(this.context.model.getRootsData()[0]);
+  }
+
+  getKeyPath(attributes) {
+    const path = super.getKeyPath(attributes);
+    const isRoot = this.targetNode.id === this.rootId;
+    const labelWidth = getNodeWidth(this.targetNode.id, isRoot);
+
+    const [, tp] = this.getEndpoints(attributes);
+    const sign = this.sourceNode.getCenter()[0] < this.targetNode.getCenter()[0] ? 1 : -1;
+    return [...path, ['L', tp[0] + labelWidth * sign, tp[1]]];
+  }
+}
+
+// 自定义行为类
+class CollapseExpandTree extends BaseBehavior {
+  constructor(context, options) {
+    super(context, options);
+    this.bindEvents();
+  }
+
+  update(options) {
+    this.unbindEvents();
+    super.update(options);
+    this.bindEvents();
+  }
+
+  bindEvents() {
+    const { graph } = this.context;
+
+    graph.on(NodeEvent.POINTER_ENTER, this.showIcon);
+    graph.on(NodeEvent.POINTER_LEAVE, this.hideIcon);
+    graph.on(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);
+    graph.on(TreeEvent.ADD_CHILD, this.addChild);
+  }
+
+  unbindEvents() {
+    const { graph } = this.context;
+
+    graph.off(NodeEvent.POINTER_ENTER, this.showIcon);
+    graph.off(NodeEvent.POINTER_LEAVE, this.hideIcon);
+    graph.off(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);
+    graph.off(TreeEvent.ADD_CHILD, this.addChild);
+  }
+
+  status = 'idle';
+
+  showIcon = (event) => {
+    this.setIcon(event, true);
+  };
+
+  hideIcon = (event) => {
+    this.setIcon(event, false);
+  };
+
+  setIcon = (event, show) => {
+    if (this.status !== 'idle') return;
+    const { target } = event;
+    const id = target.id;
+    const { graph, element } = this.context;
+    graph.updateNodeData([{ id, style: { showIcon: show } }]);
+    element.draw({ animation: false, silence: true });
+  };
+
+  onCollapseExpand = async (event) => {
+    this.status = 'busy';
+    const { id, collapsed } = event;
+    const { graph } = this.context;
+    await graph.frontElement(id);
+    if (collapsed) await graph.collapseElement(id);
+    else await graph.expandElement(id);
+    this.status = 'idle';
+  };
+
+  addChild = async (event) => {
+    this.status = 'busy';
+    const {
+      onCreateChild = () => {
+        const currentTime = new Date(Date.now()).toLocaleString();
+        return { id: `新节点 in ${currentTime}` };
+      }
+    } = this.options;
+    const { graph } = this.context;
+    const datum = onCreateChild(event.id);
+    const parent = graph.getNodeData(event.id);
+
+    graph.addNodeData([datum]);
+    graph.addEdgeData([{ source: event.id, target: datum.id }]);
+    graph.updateNodeData([
+      {
+        id: event.id,
+        children: [...(parent.children || []), datum.id],
+        style: { collapsed: false, showIcon: false }
+      }
+    ]);
+    await graph.render();
+    await graph.focusElement(datum.id);
+    this.status = 'idle';
+  };
+}
+
+// 自定义变换类
+class AssignColorByBranch extends BaseTransform {
+  static defaultOptions = {
+    colors: [
+      '#80B4FF', // 淡蓝色
+      '#FFC8A2', // 淡橙色
+      '#E6C8FF', // 淡紫色
+      '#A2E6E6', // 淡青色
+      '#C8B4FF', // 淡紫色
+      '#FFD878', // 淡黄色
+      '#A2E680', // 淡绿色
+      '#FFA2D8', // 淡粉色
+      '#80C8D8', // 淡蓝色
+      '#80E6A2', // 淡绿色
+      '#FFD878', // 淡橙色
+      '#D880D8', // 淡紫色
+      '#80C880', // 淡绿色
+      '#FFA280', // 淡红色
+      '#80E6E6', // 淡青色
+      '#FFA2D8', // 淡粉色
+      '#C8C8D8', // 淡蓝色
+      '#FFC8C8', // 淡红色
+      '#FFD878', // 淡黄色
+      '#C8FF80' // 淡绿色
+    ] // 增加更多颜色以避免重复
+  };
+
+  constructor(context, options) {
+    super(context, Object.assign({}, AssignColorByBranch.defaultOptions, options));
+  }
+
+  beforeDraw(input) {
+    const nodes = this.context.model.getNodeData();
+    const edges = this.context.model.getEdgeData();
+
+    if (nodes.length === 0) return input;
+
+    let colorIndex = 0;
+
+    // 为每个节点分配一个独立的颜色
+    nodes.forEach((node) => {
+      node.style ||= {};
+      node.style.color = this.options.colors[colorIndex++ % this.options.colors.length];
+    });
+
+    // 为每条边分配颜色,颜色与目标节点的颜色一致
+    edges.forEach((edge) => {
+      const targetNode = nodes.find((node) => node.id === edge.target);
+      if (targetNode) {
+        edge.style ||= {};
+        edge.style.stroke = targetNode.style.color;
+      }
+    });
+
+    return input;
+  }
+}
+
+// 注册扩展
+register(ExtensionCategory.NODE, 'mindmap', MindmapNode);
+register(ExtensionCategory.EDGE, 'mindmap', MindmapEdge);
+register(ExtensionCategory.BEHAVIOR, 'collapse-expand-tree', CollapseExpandTree);
+register(ExtensionCategory.TRANSFORM, 'assign-color-by-branch', AssignColorByBranch);
+
+// 获取节点方向
+const getNodeSide = (nodeData, parentData) => {
+  if (!parentData) return 'center';
+
+  const nodePositionX = positionOf(nodeData)[0];
+  const parentPositionX = positionOf(parentData)[0];
+  return parentPositionX > nodePositionX ? 'left' : 'right';
+};
+
+// 初始化图表
+onMounted(async () => {
+  // const response = await fetch('https://assets.antv.antgroup.com/g6/algorithm-category.json');
+  // const data = await response.json();
+  const data = MindMappingData;
+  const rootId = data.id;
+  const graph = new Graph({
+    container: container.value,
+    width: container.value.clientWidth || 1000, // 画布宽度,默认值800
+    height: container.value.clientHeight || 800, // 画布高度,默认值600
+    fitView: true, // 自动缩放以适应视图
+    fitCenter: true, // 自动居中
+    data: treeToGraphData(data),
+    node: {
+      type: 'mindmap',
+      style: function (d) {
+        const direction = getNodeSide(d, this.getParentData(idOf(d), 'tree'));
+        const isRoot = idOf(d) === rootId;
+
+        return {
+          direction,
+          labelText: idOf(d),
+          size: getNodeSize(idOf(d), isRoot),
+          labelFontFamily: 'Gill Sans',
+          labelBackground: true,
+          labelBackgroundFill: 'transparent',
+          labelPadding: direction === 'left' ? [2, 0, 10, 40] : [2, 40, 10, 0],
+          ...(isRoot ? RootNodeStyle : NodeStyle)
+        };
+      }
+    },
+    edge: {
+      type: 'mindmap',
+      style: {
+        lineWidth: 3,
+        stroke: function (data) {
+          return this.getNodeData(data.target).style.color || '#99ADD1';
+        }
+      }
+    },
+    layout: {
+      type: 'mindmap',
+      direction: 'H',
+      getHeight: () => 80,
+      getWidth: (node) => getNodeWidth(node.id, node.id === rootId),
+      getVGap: () => 6,
+      getHGap: () => 60,
+      animation: false,
+      // 新增配置项,使图表从左往右发散
+      getSide: () => 'right'
+    },
+    behaviors: ['drag-canvas', 'zoom-canvas', 'collapse-expand-tree'],
+    transforms: ['assign-color-by-branch'],
+    animation: {
+      duration: 200 // 动画时长,单位为毫秒
+    }
+  });
+
+  graph.once(GraphEvent.AFTER_RENDER, () => {
+    graph.fitView();
+  });
+
+  graph.render();
+});
+</script>
+
+<style scoped>
+.mindmap-container {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 58 - 0
src/modules/AISearch/MindMappingData.json

@@ -0,0 +1,58 @@
+{
+  "id": "电力维修",
+  "children": [
+    {
+      "id": "电力维修的资质要求",
+      "children": [
+        {
+          "id": "资质等级",
+          "children": [
+            {
+              "id": "分为一级、二级、三级、四级和五级,五级是初级,一级资质最高"
+            }
+          ]
+        },
+        {
+          "id": "资质申请材料",
+          "children": [
+            {
+              "id": "包括许可证申请表、法人证明材料和净资产证明材料、主要设备及机具清单、经营场所证明材料、主要负责人的简历、专业技术任职资格证书等有关证明材料、工程技术人员、经济管理人员明细表及其专业技术任职资格证明文件、电工作业人员登记表"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": "申请材料提交",
+      "children": [
+        {
+          "id": "申请人确定申请事项并根据填报要求网上提交或提供书面申请材料"
+        }
+      ]
+    },
+    {
+      "id": "申请材料审查",
+      "children": [
+        {
+          "id": "国家能源局派出机构确定其是否符合法定许可条件和标准,对申请材料进行受理审查"
+        }
+      ]
+    },
+    {
+      "id": "作出许可决定",
+      "children": [
+        {
+          "id": "派出机构根据申请材料审查情况,作出准予许可或不予许可的决定"
+        }
+      ]
+    },
+    {
+      "id": "许可证申请表",
+      "children": [
+        {
+          "id": "详细填写企业的基本信息、申请的许可证类别和等级、企业的经营范围、技术能力和管理水平等"
+        }
+      ]
+    }
+  ]
+}

+ 20 - 26
src/modules/AISearch/RetrievalDocuments.vue

@@ -7,14 +7,15 @@
       <template #header>
         <a-space>
           <!-- {{ t('knowledgeDetails.filesSelected') }} -->
-          选定文件
+          {{ selectedDocumentIds.length }}/{{ documents.length }} 选定文件
         </a-space>
       </template>
       <a-table
         :data="documents"
-        @select="onChange"
+        @select="onFileSelected"
         :rowKey="'doc_id'"
         :show-header="false"
+        :bordered="false"
         :row-selection="rowSelection"
         v-model:selectedKeys="selectedKeys"
         :pagination="pagination"
@@ -45,6 +46,11 @@ import { useI18n } from 'vue-i18n';
 import expandIcon from '../../assets/AISearch/selected-files-collapse.svg';
 
 const { t } = useI18n();
+const emit = defineEmits('onFilesSelected');
+
+const props = defineProps({
+  documents: String
+});
 
 const selectedDocumentIds = ref([]);
 const selectedKeys = ref([]);
@@ -55,42 +61,30 @@ const rowSelection = reactive({
 });
 const pagination = { pageSize: 10 };
 
-const onTesting = (documentIds) => {
-  // 处理测试逻辑
-};
-
 const setSelectedDocumentIds = (documentIds) => {
   selectedDocumentIds.value = documentIds;
 };
-const documents = [
-  {
-    doc_id: 1,
-    doc_name: '文件1',
-    count: 2
-  },
-  {
-    doc_id: 2,
-    doc_name: '文件2',
-    count: 1
-  },
-  {
-    doc_id: 2,
-    doc_name: '文件2',
-    count: 1
-  }
-];
 
-const onChange = (selectedRowKeys) => {
-  onTesting(selectedRowKeys);
+const onFileSelected = (selectedRowKeys) => {
+  emit('onFilesSelected', selectedRowKeys);
   setSelectedDocumentIds(selectedRowKeys);
 };
 </script>
 
 <style scoped>
 .selectFilesCollapse {
-  margin-bottom: 32px;
+  border-radius: 8px;
   overflow-y: auto;
 }
+
+:deep(.arco-collapse-item-header) {
+  padding-top: 12px;
+  padding-bottom: 12px;
+}
+:deep(.arco-collapse-item-content) {
+  background: var(--color-bg-1);
+  padding: 0;
+}
 :deep(.arco-collapse-item-header-title) {
   margin-left: 10px;
 }

+ 53 - 27
src/modules/AISearch/Sidebar.vue

@@ -1,48 +1,74 @@
 <template>
-  <a-layout-sider width="15%">
+  <a-layout-sider width="18%">
     <a-spin :loading="loading">
-      <a-tree
-        class="tree"
-        :data="modelList"
-        checkable
-        :checked-keys="checkedList"
-        :selected-keys="selectedKeys"
-        @check="onCheck"
-      />
+      <template v-if="modelList.length">
+        <a-tree
+          class="tree"
+          :data="modelList"
+          checkable
+          :checked-keys="checkedList"
+          :selected-keys="selectedKeys"
+          :field-names="{ title: 'name', key: 'id' }"
+          @check="onCheck"
+        />
+      </template>
+      <template v-else>
+        <div class="no-data">暂无数据</div>
+      </template>
     </a-spin>
   </a-layout-sider>
 </template>
 
 <script setup>
-import { ref } from 'vue';
+import { ref, onMounted, defineEmits } from 'vue';
+import { QueryAllKnowledgeList } from '../../apis/permission/common';
 
 const loading = ref(false);
-const modelList = ref([
-  {
-    title: 'Trunk 0-0',
-    key: '0-0',
-    children: [
-      {
-        title: 'Leaf',
-        key: '0-0-0-0'
-      },
-      {
-        title: 'Leaf',
-        key: '0-0-0-1'
-      }
-    ]
-  }
-]);
+const modelList = ref([]);
 const checkedList = ref([]);
 const selectedKeys = ref([]);
+const emit = defineEmits(['updateCheckedList', 'initSelectedKeys']);
+
+const fetchModelList = async () => {
+  loading.value = true;
+  try {
+    const {
+      data: { rows }
+    } = await QueryAllKnowledgeList();
+    modelList.value = rows;
+    checkedList.value = rows.map((item) => item.id); // 默认全选
+    // TODO: 选中的树ID
+    // emit('initSelectedKeys', checkedList.value); // 初始化时抛出所有的树ID
+    emit('initSelectedKeys', ['9aa72c1cc2a311ef8d280242ac1b0006', 'da47dcbaea9711ef94400242ac120006']); // 初始化时抛出所有的树ID
+  } catch (error) {
+    console.error(error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+onMounted(() => {
+  fetchModelList();
+});
 
 const onCheck = (checkedKeys) => {
   checkedList.value = checkedKeys;
+  emit('updateCheckedList', checkedKeys);
 };
 </script>
 
 <style scoped>
 .tree {
-  padding: 8px 16px;
+  padding: 8px;
+  background-color: var(--color-bg-1);
+}
+:deep(.arco-spin) {
+  height: 100%;
+  width: 100%;
+}
+.no-data {
+  padding: 8px;
+  text-align: center;
+  color: var(--color-text-secondary);
 }
 </style>

+ 197 - 0
src/modules/AISearch/hooks.js

@@ -0,0 +1,197 @@
+import { ref, computed, watch, onUnmounted } from 'vue';
+import { EventSourceParserStream } from 'eventsource-parser/stream';
+import { kuky_Authorization } from '../../base/storage';
+import { relatedQuestions, retrievalTest } from '../../apis/AISearch';
+import { Message } from '../../3rd-libs/arco-vue-libs';
+import { useRoute } from 'vue-router';
+
+export function useSetPaginationParams() {
+  const page = ref(1);
+  const pageSize = ref(10);
+
+  const setPaginationParams = (newPage, newPageSize) => {
+    page.value = newPage;
+    if (newPageSize) {
+      pageSize.value = newPageSize;
+    }
+  };
+
+  return {
+    setPaginationParams,
+    page,
+    size: pageSize
+  };
+}
+
+export function useTestChunkRetrieval() {
+  const { page, size: pageSize } = useSetPaginationParams();
+
+  const data = ref({ chunks: [], documents: [], total: 0 });
+  const loading = ref(false);
+
+  const route = useRoute();
+  const knowledgeBaseId = route?.query?.id || '';
+
+  const testChunk = async (values) => {
+    loading.value = true;
+    try {
+      const response = await retrievalTest({
+        ...values,
+        kb_id: values.kb_id ?? knowledgeBaseId,
+        page: page.value,
+        size: pageSize.value
+      });
+      if (response.data.retcode === 0) {
+        const res = response.data.data;
+        data.value = {
+          chunks: res.chunks,
+          documents: res.doc_aggs,
+          total: res.total
+        };
+      } else {
+        data.value = { chunks: [], documents: [], total: 0 };
+      }
+    } catch (error) {
+      data.value = { chunks: [], documents: [], total: 0 };
+      Message.error('检索失败');
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  return {
+    chunksData: computed(() => data.value),
+    loading: computed(() => loading.value),
+    testChunk
+  };
+}
+
+export function useFetchRelatedQuestions() {
+  const data = ref([]);
+  const loading = ref(false);
+
+  const fetchRelatedQuestions = async (question) => {
+    loading.value = true;
+    try {
+      const { data: responseData } = await relatedQuestions({ question });
+      data.value = responseData?.slice(0, 5) ?? [];
+    } catch (error) {
+      Message.error(error.message);
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  return {
+    data,
+    loading,
+    fetchRelatedQuestions
+  };
+}
+
+export function useSendMessageWithSse(url) {
+  const answer = ref({});
+  const done = ref(true);
+  const timer = ref(null);
+
+  const resetAnswer = () => {
+    if (timer.value) {
+      clearTimeout(timer.value);
+    }
+    timer.value = setTimeout(() => {
+      answer.value = {};
+      clearTimeout(timer.value);
+    }, 1000);
+  };
+
+  const send = async (body, controller) => {
+    try {
+      done.value = false;
+      const token = kuky_Authorization.get();
+      const response = await fetch(url, {
+        method: 'POST',
+        headers: {
+          Authorization: `Bearer ${token}`,
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(body),
+        signal: controller?.signal
+      });
+
+      const res = response.clone().json();
+
+      const reader = response?.body
+        ?.pipeThrough(new TextDecoderStream())
+        .pipeThrough(new EventSourceParserStream())
+        .getReader();
+
+      while (true) {
+        const x = await reader?.read();
+        if (x) {
+          const { done: readerDone, value } = x;
+          if (readerDone) {
+            console.info('done');
+            resetAnswer();
+            break;
+          }
+          try {
+            const val = JSON.parse(value?.data || '');
+            const d = val?.data;
+            if (typeof d !== 'boolean') {
+              console.info('data:', d);
+              answer.value = {
+                ...d,
+                conversationId: body?.conversation_id
+              };
+            }
+          } catch (e) {
+            console.warn(e);
+          }
+        }
+      }
+      console.info('done?');
+      done.value = true;
+      resetAnswer();
+      return { data: await res, response };
+    } catch (e) {
+      done.value = true;
+      resetAnswer();
+      console.warn(e);
+    }
+  };
+
+  onUnmounted(() => {
+    if (timer.value) {
+      clearTimeout(timer.value);
+    }
+  });
+
+  return { send, answer, done, resetAnswer };
+}
+
+export function useGetPaginationWithRouter() {
+  const { setPaginationParams, page, size: pageSize } = useSetPaginationParams();
+
+  const onPageChange = (pageNumber, pageSize) => {
+    setPaginationParams(pageNumber, pageSize);
+  };
+
+  const setCurrentPagination = (pagination) => {
+    setPaginationParams(pagination.page, pagination.pageSize);
+  };
+
+  const pagination = computed(() => ({
+    showQuickJumper: true,
+    total: 0,
+    showSizeChanger: true,
+    current: page.value,
+    pageSize: pageSize.value,
+    pageSizeOptions: [1, 2, 10, 20, 50, 100],
+    onChange: onPageChange
+  }));
+
+  return {
+    pagination,
+    setPagination: setCurrentPagination
+  };
+}

+ 390 - 30
src/modules/AISearch/index.vue

@@ -1,63 +1,413 @@
 <template>
   <div class="ai-search-container">
-    <Sidebar />
+    <Sidebar @updateCheckedList="handleUpdateCheckedList" @initSelectedKeys="initSidebar" />
     <div class="content-box" :class="{ 'search-active': searchActive }">
       <a-input-search
         class="input-search"
         v-model="searchQuery"
         placeholder="请输入搜索内容"
         search-button
+        :loading="sendingLoading"
         @search="onSearch"
       />
       <transition name="fade">
         <div v-if="searchActive" class="search-results">
-          <RetrievalDocuments />
-          <ChunkList :chunks="chunks" :highlightWord="searchQuery" @clickDocumentButton="handleDocumentClick" />
-
-          <a-button class="floating-btn" shape="circle">
+          <MarkdownContent :content="answer?.answer" />
+          <a-divider />
+          <RetrievalDocuments
+            :documents="chunksData.doc_aggs"
+            :selectedDocumentIds="selectedDocumentIds"
+            @onFileSelected="handleTestChunk"
+          />
+          <a-divider />
+          <ChunkList
+            :chunks="chunksData.chunks"
+            :highlightWord="searchQuery"
+            @clickDocumentButton="handleDocumentClick"
+          />
+          <a-divider />
+          <a-card v-if="relatedQuestionsData?.length > 0" title="相关问题">
+            <a-space wrap>
+              <a-tag
+                v-for="(question, index) in relatedQuestionsData"
+                :key="index"
+                @click="handleClickRelatedQuestion(question)"
+                class="related-question-tag"
+              >
+                {{ question }}
+              </a-tag>
+            </a-space>
+          </a-card>
+          <a-divider />
+          <a-pagination
+            v-model:current="pagination.current"
+            :total="chunksData.total"
+            :pageSize="pagination.pageSize"
+            @change="onChange"
+          />
+          <a-button class="floating-btn" shape="circle" @click="handleFloatingBtnClick">
             <icon-attachment size="32" />
           </a-button>
         </div>
       </transition>
     </div>
   </div>
+
+  <a-drawer v-model:visible="drawerVisible" :title="drawerTitle" :width="drawerWidth" :footer="false">
+    <MindMapping />
+  </a-drawer>
 </template>
 
 <script setup>
-import { ref } from 'vue';
+import { ref, watch, onMounted } from 'vue';
 import Sidebar from './Sidebar.vue';
 import RetrievalDocuments from './RetrievalDocuments.vue';
-import { IconAttachment } from '@arco-design/web-vue/es/icon';
+import MarkdownContent from './MarkdownContent.vue';
+import MindMapping from './MindMapping.vue';
 import ChunkList from './ChunkList.vue';
+import { IconAttachment } from '@arco-design/web-vue/es/icon';
+import { conversationAskPath } from '../../apis/AISearch';
+import { queryKbList } from '../../apis/rgflow-dify';
+import {
+  useSendMessageWithSse,
+  useTestChunkRetrieval,
+  useFetchRelatedQuestions,
+  useGetPaginationWithRouter
+} from './hooks';
+import { isEmpty, trim } from 'lodash';
+
 const searchQuery = ref('');
 const searchActive = ref(false);
+const drawerVisible = ref(false);
+const drawerWidth = ref(1000);
+const drawerTitle = ref('思维导图');
+const checkedList = ref([]);
+const checkedWithoutEmbeddingIdList = ref([]);
+const sendingLoading = ref(false);
+const searchStr = ref('');
+const isFirstRender = ref(true);
+const selectedDocumentIds = ref([]);
 
-const onSearch = (value) => {
-  console.log('搜索内容:', value);
-  searchActive.value = true;
-  // 在这里处理搜索逻辑
+const queryKnowledgeList = async () => {
+  const knowledgeList = ref([]);
+  const {
+    data: { kbs }
+  } = await queryKbList();
+  if (kbs) {
+    knowledgeList.value = kbs;
+    checkedWithoutEmbeddingIdList.value = checkedList.value.filter((x) => knowledgeList.value.some((y) => y.id === x));
+  }
 };
 
-const chunks = ref([
-  {
-    docId: '1',
-    docReference:
-      '参考文献参考文献参考文献参考文献参考文献参考文参考文献参考文献参考文献参考文献参考文献参考文献参考文献参考文献参考文献参考文献参考文献参考文献参考文献参考文献献参考文献参考文献参考文献参考文献参考文献参考文献参考文献参考文献 1',
-    fileType: 'pdf',
-    docName: '文档名称 1',
-    highlightContent: '<p>这是高亮内容 1</p>'
-  },
-  {
-    docId: '2',
-    docReference: '参考文献 2',
-    fileType: 'doc',
-    docName: '文档名称 2',
-    highlightContent: '<p>这是高亮内容 2</p>'
-  }
-]);
+onMounted(queryKnowledgeList);
+
+const { send, answer, done } = useSendMessageWithSse(conversationAskPath);
+const { testChunk } = useTestChunkRetrieval();
+const chunksData = {
+  chunks: [
+    {
+      chunk_id: 'dfb302c677d12caa',
+      content_ltks:
+        '为 进一步 明确 500 千伏 三林 站 gi 气室 故障 原因 设备 中心 于 12 月 组织 开展 母线 导体 连接处 接触不良 发热 烧熔 的 复现 试验 以 修复 后 的 故障 气室 为 研究 对象 模拟 因 螺栓 松动 导致 的 导体 接触不良 缺陷 复现 烧熔 现象 本次 触头 发热 复现 试验 中 模拟 通过 四颗 螺栓 进行 导流 调整 接触 电阻 至 毫 欧 级 施加 800 a 电流 在 持续 数 小时 后 触头',
+      content_with_weight:
+        '为进一步明确500千伏三林站…GIS气室故障原因,设备中心于12月组织开展母线导体连接处接触不良、发热烧熔的复现试验。以修复后的故障气室为研究对象,模拟因螺栓松动导致的导体接触不良缺陷,复现烧熔现象。本次触头发热复现试验中,模拟通过四颗螺栓进行导流,调整接触电阻至毫欧级,施加800A电流,在持续数小时后触头',
+      doc_id: 'fef62ff8ea9711efa8250242ac120006',
+      docnm_kwd: '《设备技术中心工作动态》2024年第1期.pdf',
+      highlight:
+        '为进一步明确500千伏三林站 gi气室故障原因设备中心于 12月组织开展母线导体连接处接触不良发热烧熔的复现试验以 <em>修复</em>后的故障气室为研究',
+      image_id: 'da47dcbaea9711ef94400242ac120006-dfb302c677d12caa',
+      important_kwd: [],
+      kb_id: 'da47dcbaea9711ef94400242ac120006',
+      positions: [[10, 74, 264, 520, 705]],
+      similarity: 0.8448677925218482,
+      term_similarity: 1,
+      vector_similarity: 0.4828926417394943
+    },
+    {
+      chunk_id: 'b4551e204904b0bc',
+      content_ltks:
+        '应急 响应 220 千伏 万荣 站 闸刀 内部 机构 异常 技术 支撑 11 月 1 日 , 万荣 站 配合 天宝 站 2号 主 变 调换 启动 方式 恢复 操作 中 合 上 220 千伏 1号 母 联 开关 合 后 , 发现 1号 母 联 开关 三相 电流 不 平衡 , 调取 历史潮流 数据分析 认为 荣仁 2b 58 正 母 闸刀 可能 存在 异常 。 随后 现场 召开 会议 制定 了 差异化 运维 措施 。 11 月 13 日 开始 , 公司 安排 检修 计划 对 2b 58 正 母 闸刀 b 相 进行 解体 检查和 修复 工作 , 电科院 技术人员 现场 见证 了 现场 缺陷 检查和 分析 工作 。 初步 分析 认为 缺陷 主要 原因 是 固定 夹 叉 的 螺栓 松动 。 11 月 18 日 , 该站 在 用 x射线 进行 闸刀 隐患 排查 时 , 发现 石 荣 2136 付 母 闸刀 a 相 内部 夹 叉 也 存在 松动 现象 。 公司 立即 安排 解体 检修 , 拆开 后 发现 夹 叉 有 轻微 倾斜 移位 , 紧固 夹 叉 的 螺栓 明显 松动 , 然后 公司 对 机构 进行 了 修复 工作 。 由于 两把 闸刀 均 出现 同 类型 的 缺陷 , 公司 组织 召开 了 专题 分析 讨论 会议 , 制定 了 下 一步 工作 计划 。 ( 文 : 胡 正 勇 )',
+      content_with_weight:
+        '应急响应220千伏万荣站闸刀内部机构异常技术支撑11月1日,万荣站配合天宝站2号主变调换启动方式恢复操作中合上220千伏1号母联开关合后,发现1号母联开关三相电流不平衡,调取历史潮流数据分析认为荣仁2B58正母闸刀可能存在异常。随后现场召开会议制定了差异化运维措施。11月13日开始,公司安排检修计划对2B58正母闸刀B相进行解体检查和修复工作,电科院技术人员现场见证了现场缺陷检查和分析工作。初步分析认为缺陷主要原因是固定夹叉的螺栓松动。11月18日,该站在用X射线进行闸刀隐患排查时,发现石荣2136付母闸刀A相内部夹叉也存在松动现象。公司立即安排解体检修,拆开后发现夹叉有轻微倾斜移位,紧固夹叉的螺栓明显松动,然后公司对机构进行了修复工作。由于两把闸刀均出现同类型的缺陷,公司组织召开了专题分析讨论会议,制定了下一步工作计划。(文:胡正勇)',
+      doc_id: 'cb5a8c5ac2a311ef82860242ac1b0006',
+      docnm_kwd: '《设备技术中心工作动态》2023年第12期.pdf',
+      highlight:
+        '11月13日开始,公司安排检修计划对 2b 58正母闸刀b相进行解体检查和<em>修复</em>工作,电科院技术人员现场见证了现场缺陷检查和分析工作。...公司立即安排解体检修,拆开后发现夹叉有轻微倾斜移位,紧固夹叉的螺栓明显松动,然后公司对机构进行了 <em>修复</em>工作。',
+      image_id: '9aa72c1cc2a311ef8d280242ac1b0006-b4551e204904b0bc',
+      important_kwd: [],
+      kb_id: '9aa72c1cc2a311ef8d280242ac1b0006',
+      positions: [
+        [25, 91, 523, 297, 324],
+        [25, 88, 520, 332, 514]
+      ],
+      similarity: 0.8445477432805392,
+      term_similarity: 1,
+      vector_similarity: 0.4818258109351308
+    },
+    {
+      chunk_id: '81346015403dcd12',
+      content_ltks:
+        '开展 三林 站 母线 修复 高风险 作业 督查 2023 年 11 月 , 生产 管控 室 组织 浦东 和 松江 公司 专家 开展 三林 站 三 母线 修复 二级 高风险 作业 的 作业 督查 工作 。 2023 年 10 月 , 三林 站 三 母线 跳闸 , 现场 发现 三 母线 气室 外壳 有 击 穿孔 。 事故 发生 后 , 超高压 公司 隔离 了 故障 母线 。 进 博会 保 电 结束 后 , 超高压 公司 开始 开展 母线 修复 工作 。 根据 国网公司 “ 五级 五 控 ” 的 要求 , 上海市 生产 管控 中心 生产 管控 室 组织 浦东 公司 和 青浦 公司 专家 于 2023 年 11 月 21 日和 22 日 两次 去 现场 进行 作业 督查 。 督查 过程 中 , 现场 正 开展 母线 抽 真空 和 分段 开关 机构 渗 油 处理 , 现场 作业 督查 过程 中 未 发现 问题 。 ( 文 : 吴 天逸 )',
+      content_with_weight:
+        '开展三林站母线修复高风险作业督查2023年11月,生产管控室组织浦东和松江公司专家开展三林站三母线修复二级高风险作业的作业督查工作。2023年10月,三林站三母线跳闸,现场发现三母线气室外壳有击穿孔。事故发生后,超高压公司隔离了故障母线。进博会保电结束后,超高压公司开始开展母线修复工作。根据国网公司“五级五控”的要求,上海市生产管控中心生产管控室组织浦东公司和青浦公司专家于2023年11月21日和22日两次去现场进行作业督查。督查过程中,现场正开展母线抽真空和分段开关机构渗油处理,现场作业督查过程中未发现问题。(文:吴天逸)',
+      doc_id: 'cb5a8c5ac2a311ef82860242ac1b0006',
+      docnm_kwd: '《设备技术中心工作动态》2023年第12期.pdf',
+      highlight:
+        '开展三林站母线<em>修复</em>高风险作业督查2023年11月,生产管控室组织浦东和松江公司专家开展三林站三母线<em>修复</em>二级高风险作业的作业督查工作。...进博会保电结束后 ,超高压公司开始开展母线<em>修复</em>工作。',
+      image_id: '9aa72c1cc2a311ef8d280242ac1b0006-81346015403dcd12',
+      important_kwd: [],
+      kb_id: '9aa72c1cc2a311ef8d280242ac1b0006',
+      positions: [
+        [35, 94, 526, 571, 588],
+        [35, 88, 520, 597, 723]
+      ],
+      similarity: 0.8438986441373534,
+      term_similarity: 1,
+      vector_similarity: 0.47966214712451133
+    },
+    {
+      chunk_id: '92b840b911f4d3da',
+      content_ltks:
+        '7 月 期间 , 在 电科院 设备 中心 的 协调 组织 下 , 南瑞 信通 、 北京 信 普达 、 上海 翊 邦 项目组 的 相关 专业 人员 进行 了 长达 一个月 的 项目 内部测试 工作 , 7 月 25 日 , 项目组 总结 了 工业 互联网 设备 远程 运维 模块 的 改进 和 部署 实施 情况 , 并 制定 了 8 月份 的 bug 修复 和 系统 部署 计划 。 … 针对 于 目前 系统 存在 的 问题 和 部署 执行 方 解决问题 能力 不足 的 情况 , 项目组 已 与 南瑞 信通 协商 并 达成 一致意见 , 在 接下来 的 建设 和 部署 工作 中 , 南瑞 信通 将 派遣 新 的 项目 成员 入 组 进行 系统 完善 以及 功能测试 , 坚决 在 规定 时间 内 保质保量 地 如期完成 项目 所有 功能 。 ( 文 : 彭 政 睿 )',
+      content_with_weight:
+        '7月期间,在电科院设备中心的协调组织下,南瑞信通、北京信普达、上海翊邦项目组的相关专业人员进行了长达一个月的项目内部测试工作,7月25日,项目组总结了工业互联网设备远程运维模块的改进和部署实施情况,并制定了8月份的BUG修复和系统部署计划。…针对于目前系统存在的问题和部署执行方解决问题能力不足的情况,项目组已与南瑞信通协商并达成一致意见,在接下来的建设和部署工作中,南瑞信通将派遣新的项目成员入组进行系统完善以及功能测试,坚决在规定时间内保质保量地如期完成项目所有功能。(文:彭政睿)',
+      doc_id: 'c57a96e0c2a311ef82860242ac1b0006',
+      docnm_kwd: '《设备技术中心工作动态》2023年第8期 - 副本.pdf',
+      highlight:
+        '了长达一个月的项目内部测试工作, 7月25日,项目组总结了工业互联网设备远程运维模块的改进和部署实施情况,并制定了 8月份的 bug <em>修复</em>',
+      image_id: '9aa72c1cc2a311ef8d280242ac1b0006-92b840b911f4d3da',
+      important_kwd: [],
+      kb_id: '9aa72c1cc2a311ef8d280242ac1b0006',
+      positions: [[36, 87, 519, 312, 437]],
+      similarity: 0.8438452874204787,
+      term_similarity: 1,
+      vector_similarity: 0.4794842914015958
+    },
+    {
+      chunk_id: '0ac501cf467e1f56',
+      content_ltks:
+        '7 月 期间 , 在 电科院 设备 中心 的 协调 组织 下 , 南瑞 信通 、 北京 信 普达 、 上海 翊 邦 项目组 的 相关 专业 人员 进行 了 长达 一个月 的 项目 内部测试 工作 , 7 月 25 日 , 项目组 总结 了 工业 互联网 设备 远程 运维 模块 的 改进 和 部署 实施 情况 , 并 制定 了 8 月份 的 bug 修复 和 系统 部署 计划 。 … 针对 于 目前 系统 存在 的 问题 和 部署 执行 方 解决问题 能力 不足 的 情况 , 项目组 已 与 南瑞 信通 协商 并 达成 一致意见 , 在 接下来 的 建设 和 部署 工作 中 , 南瑞 信通 将 派遣 新 的 项目 成员 入 组 进行 系统 完善 以及 功能测试 , 坚决 在 规定 时间 内 保质保量 地 如期完成 项目 所有 功能 。 ( 文 : 彭 政 睿 )',
+      content_with_weight:
+        '7月期间,在电科院设备中心的协调组织下,南瑞信通、北京信普达、上海翊邦项目组的相关专业人员进行了长达一个月的项目内部测试工作,7月25日,项目组总结了工业互联网设备远程运维模块的改进和部署实施情况,并制定了8月份的BUG修复和系统部署计划。…针对于目前系统存在的问题和部署执行方解决问题能力不足的情况,项目组已与南瑞信通协商并达成一致意见,在接下来的建设和部署工作中,南瑞信通将派遣新的项目成员入组进行系统完善以及功能测试,坚决在规定时间内保质保量地如期完成项目所有功能。(文:彭政睿)',
+      doc_id: 'c6075116c2a311ef82860242ac1b0006',
+      docnm_kwd: '《设备技术中心工作动态》2023年第8期.pdf',
+      highlight:
+        '了长达一个月的项目内部测试工作, 7月25日,项目组总结了工业互联网设备远程运维模块的改进和部署实施情况,并制定了 8月份的 bug <em>修复</em>',
+      image_id: '9aa72c1cc2a311ef8d280242ac1b0006-0ac501cf467e1f56',
+      important_kwd: [],
+      kb_id: '9aa72c1cc2a311ef8d280242ac1b0006',
+      positions: [[36, 87, 519, 312, 437]],
+      similarity: 0.8434534957462398,
+      term_similarity: 1,
+      vector_similarity: 0.47817831915413295
+    },
+    {
+      chunk_id: '49a3df09c2b1496d',
+      content_ltks:
+        '2023 年 11 月 在 万荣 站 220 千伏 gi 完成 相关 闸刀 机构 缺陷 修复 工作 后 电科院 对 220 千伏 gi 相关 间隔 安装 了 全站 域 无线 局 放 连续 监测 装置 监测数据 显示 部分 测点 存在 小幅 值 强 间歇性 的 特高频 局 放 信号 为 核实 复测 在线 监测数据 并 进行 定位 电科院 带电 检测 人员 于 2023 年 12 月 8 日 14 日 15 日 21 日 对 万荣 站 220 千伏 设',
+      content_with_weight:
+        '2023年11月,在万荣站220千伏GIS完成相关闸刀机构缺陷修复工作后,电科院对220千伏GIS相关间隔安装了全站域无线局放连续监测装置,监测数据显示部分测点存在小幅值强间歇性的特高频局放信号。为核实复测在线监测数据并进行定位,电科院带电检测人员于2023 年12 月8 日、14 日-15日、21日,对万荣站220千伏设',
+      doc_id: 'fef62ff8ea9711efa8250242ac120006',
+      docnm_kwd: '《设备技术中心工作动态》2024年第1期.pdf',
+      highlight:
+        '2023年11月在万荣站 220千伏gi完成相关闸刀机构缺陷<em>修复</em>工作后电科院对 220千伏gi相关间隔安装了全站域无线局放连续监测装置监测数据',
+      image_id: 'da47dcbaea9711ef94400242ac120006-49a3df09c2b1496d',
+      important_kwd: [],
+      kb_id: 'da47dcbaea9711ef94400242ac120006',
+      positions: [[21, 335, 522, 178, 361]],
+      similarity: 0.8422425543227802,
+      term_similarity: 1,
+      vector_similarity: 0.47414184774260076
+    },
+    {
+      chunk_id: '947fe2f63e6589e4',
+      content_ltks:
+        '院 现场 支撑 500 kv 三林 站 220 kv … gi 母线 消 缺 工作 , 对 拆解 下 的 母线 导体 连接结构 进行 查看 。 设备部 现场 组织 召开 了 情况 分析 会 和 母线 三相 电流 不 平衡 讨论会 , 针对 母线 出现 三相 不 平衡 电流 , 如何 快速 查找 可疑 位置 的 问题 , 电科院 结合 现有 计算 分析方法 , 提出 了 用 电流 绝对值 代替 向 量值 的 计算 方案 。 为 保障 本次 设备 的 顺利 投运 , 电科院 于 11 月 23-24 日 该站 gi 设备 耐压 期间 , 同步 利用 开关设备 交流 耐压 试验 放电 故障 定位系统 , 以及 声学 可视化 击穿 定位系统 进行 耐压 闪络 定位 监测 , 测试 过程 中 未 发现异常 信号 , 设备 顺利 投运 。 11 月 29 日 , 公司 设备部 组织 电科院 、 超高压 、 厂家 等 各 单位 于 电科院 开展 故障 间隔 解体 分析 , 国网 设备部 、 中国 电科院 远程 出席 并 全过程 见证 , 特邀 专家 刘 兆 林 参会 。 会上 , 运维 单位 和 厂家 分别 介绍 了 故障 gi 修复 情况 、 解体 方案 , 电科院 对 复现 试验 方案 进行 了 汇报 。 解体 发现 熔融 金属 来源于 触 指 弹簧 。 国网 设备部 、 中国 电科院 和 参会 专家 结合 解体 情况 对 故障 原因 讨论 和 明确 , 同时 认为 该 事件 是 一起 典型 的 设备 自身 缺陷 导致 的 故障 。 为 对 故障 根源 进行 进一步 验证 , 电科院 将 于 故障 解体 分析 后 , 对 故障 gi 母线 气室 开展 触头 接触不良 、 发热 烧熔 的 复现 试验 。 ( 文 / 图 : 刘 忠 岳 )',
+      content_with_weight:
+        '院现场支撑500kV三林站220kV…GIS母线消缺工作,对拆解下的母线导体连接结构进行查看。设备部现场组织召开了情况分析会和母线三相电流不平衡讨论会,针对母线出现三相不平衡电流,如何快速查找可疑位置的问题,电科院结合现有计算分析方法,提出了用电流绝对值代替向量值的计算方案。为保障本次设备的顺利投运,电科院于11月23-24日该站GIS设备耐压期间,同步利用开关设备交流耐压试验放电故障定位系统,以及声学可视化击穿定位系统进行耐压闪络定位监测,测试过程中未发现异常信号,设备顺利投运。11月29日,公司设备部组织电科院、超高压、厂家等各单位于电科院开展故障间隔解体分析,国网设备部、中国电科院远程出席并全过程见证,特邀专家刘兆林参会。会上,运维单位和厂家分别介绍了故障GIS修复情况、解体方案,电科院对复现试验方案进行了汇报。解体发现熔融金属来源于触指弹簧。国网设备部、中国电科院和参会专家结合解体情况对故障原因讨论和明确,同时认为该事件是一起典型的设备自身缺陷导致的故障。为对故障根源进行进一步验证,电科院将于故障解体分析后,对故障GIS母线气室开展触头接触不良、发热烧熔的复现试验。(文/图:刘忠岳)',
+      doc_id: 'cb5a8c5ac2a311ef82860242ac1b0006',
+      docnm_kwd: '《设备技术中心工作动态》2023年第12期.pdf',
+      highlight:
+        '会上,运维单位和厂家分别介绍了故障gi <em>修复</em>情况、解体方案,电科院对复现试验方案进行了汇报。解体发现熔融金属来源于触指弹簧。',
+      image_id: '9aa72c1cc2a311ef8d280242ac1b0006-947fe2f63e6589e4',
+      important_kwd: [],
+      kb_id: '9aa72c1cc2a311ef8d280242ac1b0006',
+      positions: [[11, 88, 520, 312, 571]],
+      similarity: 0.8385149276383687,
+      term_similarity: 1,
+      vector_similarity: 0.46171642546122904
+    },
+    {
+      chunk_id: '109e6d02f73f96ae',
+      content_ltks:
+        '推进 工业 互联网 设备 远程 运维 测试 及 完善 工作 8 月 22 日 下午 , 工业 互联网 项目组 在 汇泰 大楼 现场 召开 “ 工业 互联网 设备 远程 运维 建设 推进 会 ” 。 此次 会议 参会 公司 为 电科院 、 南瑞 信通 、 北京 信 普达 、 上海 翊 邦 项目组 的 相关 负责人 , 项目组 各 建设部门 汇报 了 工业 互联网 设备 远程 运维 功能 改进 和 部署 实施 情况 。 8 月前 半月 , 项目组 按 详细 设计方案 归纳 出 了 项目 的 测试 大纲 , 并 根据 项目 大纲 进行 了 全面 的 测试 。 结果显示 , 除去 与 i 6000 相关 的 人员 权限 之类 的 功能 , 其余 大部分 功能 已 建设 完成 。 但 在 数据 方面 , 由于 pm 数据 质量 及其 他 问题 , 导致 项目 获取 的 数据 质量 仍然 不 完善 , 后续 将 针对 当前 已 测试 出 的 数据 问题 和 归集 共享 页面 的 问题 进行 改进 。 截至 目前 , i 6000 接入 测试 已 完成 , 上线 试运行 资料 已 提交 , 整个 设备 远程 运维 平台 具备 了 上线 试运行 部署 条件 。 在 南瑞 和 科大 企业 节点 对接 的 工作 上 , 因 国网 安全 攻防 演练 暂时 延后 , 演练 结束 后 , 项目组 将 协调 信通 公司 网 安室 对 相关 功能 进行 支撑 。 接下来 , 项目组 将 持续 关注 生产 环境 部署 实施 , 地址 转换 以及 pc 端 概况 页面 样式 修改 工作 , 确保 系统 功能 的 用户 使用 体验 , 不断 优化 业务 功能 , 并 针对 问题 清单 , 协调 各部 门 加快 问题 修复 。 ( 文 : 彭 政 睿 )',
+      content_with_weight:
+        '推进工业互联网设备远程运维测试及完善工作8月22日下午,工业互联网项目组在汇泰大楼现场召开“工业互联网设备远程运维建设推进会”。此次会议参会公司为电科院、南瑞信通、北京信普达、上海翊邦项目组的相关负责人,项目组各建设部门汇报了工业互联网设备远程运维功能改进和部署实施情况。8月前半月,项目组按详细设计方案归纳出了项目的测试大纲,并根据项目大纲进行了全面的测试。结果显示,除去与i6000相关的人员权限之类的功能,其余大部分功能已建设完成。但在数据方面,由于PMS数据质量及其他问题,导致项目获取的数据质量仍然不完善,后续将针对当前已测试出的数据问题和归集共享页面的问题进行改进。截至目前,i6000接入测试已完成,上线试运行资料已提交,整个设备远程运维平台具备了上线试运行部署条件。在南瑞和科大企业节点对接的工作上,因国网安全攻防演练暂时延后,演练结束后,项目组将协调信通公司网安室对相关功能进行支撑。接下来,项目组将持续关注生产环境部署实施,地址转换以及PC端概况页面样式修改工作,确保系统功能的用户使用体验,不断优化业务功能,并针对问题清单,协调各部门加快问题修复。(文:彭政睿)',
+      doc_id: 'c70bb142c2a311ef82860242ac1b0006',
+      docnm_kwd: '《设备技术中心工作动态》2023年第9期.pdf',
+      highlight:
+        '实施,地址转换以及pc端概况页面样式修改工作,确保系统功能的用户使用体验,不断优化业务功能,并针对问题清单,协调各部门加快问题<em>修复</em>',
+      image_id: '9aa72c1cc2a311ef8d280242ac1b0006-109e6d02f73f96ae',
+      important_kwd: [],
+      kb_id: '9aa72c1cc2a311ef8d280242ac1b0006',
+      positions: [
+        [49, 85, 517, 114, 132],
+        [49, 76, 508, 141, 380]
+      ],
+      similarity: 0.8375257435045269,
+      term_similarity: 1,
+      vector_similarity: 0.45841914501508996
+    },
+    {
+      chunk_id: '1579874bce44ab41',
+      content_ltks:
+        '3 月 27 日 - 31 日 , 根据 公司 设备部 要求 , 设备 中心 赴 吴江 变压器厂 开展 南桥 换 流变 返 厂 检修 技术 监督 工作 。 该台 南桥 换流 变因 产 氢 问题 , 于 2022 年 11 月 返 厂 检修 , 电科院 进行 全程 技术 监督 。 结合 此次 返 厂 检修 机会 , 电科院 开展 了 换 流变 寿命 评估 相关 工作 , 对 绝缘纸 、 绝缘油 、 密封件 、 继电器 、 表计 等 组 部件 进行 取样 检测 , 评估 老化 状态 。 29 日 , 上海 公司 在 吴江 变压器厂 组织 召开 南桥 换 流变 检修 总结会 , 会议 对 南桥 换 流变 检修 过程 中 发现 的 问题 , 修复 处理 措施 , 以及 产 气 原因 进行 了 讨论 分析 , 明确 了 发货 时间 及 检修 总结报告 编制 要求 。 目前 , 该换 流变 已 顺利 通过 出厂 试验 , 具备 发往 现场 条件 。 待 换 流变 运至 现场 后 , 电科院 将 对 现场 安装 过程 进行 监督 , 并 开展 局 放 试验 , 以 验证 其 绝缘 状态 是否 良好 。 ( 文 : 崔 律 )',
+      content_with_weight:
+        '3月27日-31日,根据公司设备部要求,设备中心赴吴江变压器厂开展南桥换流变返厂检修技术监督工作。该台南桥换流变因产氢问题,于2022年11月返厂检修,电科院进行全程技术监督。结合此次返厂检修机会,电科院开展了换流变寿命评估相关工作,对绝缘纸、绝缘油、密封件、继电器、表计等组部件进行取样检测,评估老化状态。29日,上海公司在吴江变压器厂组织召开南桥换流变检修总结会,会议对南桥换流变检修过程中发现的问题,修复处理措施,以及产气原因进行了讨论分析,明确了发货时间及检修总结报告编制要求。目前,该换流变已顺利通过出厂试验,具备发往现场条件。待换流变运至现场后,电科院将对现场安装过程进行监督,并开展局放试验,以验证其绝缘状态是否良好。(文:崔律)',
+      doc_id: 'c3257f7cc2a311ef82860242ac1b0006',
+      docnm_kwd: '《设备技术中心工作动态》2023年第4期.pdf',
+      highlight:
+        '29日,上海公司在吴江变压器厂组织召开南桥换流变检修总结会,会议对南桥换流变检修过程中发现的问题, <em>修复</em>处理措施,以及产气原因进行',
+      image_id: '9aa72c1cc2a311ef8d280242ac1b0006-1579874bce44ab41',
+      important_kwd: [],
+      kb_id: '9aa72c1cc2a311ef8d280242ac1b0006',
+      positions: [[35, 76, 507, 542, 706]],
+      similarity: 0.8369589835828704,
+      term_similarity: 1,
+      vector_similarity: 0.45652994527623464
+    },
+    {
+      chunk_id: 'd466025984fd23ec',
+      content_ltks:
+        '为了 有序 推进 了 相关 改造 工作 4 月 12 日 国网 江苏省 电力 有限公司 超高压 公司 组织 召开 无锡 500 kv 岷 珠 变 5220 线 5042 流变 改造 方案 评审会 相关 与会 单位 审查 并 通过 了 上述 流变 的 改造 方案 本次 涉及 造 的 流变 为 阿海 珐 输配电 上海 互感器 有限公司 生产 运行 时间 已有 12 年 设备 老 旧 绝缘 水平 低 且 此 型号 设备 具有 家族 性 缺陷 无法 修复 影响 电网 供电 可靠性 根据 国家电网 设备 2024 7 号 国家电网 有限公司 关于 开展 2024 年 电网 设备 重点 隐患 排查 治理 工作 的 通知 的 最新 要求 上海 雷 兹 阿海 珐 流变 投运 15 年 以上 油 漫 正 立式 流变 缺陷 批次 mwb 流变 2006 年 11 12 月 出厂 需 在 2024 年底 前 完成 治理',
+      content_with_weight:
+        '为了有序推进了相关改造工作,4月12日,国网江苏省电力有限公司超高压公司组织召开无锡500kV岷珠变5220线5042流变改造方案评审会,相关与会单位审查并通过了上述流变的改造方案。本次涉及造的流变为阿海珐输配电(上海)互感器有限公司生产,运行时间已有12年,设备老旧,绝缘水平低,且此型号设备具有家族性缺陷,无法修复,影响电网供电可靠性。根据国家电网设备(2024)7号《国家电网有限公司关于开展2024年电网设备重点隐患排查治理工作的通知》的最新要求,上海雷兹(阿海珐)流变、投运15年以上油漫正立式流变、缺陷批次…MWB…流变(2006年11-12月出厂)需在2024年底前完成治理。',
+      doc_id: '058e6a60ea9811efa8250242ac120006',
+      docnm_kwd: '《设备技术中心工作动态》2024年第5期.pdf',
+      highlight:
+        '的改造方案本次涉及造的流变为阿海珐输配电上海互感器有限公司生产运行时间已有12年设备老旧绝缘水平低且此型号设备具有家族性缺陷无法<em>修复</em>',
+      image_id: 'da47dcbaea9711ef94400242ac120006-d466025984fd23ec',
+      important_kwd: [],
+      kb_id: 'da47dcbaea9711ef94400242ac120006',
+      positions: [[32, 74, 510, 615, 761]],
+      similarity: 0.8347282031212104,
+      term_similarity: 1,
+      vector_similarity: 0.44909401040403474
+    }
+  ],
+  doc_aggs: [
+    {
+      count: 3,
+      doc_id: 'cb5a8c5ac2a311ef82860242ac1b0006',
+      doc_name: '《设备技术中心工作动态》2023年第12期.pdf'
+    },
+    {
+      count: 2,
+      doc_id: 'fef62ff8ea9711efa8250242ac120006',
+      doc_name: '《设备技术中心工作动态》2024年第1期.pdf'
+    },
+    {
+      count: 1,
+      doc_id: 'c57a96e0c2a311ef82860242ac1b0006',
+      doc_name: '《设备技术中心工作动态》2023年第8期 - 副本.pdf'
+    },
+    {
+      count: 1,
+      doc_id: 'c6075116c2a311ef82860242ac1b0006',
+      doc_name: '《设备技术中心工作动态》2023年第8期.pdf'
+    },
+    {
+      count: 1,
+      doc_id: 'c70bb142c2a311ef82860242ac1b0006',
+      doc_name: '《设备技术中心工作动态》2023年第9期.pdf'
+    },
+    {
+      count: 1,
+      doc_id: 'c3257f7cc2a311ef82860242ac1b0006',
+      doc_name: '《设备技术中心工作动态》2023年第4期.pdf'
+    },
+    {
+      count: 1,
+      doc_id: '058e6a60ea9811efa8250242ac120006',
+      doc_name: '《设备技术中心工作动态》2024年第5期.pdf'
+    }
+  ],
+  labels: null,
+  total: 20
+};
+const { fetchRelatedQuestions, data: relatedQuestionsData } = useFetchRelatedQuestions();
+const { pagination, setPagination } = useGetPaginationWithRouter();
+
+const sendQuestion = async (question) => {
+  const q = trim(question);
+  if (isEmpty(q)) return;
+
+  setPagination({ page: 1 });
+  isFirstRender.value = false;
+  sendingLoading.value = true;
+  send({ kb_ids: checkedWithoutEmbeddingIdList.value, question: q });
+  testChunk({
+    kb_id: checkedWithoutEmbeddingIdList.value,
+    highlight: true,
+    question: q,
+    page: 1,
+    size: pagination.pageSize
+  });
+
+  await fetchRelatedQuestions(q);
+};
+
+const handleClickRelatedQuestion = (question) => {
+  if (sendingLoading.value) return;
+  searchStr.value = question;
+  sendQuestion(question);
+};
+
+const handleTestChunk = (documentIds, page = 1, size = 10) => {
+  const q = trim(searchStr.value);
+  if (sendingLoading.value || isEmpty(q)) return;
+
+  testChunk({
+    kb_id: checkedWithoutEmbeddingIdList.value,
+    highlight: true,
+    question: q,
+    doc_ids: documentIds ?? selectedDocumentIds.value,
+    page,
+    size
+  });
+};
+
+watch(done, (newDone) => {
+  if (newDone) sendingLoading.value = false;
+});
 
 const handleDocumentClick = (docId, item) => {
   console.log('文档点击:', docId, item);
+  drawerTitle.value = '文档详情';
+  drawerVisible.value = true;
+};
+
+const handleFloatingBtnClick = () => {
+  drawerTitle.value = '思维导图';
+  drawerVisible.value = true;
+};
+
+const onChange = (pageNumber, pageSize) => {
+  pagination.onChange?.(pageNumber, pageSize);
+  handleTestChunk(selectedDocumentIds.value, pageNumber, pageSize);
+};
+
+const handleUpdateCheckedList = (newCheckedList) => {
+  checkedList.value = newCheckedList;
+};
+
+const initSidebar = (initCheckedList) => {
+  checkedList.value = initCheckedList;
+};
+
+const onSearch = (value) => {
+  sendQuestion(value);
+  searchActive.value = true;
 };
 </script>
 
@@ -72,8 +422,10 @@ const handleDocumentClick = (docId, item) => {
   padding: 16px;
   overflow-y: auto;
   width: 100%;
-  padding: 20px 25% 10px;
+  padding: 20px 15% 10px;
   transition: padding-top 0.3s ease;
+  line-height: 1.6; /* 调整行间距 */
+  letter-spacing: 0.5px; /* 调整字间距 */
 }
 
 .content-box.search-active {
@@ -95,7 +447,6 @@ const handleDocumentClick = (docId, item) => {
 }
 
 :deep(.arco-input-wrapper) {
-  width: 500px;
   height: 50px;
   border-top-left-radius: 18px;
   border-bottom-left-radius: 18px;
@@ -138,4 +489,13 @@ const handleDocumentClick = (docId, item) => {
 .floating-btn:hover {
   transform: scale(1.1);
 }
+
+.related-question-tag {
+  cursor: pointer;
+  transition: background-color 0.3s ease;
+}
+
+.related-question-tag:hover {
+  background-color: var(--color-neutral-4);
+}
 </style>

+ 2 - 1
vite.config.mjs

@@ -49,7 +49,8 @@ export default defineConfig(({ mode }) => {
           rewrite: (path) => path.replace(/^\/dify/, '')
         },
         '/rgflow': {
-          target: 'http://192.168.20.119:28002/',
+          // target: 'http://192.168.20.119:28002/',
+          target: 'http://smartai.com:8294/',
           changeOrigin: true,
           secure: false,
           ws: true,

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff