Explorar o código

[Feat 0000]添加插入会话功能

wangkeyi hai 2 días
pai
achega
2262d0ca2e

+ 27 - 10
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -98,13 +98,14 @@
           :initialThinkLevel="thinkLevel"
           :initialEditMode="editMode"
           :taskList="taskList"
+          :currentTaskId="currentTaskId"
           @send="handleSendMessage"
           @file-upload="triggerFileUpload"
           @file-change="handleFileUpload"
           @remove-pending-file="removePendingFile"
           @think-level-change="handleThinkLevelChange"
           @edit-mode-change="handleEditModeChange"
-          @insert-session="handleInsertSession"
+          @update:selected-sessions="handleUpdateSelectedSessions"
         />
       </div>
 
@@ -333,6 +334,7 @@
 
   const inputMessage = ref('');
   const loading = ref(false);
+  const selectedSessions = ref<Array<{ id: string; name: string }>>([]);
 
   const getCurrentSessionId = (): string => {
     const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
@@ -444,8 +446,13 @@
     }
   };
 
-  const handleInsertSession = (_taskId: string, _taskName: string) => {
-    // 会话引用已由 ChatInputArea 内部以 chip 形式管理
+  const handleUpdateSelectedSessions = (sessions: Array<{ id: string; name: string }>) => {
+    selectedSessions.value = sessions;
+  };
+
+  const getSessionRefIconUrl = () => {
+    const val = getComputedStyle(document.documentElement).getPropertyValue('--img-chat-popup-session-icon').trim();
+    return val.replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
   };
 
   const handleEditModeChange = async (modeKey: string) => {
@@ -771,11 +778,21 @@
       return;
     }
 
-    const userInput = inputMessage.value.trim();
+    const sessionPrefix = selectedSessions.value.length > 0 ? selectedSessions.value.map((s) => `#session:${s.id}`).join(' ') + ' ' : '';
+    const userInput = sessionPrefix + inputMessage.value.trim();
+
+    let displayContent = inputMessage.value.trim();
+    if (selectedSessions.value.length > 0) {
+      const names = selectedSessions.value.map((s) => s.name).join('、');
+      const iconUrl = getSessionRefIconUrl();
+      displayContent = `[<img src="${iconUrl}" class="session-ref-icon" /> 引用 ${selectedSessions.value.length} 个会话:${names}] ${displayContent}`;
+    }
+
     const fileToSend = pendingFile.value;
 
     inputMessage.value = '';
     pendingFile.value = null;
+    selectedSessions.value = [];
 
     try {
       loading.value = true;
@@ -783,9 +800,9 @@
       chatMessagesRef.value?.forceScrollToBottom();
 
       if (hasFile) {
-        await sendWithAttachment(fileToSend!, userInput);
+        await sendWithAttachment(fileToSend!, userInput, displayContent);
       } else {
-        await sendTextOnly(userInput);
+        await sendTextOnly(userInput, displayContent);
       }
     } catch (error) {
       console.error('发送消息失败:', error);
@@ -1125,13 +1142,13 @@
     }
   };
 
-  const sendTextOnly = async (userInput: string) => {
+  const sendTextOnly = async (userInput: string, displayContent?: string) => {
     const originTaskId = currentTaskId.value;
 
     const now = dayjs();
     const userMsg: Message = {
       type: 'user',
-      content: userInput,
+      content: displayContent || userInput,
       time: now.format('HH:mm'),
       createdAt: now.toISOString(),
     };
@@ -1191,7 +1208,7 @@
     }
   };
 
