Explorar el Código

[Mod 0000]修改计划模式的审批规则,实现多段式审批

wangkeyi hace 3 semanas
padre
commit
fabbe6e652

+ 8 - 4
src/views/ventAI/manageAssistent/api.ts

@@ -278,22 +278,26 @@ export const reviewPdfStream = async (
  * 恢复会话接口(SSE流式响应)
  * @param params.session_id - 会话ID,必填
  * @param params.thread_id - LangGraph线程ID,可选
- * @param params.action - "approve"(批准)或 "reject"(拒绝),必填
+ * @param params.status - ask_user响应状态:"answered"(默认,消费answers)/ "cancelled"(取消)/ "error"(出错)
+ * @param params.answers - ask_user答案列表,与中断事件ask_user.questions一一对应(status=answered时必填)
  * @param onChunk - 流式数据回调函数
  * @returns Promise<{ session_id: string }>
  */
 export const chatResumeStream = async (
-  params: { session_id: string; thread_id?: string; action: string; signal?: AbortSignal },
+  params: { session_id: string; thread_id?: string; status?: string; answers?: string[]; signal?: AbortSignal },
   onChunk: (chunk: string) => void
 ): Promise<{ session_id: string }> => {
   try {
-    const body: Record<string, string> = {
+    const body: Record<string, any> = {
       session_id: params.session_id,
-      action: params.action,
+      status: params.status || 'answered',
     };
     if (params.thread_id) {
       body.thread_id = params.thread_id;
     }
+    if (params.answers) {
+      body.answers = params.answers;
+    }
 
     const response = await fetch(Api.chatResume, {
       method: 'POST',

+ 59 - 72
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -74,8 +74,8 @@
             :messages="messages"
             @file-preview="openFilePreview"
             @download-word="downloadWordFile"
-            @approve="handleApprove"
-            @reject="handleReject"
+            @ask-user-submit="handleAskUserSubmit"
+            @ask-user-cancel="handleAskUserCancel"
           />
 
           <!-- 下载确认条 -->
@@ -689,63 +689,63 @@
     }
   };
 
-  const handleApprove = async () => {
-    // 优先使用当前任务的审批状态,如果没有则查找任何有待审批状态的任务
-    let pendingApproval = currentPendingApproval.value;
-    if (!pendingApproval) {
-      // 查找任何有待审批状态的任务
-      for (const [taskId, approval] of taskPendingApprovals.value.entries()) {
-        pendingApproval = approval;
-        // 切换到该任务以便用户看到审批界面
-        if (currentTaskId.value !== taskId) {
-          await switchTask(taskId);
-        }
-        break;
+  const findPendingApprovalTask = async (): Promise<{ taskId: string; approval: { sessionId: string; threadId?: string } } | null> => {
+    let approval = currentPendingApproval.value;
+    if (approval) return { taskId: currentTaskId.value, approval };
+    for (const [taskId, a] of taskPendingApprovals.value.entries()) {
+      if (currentTaskId.value !== taskId) {
+        await switchTask(taskId);
       }
+      return { taskId, approval: a };
     }
-    if (!pendingApproval) return;
+    return null;
+  };
 
-    const sessionId = pendingApproval.sessionId;
+  const handleAskUserSubmit = async (answers: string[]) => {
+    const found = await findPendingApprovalTask();
+    if (!found) return;
+
+    const { approval } = found;
+    const sessionId = approval.sessionId;
     if (!sessionId) return;
 
-    const approveTask = taskList.value.find((t) => t.sessionId === sessionId);
-    if (!approveTask) return;
+    const task = taskList.value.find((t) => t.sessionId === sessionId);
+    if (!task) return;
 
-    const taskMessages = approveTask.messages;
+    const taskMessages = task.messages;
     const lastMsg = taskMessages[taskMessages.length - 1];
     if (!lastMsg || !lastMsg.isPendingApproval) return;
 
-    // 复用同一条消息继续渲染:清除审批状态,恢复 loading,重置前端计时起点
     lastMsg.isPendingApproval = false;
     lastMsg.pendingApprovalData = undefined;
     lastMsg.isLoading = true;
     lastMsg.generateStartTime = Date.now();
-    // thinkingSteps、thinkingContent、baseDurationMs 保持不变,后续继续累积
 
     const aiMsgIndex = taskMessages.length - 1;
-    approveTask.messages = [...taskMessages];
-    if (currentTaskId.value === approveTask.id) {
-      messages.value = approveTask.messages;
+    task.messages = [...taskMessages];
+    if (currentTaskId.value === task.id) {
+      messages.value = task.messages;
     }
 
-    approveTask.isStreaming = true;
+    task.isStreaming = true;
     showTodoBar.value = false;
     currentTodos.value = [];
 
     const controller = new AbortController();
-    setTaskAbortController(approveTask.id, controller);
+    setTaskAbortController(task.id, controller);
 
     try {
       const result = await chatResumeStream(
         {
           session_id: sessionId,
-          thread_id: pendingApproval?.threadId || undefined,
-          action: 'approve',
+          thread_id: approval.threadId || undefined,
+          status: 'answered',
+          answers,
           signal: controller.signal,
         },
         (chunk: string) => {
-          parseSSEData(chunk, aiMsgIndex, approveTask.id);
-          if (currentTaskId.value === approveTask.id) scrollToBottom();
+          parseSSEData(chunk, aiMsgIndex, task.id);
+          if (currentTaskId.value === task.id) scrollToBottom();
         }
       );
 
@@ -754,85 +754,69 @@
       }
     } catch (error) {
       if ((error as Error)?.name === 'AbortError') {
-        // 用户主动停止生成
         lastMsg.isLoading = false;
-        // 后端未返回耗时,冻结前端伪计时
         lastMsg.durationMs = (lastMsg.baseDurationMs || 0) + (lastMsg.generateStartTime ? Date.now() - lastMsg.generateStartTime : 0);
         if (!lastMsg.content) {
           lastMsg.content = '已停止生成。';
         }
       } else {
-        console.error('审批继续执行失败:', error);
-        message.error('审批失败,请重试');
+        console.error('提交答案失败:', error);
+        message.error('提交失败,请重试');
         lastMsg.isLoading = false;
         if (!lastMsg.content) {
-          lastMsg.content = '抱歉,审批请求失败,请稍后重试。';
+          lastMsg.content = '抱歉,提交请求失败,请稍后重试。';
         }
       }
     } finally {
-      setTaskAbortController(approveTask.id, null);
-      approveTask.isStreaming = false;
-      clearTaskPendingApproval(approveTask.id);
-      if (currentTaskId.value === approveTask.id) {
+      setTaskAbortController(task.id, null);
+      task.isStreaming = false;
+      clearTaskPendingApproval(task.id);
+      if (currentTaskId.value === task.id) {
         await scrollToBottom();
       }
     }
   };
 
-  const handleReject = async () => {
-    // 优先使用当前任务的审批状态,如果没有则查找任何有待审批状态的任务
-    let pendingApproval = currentPendingApproval.value;
-    if (!pendingApproval) {
-      // 查找任何有待审批状态的任务
-      for (const [taskId, approval] of taskPendingApprovals.value.entries()) {
-        pendingApproval = approval;
-        // 切换到该任务以便用户看到审批界面
-        if (currentTaskId.value !== taskId) {
-          await switchTask(taskId);
-        }
-        break;
-      }
-    }
-    if (!pendingApproval) return;
+  const handleAskUserCancel = async () => {
+    const found = await findPendingApprovalTask();
+    if (!found) return;
 
-    const sessionId = pendingApproval.sessionId;
+    const { approval } = found;
+    const sessionId = approval.sessionId;
     if (!sessionId) return;
 
-    const rejectTask = taskList.value.find((t) => t.sessionId === sessionId);
-    if (!rejectTask) return;
+    const task = taskList.value.find((t) => t.sessionId === sessionId);
+    if (!task) return;
 
-    const taskMessages = rejectTask.messages;
+    const taskMessages = task.messages;
     const lastMsg = taskMessages[taskMessages.length - 1];
     if (!lastMsg || !lastMsg.isPendingApproval) return;
 
-    // 复用同一条消息:清除审批状态,标记为已拒绝
     lastMsg.isPendingApproval = false;
     lastMsg.pendingApprovalData = undefined;
     lastMsg.isLoading = false;
-    lastMsg.content = (lastMsg.content ? lastMsg.content + '\n\n' : '') + '已拒绝执行计划。';
-
-    rejectTask.messages = [...taskMessages];
+    lastMsg.content = (lastMsg.content ? lastMsg.content + '\n\n' : '') + '已取消提问。';
 
-    const threadId = pendingApproval?.threadId || undefined;
-    clearTaskPendingApproval(rejectTask.id);
+    task.messages = [...taskMessages];
+    clearTaskPendingApproval(task.id);
 
     try {
       await chatResumeStream(
         {
           session_id: sessionId,
-          thread_id: threadId,
-          action: 'reject',
+          thread_id: approval.threadId || undefined,
+          status: 'cancelled',
         },
         () => {}
       );
     } catch (error) {
-      console.error('拒绝操作失败:', error);
+      console.error('取消操作失败:', error);
     }
 
-    rejectTask.isStreaming = false;
+    task.isStreaming = false;
 
-    if (currentTaskId.value === rejectTask.id) {
-      messages.value = rejectTask.messages;
+    if (currentTaskId.value === task.id) {
+      messages.value = task.messages;
       await scrollToBottom();
     }
   };
@@ -1383,17 +1367,20 @@
         const elapsedMs = (aiMsg.baseDurationMs || 0) + (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
         aiMsg.baseDurationMs = elapsedMs;
         aiMsg.isLoading = false;
-        // 流已结束,等待审批;批准继续时 handleApprove 会重新置为流式中
+        // 流已结束,等待用户回答;提交答案后 handleAskUserSubmit 会重新置为流式中
         streamingTask.isStreaming = false;
         aiMsg.isPendingApproval = true;
         const interruptSessionId = data.session_id || streamingTask.sessionId;
         aiMsg.pendingApprovalData = {
-          message: data.message || '智能体制定了执行计划,等待您的审批...',
+          message: data.message || '智能体需要您回答一些问题以继续...',
           sessionId: interruptSessionId,
           threadId: data.thread_id,
           interrupts: data.interrupts,
           elapsedMs,
           prevThinkingContent: aiMsg.thinkingContent,
+          plan: data.plan,
+          ask_user: data.ask_user,
+          answers: data.ask_user?.questions?.map(() => '') || [],
         };
         setTaskPendingApproval(taskId, interruptSessionId, data.thread_id || '');
         break;

+ 331 - 16
src/views/ventAI/manageAssistent/components/chatModal/ChatMessages.vue

@@ -111,15 +111,99 @@
               <span class="dot"></span>
             </div>
 
-            <!-- 计划审批 UI -->
-            <div v-if="message.isPendingApproval" class="approval-card">
+            <!-- ask_user 交互提问 UI(分步向导) -->
+            <div v-if="message.isPendingApproval && message.pendingApprovalData?.ask_user" class="approval-card">
               <div class="approval-icon"></div>
               <div class="approval-content">
-                <div class="approval-title">执行计划待审批</div>
-                <div class="approval-desc">{{ message.pendingApprovalData?.message }}</div>
+                <div class="approval-title">{{ message.pendingApprovalData?.message }}</div>
+
+                <!-- 计划内容 -->
+                <div v-if="message.pendingApprovalData?.plan" class="approval-plan">
+                  <div class="approval-plan-label">执行计划</div>
+                  <div class="approval-plan-content">{{ message.pendingApprovalData.plan }}</div>
+                </div>
+
+                <!-- 步骤指示器 -->
+                <div v-if="message.pendingApprovalData.ask_user.questions.length > 1" class="ask-user-stepper">
+                  <div
+                    v-for="(_, sIdx) in message.pendingApprovalData.ask_user.questions"
+                    :key="sIdx"
+                    :class="['ask-user-step-dot', {
+                      active: sIdx === (askUserCurrentStep[index] ?? 0),
+                      completed: sIdx < (askUserCurrentStep[index] ?? 0) || !!(askUserAnswers[index]?.[sIdx] && askUserAnswers[index][sIdx].trim())
+                    }]"
+                    @click="askUserCurrentStep[index] = sIdx"
+                  ></div>
+                  <span class="ask-user-step-text">{{ (askUserCurrentStep[index] ?? 0) + 1 }} / {{ message.pendingApprovalData.ask_user.questions.length }}</span>
+                </div>
+
+                <!-- 当前问题 -->
+                <div
+                  v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0]"
+                  class="ask-user-question"
+                >
+                  <div class="ask-user-question-text">
+                    {{ message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].question }}
+                    <span v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].required !== false" class="ask-user-required">*</span>
+                  </div>
+
+                  <!-- multiple_choice 类型 -->
+                  <div v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].type === 'multiple_choice'" class="ask-user-choices">
+                    <div
+                      v-for="(choice, cIdx) in message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].choices"
+                      :key="cIdx"
+                      :class="['ask-user-choice', { selected: askUserAnswers[index]?.[askUserCurrentStep[index] ?? 0] === getChoiceValue(choice) && !askUserOtherSelected[index]?.[askUserCurrentStep[index] ?? 0] }]"
+                      @click="selectChoice(index, askUserCurrentStep[index] ?? 0, choice, message.pendingApprovalData.ask_user.questions.length)"
+                    >
+                      <span>{{ getChoiceValue(choice) }}</span>
+                    </div>
+                    <!-- Other 选项 -->
+                    <div
+                      :class="['ask-user-choice', { selected: askUserOtherSelected[index]?.[askUserCurrentStep[index] ?? 0] }]"
+                      @click="selectOther(index, askUserCurrentStep[index] ?? 0)"
+                    >
+                      <span>其他</span>
+                    </div>
+                    <input
+                      v-if="askUserOtherSelected[index]?.[askUserCurrentStep[index] ?? 0]"
+                      class="ask-user-other-input"
+                      placeholder="请输入..."
+                      :value="askUserAnswers[index]?.[askUserCurrentStep[index] ?? 0]"
+                      @input="askUserAnswers[index][askUserCurrentStep[index] ?? 0] = ($event.target as HTMLInputElement).value"
+                    />
+                  </div>
+
+                  <!-- text 类型 -->
+                  <div v-else-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].type === 'text'" class="ask-user-text">
+                    <textarea
+                      class="ask-user-textarea"
+                      placeholder="请输入..."
+                      :value="askUserAnswers[index]?.[askUserCurrentStep[index] ?? 0]"
+                      @input="askUserAnswers[index][askUserCurrentStep[index] ?? 0] = ($event.target as HTMLTextAreaElement).value"
+                    ></textarea>
+                  </div>
+                </div>
+
+                <!-- 操作按钮 -->
                 <div class="approval-actions">
-                  <button class="approval-btn approve-btn" @click="emit('approve')">同意</button>
-                  <button class="approval-btn reject-btn" @click="emit('reject')">拒绝</button>
+                  <button
+                    v-if="(askUserCurrentStep[index] ?? 0) > 0"
+                    class="approval-btn nav-btn"
+                    @click="goPrevStep(index)"
+                  >上一步</button>
+                  <button
+                    v-if="(askUserCurrentStep[index] ?? 0) < message.pendingApprovalData.ask_user.questions.length - 1"
+                    class="approval-btn approve-btn"
+                    :disabled="!isCurrentStepAnswered(index, askUserCurrentStep[index] ?? 0, message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0])"
+                    @click="goNextStep(index, message.pendingApprovalData.ask_user.questions.length)"
+                  >下一步</button>
+                  <button
+                    v-if="(askUserCurrentStep[index] ?? 0) === message.pendingApprovalData.ask_user.questions.length - 1"
+                    class="approval-btn approve-btn"
+                    :disabled="!isCurrentStepAnswered(index, askUserCurrentStep[index] ?? 0, message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0])"
+                    @click="handleSubmitAskUser(index)"
+                  >提交</button>
+                  <button class="approval-btn reject-btn" @click="handleCancelAskUser">取消</button>
                 </div>
               </div>
             </div>
@@ -177,7 +261,7 @@
 </template>
 
 <script setup lang="ts">
-  import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue';
+  import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from 'vue';
   import dayjs from 'dayjs';
   import { renderMarkdown, formatFileSize, tableHtmlToMarkdown, tableHtmlToCsv } from './utils';
   import type { Message, AttachedFile, ThinkingStep } from './types';
@@ -232,17 +316,94 @@
     return '';
   };
 
-  defineProps<{
+  const props = defineProps<{
     messages: Message[];
   }>();
 
+  // 自动初始化 ask_user 答案状态
+  watch(
+    () => props.messages,
+    (msgs) => {
+      msgs.forEach((msg, idx) => {
+        if (msg.isPendingApproval && msg.pendingApprovalData?.ask_user?.questions) {
+          initAskUserAnswers(idx, msg.pendingApprovalData.ask_user.questions);
+        }
+      });
+    },
+    { immediate: true, deep: true }
+  );
+
   const emit = defineEmits<{
     (e: 'file-preview', file: AttachedFile): void;
     (e: 'download-word', url: string): void;
-    (e: 'approve'): void;
-    (e: 'reject'): void;
+    (e: 'ask-user-submit', answers: string[]): void;
+    (e: 'ask-user-cancel'): void;
   }>();
 
+  // ask_user 答案状态(按消息索引存储)
+  const askUserAnswers = reactive<Record<number, string[]>>({});
+  const askUserOtherSelected = reactive<Record<number, boolean[]>>({});
+  const askUserCurrentStep = reactive<Record<number, number>>({});
+
+  // choice 可能是字符串或 { value: "..." } 对象,统一提取字符串
+  const getChoiceValue = (choice: any): string => {
+    if (typeof choice === 'string') return choice;
+    if (choice && typeof choice === 'object' && 'value' in choice) return choice.value;
+    return String(choice);
+  };
+
+  const initAskUserAnswers = (index: number, questions: { question: string; type: string; choices?: any[] }[]) => {
+    if (askUserAnswers[index]) return;
+    askUserAnswers[index] = questions.map(() => '');
+    askUserOtherSelected[index] = questions.map(() => false);
+    askUserCurrentStep[index] = 0;
+  };
+
+  const isCurrentStepAnswered = (msgIndex: number, qIndex: number, question: { required?: boolean; type: string }) => {
+    const answers = askUserAnswers[msgIndex];
+    if (!answers) return false;
+    if (question.required === false && !answers[qIndex]) return true;
+    return !!(answers[qIndex] && answers[qIndex].trim() !== '');
+  };
+
+  const goNextStep = (msgIndex: number, totalQuestions: number) => {
+    if (askUserCurrentStep[msgIndex] < totalQuestions - 1) {
+      askUserCurrentStep[msgIndex]++;
+    }
+  };
+
+  const goPrevStep = (msgIndex: number) => {
+    if (askUserCurrentStep[msgIndex] > 0) {
+      askUserCurrentStep[msgIndex]--;
+    }
+  };
+
+  const selectChoice = (msgIndex: number, qIndex: number, choice: any, totalQuestions: number) => {
+    if (!askUserAnswers[msgIndex]) return;
+    askUserAnswers[msgIndex][qIndex] = getChoiceValue(choice);
+    askUserOtherSelected[msgIndex][qIndex] = false;
+    // 单选自动跳到下一题(非最后一题时)
+    if (qIndex < totalQuestions - 1) {
+      goNextStep(msgIndex, totalQuestions);
+    }
+  };
+
+  const selectOther = (msgIndex: number, qIndex: number) => {
+    if (!askUserOtherSelected[msgIndex]) return;
+    askUserOtherSelected[msgIndex][qIndex] = true;
+    askUserAnswers[msgIndex][qIndex] = '';
+  };
+
+  const handleSubmitAskUser = (index: number) => {
+    const answers = askUserAnswers[index];
+    if (!answers) return;
+    emit('ask-user-submit', [...answers]);
+  };
+
+  const handleCancelAskUser = () => {
+    emit('ask-user-cancel');
+  };
+
   const messagesRef = ref<HTMLElement>();
   const collapsedThinking = reactive<Record<number, boolean>>({});
   const collapsedThinkingContent = reactive<Record<number, boolean>>({});
@@ -912,11 +1073,150 @@
               margin-bottom: 6px;
             }
 
-            .approval-desc {
-              font-size: 13px;
-              color: #8b949e;
-              line-height: 1.5;
-              margin-bottom: 12px;
+            .approval-plan {
+              margin-bottom: 14px;
+              padding: 10px 12px;
+              background: rgba(10, 132, 255, 0.08);
+              border: 1px solid rgba(10, 132, 255, 0.2);
+              border-radius: 6px;
+
+              .approval-plan-label {
+                font-size: 12px;
+                color: #0a84ff;
+                font-weight: 600;
+                margin-bottom: 6px;
+              }
+
+              .approval-plan-content {
+                font-size: 13px;
+                color: #c9d1d9;
+                line-height: 1.6;
+                white-space: pre-wrap;
+              }
+            }
+
+            .ask-user-question {
+              margin-bottom: 14px;
+
+              .ask-user-question-text {
+                font-size: 13px;
+                color: #c9d1d9;
+                margin-bottom: 8px;
+                line-height: 1.5;
+
+                .ask-user-required {
+                  color: #f85149;
+                  margin-left: 2px;
+                }
+              }
+
+              .ask-user-choices {
+                display: flex;
+                flex-direction: column;
+                gap: 6px;
+
+                .ask-user-choice {
+                  display: flex;
+                  align-items: center;
+                  padding: 8px 12px;
+                  background: rgba(255, 255, 255, 0.04);
+                  border: 1px solid rgba(63, 80, 106, 0.4);
+                  border-radius: 6px;
+                  cursor: pointer;
+                  font-size: 13px;
+                  color: #c9d1d9;
+                  transition: all 0.2s;
+
+                  &:hover {
+                    background: rgba(10, 132, 255, 0.1);
+                    border-color: rgba(10, 132, 255, 0.4);
+                  }
+
+                  &.selected {
+                    background: rgba(10, 132, 255, 0.15);
+                    border-color: #0a84ff;
+                    color: #fff;
+                  }
+                }
+
+                .ask-user-other-input {
+                  margin-top: 4px;
+                  padding: 8px 12px;
+                  font-size: 13px;
+                  color: #e0e6ed;
+                  background: rgba(255, 255, 255, 0.06);
+                  border: 1px solid rgba(63, 80, 106, 0.5);
+                  border-radius: 6px;
+                  outline: none;
+                  width: 100%;
+
+                  &:focus {
+                    border-color: #0a84ff;
+                    background: rgba(10, 132, 255, 0.08);
+                  }
+
+                  &::placeholder {
+                    color: #6e7681;
+                  }
+                }
+              }
+
+              .ask-user-text {
+                .ask-user-textarea {
+                  width: 100%;
+                  min-height: 60px;
+                  padding: 8px 12px;
+                  font-size: 13px;
+                  color: #e0e6ed;
+                  background: rgba(255, 255, 255, 0.06);
+                  border: 1px solid rgba(63, 80, 106, 0.5);
+                  border-radius: 6px;
+                  outline: none;
+                  resize: vertical;
+                  font-family: inherit;
+
+                  &:focus {
+                    border-color: #0a84ff;
+                    background: rgba(10, 132, 255, 0.08);
+                  }
+
+                  &::placeholder {
+                    color: #6e7681;
+                  }
+                }
+              }
+            }
+
+            .ask-user-stepper {
+              display: flex;
+              align-items: center;
+              gap: 6px;
+              margin-bottom: 14px;
+
+              .ask-user-step-dot {
+                width: 8px;
+                height: 8px;
+                border-radius: 50%;
+                background: rgba(63, 80, 106, 0.5);
+                cursor: pointer;
+                transition: all 0.2s;
+
+                &.active {
+                  background: #0a84ff;
+                  width: 20px;
+                  border-radius: 4px;
+                }
+
+                &.completed {
+                  background: rgba(10, 132, 255, 0.5);
+                }
+              }
+
+              .ask-user-step-text {
+                margin-left: 6px;
+                font-size: 12px;
+                color: #6e7681;
+              }
             }
 
             .approval-actions {
@@ -936,9 +1236,24 @@
                   background: #0a84ff;
                   color: #fff;
 
-                  &:hover {
+                  &:hover:not(:disabled) {
                     background: #3a9bfd;
                   }
+
+                  &:disabled {
+                    opacity: 0.4;
+                    cursor: not-allowed;
+                  }
+                }
+
+                &.nav-btn {
+                  background: rgba(255, 255, 255, 0.08);
+                  color: #c9d1d9;
+                  border: 1px solid rgba(63, 80, 106, 0.5);
+
+                  &:hover {
+                    background: rgba(255, 255, 255, 0.15);
+                  }
                 }
 
                 &.reject-btn {

+ 13 - 0
src/views/ventAI/manageAssistent/components/chatModal/types.ts

@@ -31,6 +31,13 @@ export interface ThinkingStep {
   timestamp: number;
 }
 
+export interface AskUserQuestion {
+  question: string;
+  type: 'text' | 'multiple_choice';
+  choices?: any[];
+  required?: boolean;
+}
+
 export interface PendingApprovalData {
   message: string;
   sessionId: string;
@@ -38,6 +45,12 @@ export interface PendingApprovalData {
   interrupts?: any[];
   elapsedMs?: number;
   prevThinkingContent?: string;
+  plan?: string;
+  ask_user?: {
+    questions: AskUserQuestion[];
+    tool_call_id?: string;
+  };
+  answers?: string[];
 }
 
 export interface Message {