wangyf10 1 өдөр өмнө
parent
commit
22c76e3bf7

+ 124 - 10
yusys-ai-front/src/App.vue

@@ -75,8 +75,19 @@ function autoResize(e) {
   el.style.height = Math.min(el.scrollHeight, 180) + 'px'
 }
 
+// 尝试把 JSON 字符串格式化为缩进形式,失败则原样返回
+function prettyJson(str) {
+  const s = (str || '').trim()
+  if (!s) return ''
+  try {
+    return JSON.stringify(JSON.parse(s), null, 2)
+  } catch {
+    return s
+  }
+}
+
 watch(
-  () => messages.map((m) => m.content).join('|'),
+  () => messages.map((m) => `${m.content}|${m.args || ''}|${m.result || ''}|${m.status || ''}`).join('~'),
   async () => {
     await nextTick()
     if (listEl.value) listEl.value.scrollTop = listEl.value.scrollHeight
@@ -155,8 +166,23 @@ watch(
           <!-- 工具调用 -->
           <div v-else-if="m.role === 'tool'" class="msg-row tool">
             <div class="tool-card">
-              <span class="tool-icon">⚙</span>
-              {{ m.content }}
+              <div class="tool-head">
+                <span class="tool-icon">🔧</span>
+                <span class="tool-title">工具调用</span>
+                <span class="tool-name">{{ m.name }}</span>
+                <span class="tool-badge" :class="m.status">
+                  <span v-if="m.status !== 'done'" class="tool-spinner"></span>
+                  {{ m.status === 'done' ? '完成' : '调用中' }}
+                </span>
+              </div>
+              <div v-if="prettyJson(m.args)" class="tool-section">
+                <div class="tool-section-label">参数</div>
+                <pre class="tool-code">{{ prettyJson(m.args) }}</pre>
+              </div>
+              <div v-if="prettyJson(m.result)" class="tool-section">
+                <div class="tool-section-label">结果</div>
+                <pre class="tool-code">{{ prettyJson(m.result) }}</pre>
+              </div>
             </div>
           </div>
         </template>
@@ -564,19 +590,107 @@ watch(
 }
 
 .tool-card {
-  display: inline-flex;
+  width: 100%;
+  max-width: 640px;
+  background: #fbfcff;
+  border: 1px solid #e6eaf5;
+  border-radius: 12px;
+  overflow: hidden;
+  box-shadow: 0 1px 2px rgba(20, 24, 40, 0.04);
+}
+
+.tool-head {
+  display: flex;
   align-items: center;
   gap: 8px;
-  background: #f8f8ff;
-  border: 1px solid #e8eaf6;
-  border-radius: 8px;
-  padding: 7px 14px;
-  font-size: 13px;
-  color: #5c6bc0;
+  padding: 10px 14px;
+  background: #f4f6fd;
+  border-bottom: 1px solid #e6eaf5;
 }
 
 .tool-icon {
+  font-size: 14px;
+  line-height: 1;
+}
+
+.tool-title {
+  font-size: 12px;
+  color: #8a93a6;
+  font-weight: 500;
+}
+
+.tool-name {
   font-size: 13px;
+  font-weight: 600;
+  color: #3b4256;
+  font-family: 'SFMono-Regular', ui-monospace, Menlo, Consolas, monospace;
+}
+
+.tool-badge {
+  margin-left: auto;
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  font-size: 11px;
+  font-weight: 500;
+  padding: 3px 9px;
+  border-radius: 999px;
+  background: #eef1f7;
+  color: #7a8296;
+}
+
+.tool-badge.running {
+  background: #fff5e6;
+  color: #d98a1f;
+}
+
+.tool-badge.done {
+  background: #e8f7ee;
+  color: #2f9e5f;
+}
+
+.tool-spinner {
+  width: 9px;
+  height: 9px;
+  border: 1.5px solid currentColor;
+  border-top-color: transparent;
+  border-radius: 50%;
+  animation: tool-spin 0.7s linear infinite;
+}
+
+@keyframes tool-spin {
+  to { transform: rotate(360deg); }
+}
+
+.tool-section {
+  padding: 10px 14px;
+}
+
+.tool-section + .tool-section {
+  border-top: 1px dashed #e6eaf5;
+}
+
+.tool-section-label {
+  font-size: 11px;
+  color: #9ca3af;
+  font-weight: 600;
+  letter-spacing: 0.4px;
+  margin-bottom: 6px;
+}
+
+.tool-code {
+  margin: 0;
+  background: #f7f8fc;
+  border: 1px solid #eceef4;
+  border-radius: 8px;
+  padding: 10px 12px;
+  font-family: 'SFMono-Regular', ui-monospace, Menlo, Consolas, monospace;
+  font-size: 12.5px;
+  line-height: 1.6;
+  color: #3b4256;
+  white-space: pre-wrap;
+  word-break: break-word;
+  overflow-x: auto;
 }
 
 /* 错误条 */

+ 21 - 3
yusys-ai-front/src/useAgui.js

@@ -29,6 +29,12 @@ export function useAgui() {
     let m = messages.find((x) => x.id === id)
     if (!m) {
       m = { id: id || crypto.randomUUID(), role, content: '' }
+      if (role === 'tool') {
+        m.name = ''
+        m.args = ''
+        m.result = ''
+        m.status = 'running' // running | done
+      }
       messages.push(m)
     }
     return m
@@ -58,21 +64,33 @@ export function useAgui() {
       case 'TOOL_CALL_START': {
         const id = pick(ev, 'toolCallId', 'tool_call_id') || crypto.randomUUID()
         const name = pick(ev, 'toolCallName', 'tool_call_name') || '未知工具'
-        findOrCreate(id, 'tool').content = `🔧 调用工具:${name}`
+        const m = findOrCreate(id, 'tool')
+        m.name = name
+        m.status = 'running'
         break
       }
       case 'TOOL_CALL_ARGS': {
         const id = pick(ev, 'toolCallId', 'tool_call_id')
         const delta = pick(ev, 'delta', 'args') ?? ''
         const m = messages.find((x) => x.id === id)
-        if (m) m.content += delta
+        if (m) m.args += delta
+        break
+      }
+      case 'TOOL_CALL_END': {
+        const id = pick(ev, 'toolCallId', 'tool_call_id')
+        const m = messages.find((x) => x.id === id)
+        // 参数已接收完毕;若无后续结果事件,也视为完成
+        if (m && !m.result) m.status = 'done'
         break
       }
       case 'TOOL_CALL_RESULT': {
         const id = pick(ev, 'toolCallId', 'tool_call_id')
         const content = pick(ev, 'content', 'result') ?? ''
         const m = messages.find((x) => x.id === id)
-        if (m) m.content += `\n↳ 结果:${content}`
+        if (m) {
+          m.result += typeof content === 'string' ? content : JSON.stringify(content)
+          m.status = 'done'
+        }
         break
       }
       case 'RUN_ERROR': {