-  const sendWithAttachment = async (file: AttachedFile, userInput: string) => {
+  const sendWithAttachment = async (file: AttachedFile, userInput: string, displayContent?: string) => {
     const originTaskId = currentTaskId.value;
     let currentTask = taskList.value.find((t) => t.id === originTaskId);
     if (currentTask) {
@@ -1203,7 +1220,7 @@
     const now = dayjs();
     const userMsg: Message = {
       type: 'user',
-      content: userInput || `上传了文件:${file.name}`,
+      content: displayContent || userInput || `上传了文件:${file.name}`,
       time: now.format('HH:mm'),
       createdAt: now.toISOString(),
       attachedFile: file,

+ 95 - 6
src/views/ventAI/manageAssistent/components/chatModal/ChatInputArea.vue

@@ -198,20 +198,34 @@
       </div>
       <!-- 会话列表弹框 -->
       <div v-if="showSessionPopup" ref="sessionPopupRef" class="session-popup" @click.stop @pointerdown.stop>
+        <div class="session-popup-header">
+          <span>选择会话</span>
+          <span class="session-count">{{ selectedSessions.length }}/5</span>
+        </div>
         <div class="session-list">
           <div
             v-for="(task, index) in filteredSessionList"
             :key="task.id"
             class="session-item"
-            :class="{ active: index === activeSessionIndex }"
+            :class="{
+              active: index === activeSessionIndex,
+              selected: isSessionSelected(task.id),
+              disabled: (selectedSessions.length >= 5 && !isSessionSelected(task.id)) || task.id === props.currentTaskId,
+            }"
             @click="handleSelectSession(task)"
           >
+            <div class="session-checkbox" :class="{ checked: isSessionSelected(task.id) }">
+              <span v-if="isSessionSelected(task.id)" class="check-mark">✓</span>
+            </div>
             <div class="session-item-icon"></div>
             <span class="session-item-name">{{ task.name }}</span>
           </div>
           <div v-if="filteredSessionList.length === 0" class="session-empty">暂无匹配会话</div>
         </div>
-        <div class="session-popup-footer">输入内容以搜索对话</div>
+        <div class="session-popup-footer">
+          <span v-if="selectedSessions.length >= 5" class="max-hint">最多选择5个会话</span>
+          <span v-else>点击选择会话(最多5个)</span>
+        </div>
       </div>
     </div>
   </div>
@@ -236,6 +250,7 @@
       initialThinkLevel?: string;
       initialEditMode?: string;
       taskList?: Task[];
+      currentTaskId?: string;
     }>(),
     {
       contextUsed: 0,
@@ -245,6 +260,7 @@
       initialThinkLevel: 'off',
       initialEditMode: 'full',
       taskList: () => [],
+      currentTaskId: '',
     }
   );
 
@@ -257,7 +273,7 @@
     (e: 'model-change', modelKey: string): void;
     (e: 'think-level-change', levelKey: string): void;
     (e: 'edit-mode-change', modeKey: string): void;
-    (e: 'insert-session', taskId: string, taskName: string): void;
+    (e: 'update:selectedSessions', sessions: Array<{ id: string; name: string }>): void;
   }>();
 
   const handleSelectModel = (item: ModelOption) => {
@@ -444,9 +460,14 @@
   const showSessionPopup = ref(false);
   const activeSessionIndex = ref(0);
   const selectedSessions = ref<Array<{ id: string; name: string }>>([]);
+  const sessionPopupManual = ref(false);
   let isRemovingTag = false;
   let sessionJustSelected = false;
 
+  const isSessionSelected = (sessionId: string) => {
+    return selectedSessions.value.some((s) => s.id === sessionId);
+  };
+
   const sessionSearchText = computed(() => {
     const val = props.modelValue || '';
     const hashIndex = val.lastIndexOf('#');
@@ -490,6 +511,7 @@
 
   const handleInsertSession = () => {
     showPopup.value = false;
+    sessionPopupManual.value = true;
     showSessionPopup.value = true;
     const cur = props.modelValue || '';
     const needsSpace = cur && !cur.endsWith(' ') ? ' ' : '';
@@ -501,10 +523,16 @@
   };
 
   const handleSelectSession = (task: Task) => {
-    selectedSessions.value = [{ id: task.id, name: task.name }];
-    showSessionPopup.value = false;
+    if (task.id === props.currentTaskId) return;
+    const idx = selectedSessions.value.findIndex((s) => s.id === task.id);
+    if (idx >= 0) {
+      selectedSessions.value.splice(idx, 1);
+    } else {
+      if (selectedSessions.value.length >= 5) return;
+      selectedSessions.value.push({ id: task.id, name: task.name });
+    }
     removeSearchTag();
-    emit('insert-session', task.id, task.name);
+    emit('update:selectedSessions', [...selectedSessions.value]);
   };
 
   const removeSearchTag = () => {
@@ -531,10 +559,12 @@
 
   const removeSession = (sessionId: string) => {
     selectedSessions.value = selectedSessions.value.filter((s) => s.id !== sessionId);
+    emit('update:selectedSessions', [...selectedSessions.value]);
   };
 
   const handleTextareaInput = (val: string) => {
     emit('update:modelValue', val);
+    if (sessionPopupManual.value) return;
     const hashIndex = val.lastIndexOf('#');
     if (hashIndex === -1) {
       showSessionPopup.value = false;
@@ -554,11 +584,13 @@
     if (selectedSessions.value.length > 0) {
       e.preventDefault();
       selectedSessions.value.pop();
+      emit('update:selectedSessions', [...selectedSessions.value]);
     }
   };
 
   onClickOutside(sessionPopupRef, () => {
     showSessionPopup.value = false;
+    sessionPopupManual.value = false;
   });
 
   const handleInsertCommand = () => {
@@ -1308,6 +1340,23 @@
       z-index: 1000;
       box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
 
+      .session-popup-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 10px 14px 8px;
+        font-size: 13px;
+        color: #e0e6ed;
+        border-bottom: 1px solid rgba(63, 80, 106, 0.3);
+        flex-shrink: 0;
+
+        .session-count {
+          font-size: 12px;
+          color: #3a8fd4;
+          font-weight: 500;
+        }
+      }
+
       .session-list {
         flex: 1;
         overflow-y: auto;
@@ -1330,6 +1379,42 @@
             background: rgba(10, 132, 255, 0.2);
           }
 
+          &.selected {
+            background: rgba(10, 132, 255, 0.12);
+          }
+
+          &.disabled {
+            opacity: 0.4;
+            cursor: not-allowed;
+
+            &:hover {
+              background: transparent;
+            }
+          }
+
+          .session-checkbox {
+            width: 16px;
+            height: 16px;
+            border: 1.5px solid rgba(255, 255, 255, 0.25);
+            border-radius: 3px;
+            flex-shrink: 0;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            transition: all 0.2s;
+
+            &.checked {
+              background: #3a8fd4;
+              border-color: #3a8fd4;
+            }
+
+            .check-mark {
+              font-size: 11px;
+              color: #fff;
+              line-height: 1;
+            }
+          }
+
           .session-item-icon {
             width: 18px;
             height: 18px;
@@ -1363,6 +1448,10 @@
         text-align: center;
         border-top: 1px solid rgba(63, 80, 106, 0.3);
         flex-shrink: 0;
+
+        .max-hint {
+          color: #faad14;
+        }
       }
     }
   }

+ 12 - 0
src/views/ventAI/manageAssistent/components/chatModal/ChatMessages.vue

@@ -741,6 +741,12 @@
             word-break: break-word;
             overflow-wrap: break-word;
 
+            :deep(.session-ref-icon) {
+              width: 14px;
+              height: 14px;
+              vertical-align: -2px;
+            }
+
             :deep(h1),
             :deep(h2),
             :deep(h3),
@@ -978,6 +984,12 @@
             word-break: break-word;
             overflow-wrap: break-word;
 
+            :deep(.session-ref-icon) {
+              width: 14px;
+              height: 14px;
+              vertical-align: -2px;
+            }
+
             :deep(h1, h2, h3, h4, h5, h6) {
               color: #fff;
             }