wangyf10 2 days ago
parent
commit
805ef8a8fa
2 changed files with 255 additions and 102 deletions
  1. 244 98
      yusys-ai-front/src/App.vue
  2. 11 4
      yusys-ai-front/src/useAgui.js

+ 244 - 98
yusys-ai-front/src/App.vue

@@ -1,8 +1,8 @@
 <script setup>
-import { ref, reactive, nextTick, watch } from 'vue'
+import { ref, reactive, nextTick, watch, onMounted, onBeforeUnmount, computed } from 'vue'
 import { useAgui } from './useAgui.js'
 
-const { messages, running, error, send, reset } = useAgui()
+const { messages, running, error, sessionId, send, newChat } = useAgui()
 
 const agents = reactive([
   { name: 'DemoAgentAGUI', url: '/agent-api/DemoAgentAGUI' },
@@ -14,6 +14,40 @@ const input = ref('')
 const listEl = ref(null)
 const textareaEl = ref(null)
 
+// 智能体下拉框(位于输入框左下角)
+const agentMenuOpen = ref(false)
+const agentSelectorEl = ref(null)
+const currentAgent = computed(
+  () => agents.find((a) => a.url === currentUrl.value) || agents[0]
+)
+
+function toggleAgentMenu() {
+  agentMenuOpen.value = !agentMenuOpen.value
+}
+
+function selectAgent(a) {
+  currentUrl.value = a.url
+  agentMenuOpen.value = false
+}
+
+function onDocClick(e) {
+  if (agentSelectorEl.value && !agentSelectorEl.value.contains(e.target)) {
+    agentMenuOpen.value = false
+  }
+}
+
+onMounted(() => document.addEventListener('click', onDocClick))
+onBeforeUnmount(() => document.removeEventListener('click', onDocClick))
+
+// 开启新对话:清空并生成新的 sessionId
+function onNewChat() {
+  if (running.value) return
+  newChat()
+  agentMenuOpen.value = false
+  input.value = ''
+  if (textareaEl.value) textareaEl.value.style.height = 'auto'
+}
+
 async function onSend() {
   const text = input.value
   if (!text.trim() || running.value) return
@@ -57,26 +91,16 @@ watch(
       </div>
 
       <div class="sidebar-body">
-        <!-- 智能体选择 -->
-        <div class="sidebar-section-title">智能体</div>
-        <div class="agent-list">
-          <div
-            v-for="a in agents"
-            :key="a.url"
-            class="agent-item"
-            :class="{ active: currentUrl === a.url }"
-            @click="currentUrl = a.url"
-          >
-            <span class="agent-dot"></span>
-            {{ a.name }}
-          </div>
-        </div>
+        <!-- 开启新对话 -->
+        <button class="new-chat-btn" :disabled="running" @click="onNewChat">
+          <span class="new-chat-icon">+</span> 开启新对话
+        </button>
       </div>
 
       <div class="sidebar-footer">
-        <button class="sidebar-btn" @click="reset">
-          <span>🗑</span> 清空对话
-        </button>
+        <div class="session-tag" :title="sessionId">
+          会话 ID:{{ sessionId.slice(0, 8) }}
+        </div>
       </div>
     </aside>
 
@@ -86,7 +110,7 @@ watch(
       <!-- 顶栏 -->
       <header class="topbar">
         <div class="topbar-title">
-          {{ agents.find(a => a.url === currentUrl)?.name || '智能体对话' }}
+          {{ currentAgent?.name || '智能体对话' }}
         </div>
         <div class="topbar-status" :class="{ active: running }">
           <span class="status-dot"></span>
@@ -113,7 +137,7 @@ watch(
           <div v-else-if="m.role === 'assistant'" class="msg-row assistant">
             <div class="avatar assistant-avatar">AI</div>
             <div class="assistant-content">
-              <div class="assistant-name">{{ agents.find(a => a.url === currentUrl)?.name || '智能体' }}</div>
+              <div class="assistant-name">{{ currentAgent?.name || '智能体' }}</div>
               <div class="assistant-text">
                 {{ m.content }}<span v-if="running && !m.content" class="cursor">▋</span>
               </div>
@@ -156,20 +180,56 @@ watch(
               @keydown="onKeydown"
               @input="autoResize"
             ></textarea>
-            <button
-              class="send-btn"
-              :class="{ ready: input.trim() && !running }"
-              :disabled="running || !input.trim()"
-              @click="onSend"
-            >
-              <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
-                <path d="M22 2L11 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-                <path d="M22 2L15 22L11 13L2 9L22 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-              </svg>
-            </button>
-          </div>
-          <div class="composer-hint">
-            当前:{{ agents.find(a => a.url === currentUrl)?.name }}
+
+            <!-- 工具栏:左下角智能体下拉框 + 右下角发送 -->
+            <div class="composer-toolbar">
+              <div ref="agentSelectorEl" class="agent-selector">
+                <button
+                  type="button"
+                  class="agent-selector-btn"
+                  :class="{ open: agentMenuOpen }"
+                  @click="toggleAgentMenu"
+                >
+                  <span class="agent-dot"></span>
+                  <span class="agent-selector-name">{{ currentAgent?.name }}</span>
+                  <svg class="chevron" width="12" height="12" viewBox="0 0 24 24" fill="none">
+                    <path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                  </svg>
+                </button>
+
+                <div v-if="agentMenuOpen" class="agent-menu">
+                  <div class="agent-menu-title">选择智能体</div>
+                  <button
+                    v-for="a in agents"
+                    :key="a.url"
+                    type="button"
+                    class="agent-menu-item"
+                    :class="{ active: currentUrl === a.url }"
+                    @click="selectAgent(a)"
+                  >
+                    <span class="agent-dot"></span>
+                    <span class="agent-menu-name">{{ a.name }}</span>
+                    <svg v-if="currentUrl === a.url" class="check" width="14" height="14" viewBox="0 0 24 24" fill="none">
+                      <path d="M20 6L9 17l-5-5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
+                    </svg>
+                  </button>
+                </div>
+              </div>
+
+              <div class="toolbar-spacer"></div>
+
+              <button
+                class="send-btn"
+                :class="{ ready: input.trim() && !running }"
+                :disabled="running || !input.trim()"
+                @click="onSend"
+              >
+                <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
+                  <path d="M22 2L11 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                  <path d="M22 2L15 22L11 13L2 9L22 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                </svg>
+              </button>
+            </div>
           </div>
         </div>
       </div>
@@ -224,81 +284,62 @@ watch(
 .sidebar-body {
   flex: 1;
   overflow-y: auto;
-  padding: 16px 0;
-}
-
-.sidebar-section-title {
-  font-size: 11px;
-  color: #9ca3af;
-  padding: 0 16px 8px;
-  letter-spacing: 0.8px;
-  text-transform: uppercase;
+  padding: 16px 12px;
 }
 
-.agent-list {
-  display: flex;
-  flex-direction: column;
-  gap: 2px;
-  padding: 0 8px;
-}
-
-.agent-item {
+/* 开启新对话按钮 */
+.new-chat-btn {
   display: flex;
   align-items: center;
-  gap: 10px;
-  padding: 9px 12px;
-  border-radius: 8px;
-  font-size: 13.5px;
+  justify-content: center;
+  gap: 8px;
+  width: 100%;
+  background: #4f8cff;
+  color: #fff;
+  border: none;
+  font-size: 14px;
+  font-weight: 600;
+  padding: 11px 14px;
+  border-radius: 10px;
   cursor: pointer;
-  transition: background 0.15s;
-  color: #4b5563;
+  transition: background 0.15s, opacity 0.15s;
 }
 
-.agent-item:hover {
-  background: #ededee;
-  color: #1a1d2e;
+.new-chat-btn:hover {
+  background: #3b6fd6;
 }
 
-.agent-item.active {
-  background: #e8effe;
-  color: #3b6fd6;
+.new-chat-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.new-chat-icon {
+  font-size: 18px;
+  line-height: 1;
+  font-weight: 400;
 }
 
 .agent-dot {
   width: 7px;
   height: 7px;
   border-radius: 50%;
-  background: #d1d5db;
-  flex-shrink: 0;
-}
-
-.agent-item.active .agent-dot {
   background: #4f8cff;
+  flex-shrink: 0;
 }
 
 .sidebar-footer {
-  padding: 12px 8px;
+  padding: 14px 16px;
   border-top: 1px solid #ebebec;
 }
 
-.sidebar-btn {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  width: 100%;
-  background: transparent;
-  border: none;
+.session-tag {
+  font-size: 11.5px;
   color: #9ca3af;
-  font-size: 13px;
-  padding: 9px 12px;
-  border-radius: 8px;
-  cursor: pointer;
-  transition: background 0.15s, color 0.15s;
-}
-
-.sidebar-btn:hover {
-  background: #ededee;
-  color: #374151;
+  font-family: ui-monospace, monospace;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
 /* ── 右侧主区 ── */
@@ -540,12 +581,12 @@ watch(
 
 .composer-inner {
   display: flex;
-  align-items: flex-end;
-  gap: 10px;
+  flex-direction: column;
+  gap: 8px;
   background: #f7f8fc;
   border: 1.5px solid #e5e7f0;
   border-radius: 16px;
-  padding: 10px 10px 10px 16px;
+  padding: 12px 14px 10px;
   transition: border-color 0.2s, box-shadow 0.2s;
 }
 
@@ -555,7 +596,8 @@ watch(
 }
 
 textarea {
-  flex: 1;
+  width: 100%;
+  box-sizing: border-box;
   background: transparent;
   border: none;
   outline: none;
@@ -573,6 +615,117 @@ textarea::placeholder {
   color: #b0b7c3;
 }
 
+/* 输入框内工具栏 */
+.composer-toolbar {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.toolbar-spacer {
+  flex: 1;
+}
+
+/* 智能体下拉框(左下角) */
+.agent-selector {
+  position: relative;
+}
+
+.agent-selector-btn {
+  display: flex;
+  align-items: center;
+  gap: 7px;
+  background: #fff;
+  border: 1px solid #e5e7f0;
+  border-radius: 16px;
+  padding: 6px 10px 6px 11px;
+  font-size: 13px;
+  color: #374151;
+  cursor: pointer;
+  max-width: 220px;
+  transition: border-color 0.15s, background 0.15s;
+}
+
+.agent-selector-btn:hover,
+.agent-selector-btn.open {
+  border-color: #4f8cff;
+  background: #f5f9ff;
+}
+
+.agent-selector-name {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.chevron {
+  color: #9ca3af;
+  flex-shrink: 0;
+  transition: transform 0.15s;
+}
+
+.agent-selector-btn.open .chevron {
+  transform: rotate(180deg);
+}
+
+/* 弹出菜单(向上弹出) */
+.agent-menu {
+  position: absolute;
+  bottom: calc(100% + 8px);
+  left: 0;
+  min-width: 240px;
+  background: #fff;
+  border: 1px solid #ebebec;
+  border-radius: 12px;
+  box-shadow: 0 8px 28px rgba(20, 24, 40, 0.14);
+  padding: 6px;
+  z-index: 20;
+}
+
+.agent-menu-title {
+  font-size: 11px;
+  color: #9ca3af;
+  padding: 6px 10px 8px;
+  letter-spacing: 0.5px;
+}
+
+.agent-menu-item {
+  display: flex;
+  align-items: center;
+  gap: 9px;
+  width: 100%;
+  background: transparent;
+  border: none;
+  padding: 9px 10px;
+  border-radius: 8px;
+  font-size: 13.5px;
+  color: #4b5563;
+  cursor: pointer;
+  text-align: left;
+  transition: background 0.12s;
+}
+
+.agent-menu-item:hover {
+  background: #f2f4f9;
+  color: #1a1d2e;
+}
+
+.agent-menu-item.active {
+  color: #3b6fd6;
+}
+
+.agent-menu-name {
+  flex: 1;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.check {
+  color: #4f8cff;
+  flex-shrink: 0;
+}
+
 .send-btn {
   width: 36px;
   height: 36px;
@@ -597,12 +750,5 @@ textarea::placeholder {
 .send-btn.ready:hover {
   background: #3b6fd6;
 }
-
-.composer-hint {
-  font-size: 11.5px;
-  color: #c0c5d0;
-  margin-top: 7px;
-  text-align: center;
-}
 </style>
 

+ 11 - 4
yusys-ai-front/src/useAgui.js

@@ -17,7 +17,8 @@ export function useAgui() {
 
   // 发给后端的会话历史(仅 user / assistant 文本)
   const history = []
-  let threadId = crypto.randomUUID()
+  // 当前会话 ID(sessionId / threadId);开启新对话时会重新生成
+  const sessionId = ref(crypto.randomUUID())
 
   const pick = (obj, ...keys) => {
     for (const k of keys) if (obj && obj[k] != null) return obj[k]
@@ -127,7 +128,7 @@ export function useAgui() {
     history.push(userMsg)
 
     const body = {
-      threadId,
+      threadId: sessionId.value,
       runId: crypto.randomUUID(),
       state: {},
       messages: history,
@@ -180,8 +181,14 @@ export function useAgui() {
     messages.splice(0, messages.length)
     history.splice(0, history.length)
     error.value = ''
-    threadId = crypto.randomUUID()
+    sessionId.value = crypto.randomUUID()
   }
 
-  return { messages, running, error, send, reset }
+  // 开启新对话:清空界面与历史,并生成一个新的 sessionId
+  function newChat() {
+    reset()
+    return sessionId.value
+  }
+
+  return { messages, running, error, sessionId, send, reset, newChat }
 }