Parcourir la source

feat: simplify agent workspace ui

Sheep il y a 2 jours
Parent
commit
76631e9709

+ 103 - 8
smqjh-agent-runtime/public/app.js

@@ -1,7 +1,8 @@
 const state = {
   history: [],
   lastTables: [],
-  liveTrace: []
+  liveTrace: [],
+  selectedToolName: ""
 };
 
 const els = {
@@ -36,7 +37,7 @@ function bindEvents() {
     sendMessage();
   });
   els.input.addEventListener("input", () => {
-    els.inputCount.textContent = `${els.input.value.trim().length} 字`;
+    updateInputCount();
   });
   els.input.addEventListener("keydown", (event) => {
     if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
@@ -44,7 +45,9 @@ function bindEvents() {
       sendMessage();
     }
   });
-  els.refreshBtn.addEventListener("click", refreshStatus);
+  if (els.refreshBtn) {
+    els.refreshBtn.addEventListener("click", refreshStatus);
+  }
   document.querySelector("[data-scroll-tools]").addEventListener("click", () => {
     document.querySelector("#toolsPanel").scrollIntoView({ behavior: "smooth" });
   });
@@ -59,8 +62,12 @@ async function refreshStatus() {
     const health = await getJson("/api/health");
     const tools = await getJson("/api/tools");
     const mcp = health.mcp || {};
-    els.mcpEndpoint.textContent = mcp.path ? `${mcp.name || "mcp"} ${mcp.path}` : "-";
-    els.toolCount.textContent = `${tools.tools?.length || 0} 个`;
+    if (els.mcpEndpoint) {
+      els.mcpEndpoint.textContent = mcp.path ? `${mcp.name || "mcp"} ${mcp.path}` : "-";
+    }
+    if (els.toolCount) {
+      els.toolCount.textContent = `${tools.tools?.length || 0} 个`;
+    }
     renderTools(tools.tools || []);
     if (health.ok) {
       setStatus("ok", "已连接", `${mcp.environmentName || "MCP"} 可用`);
@@ -87,7 +94,7 @@ async function sendMessage() {
 
   addUserMessage(message);
   els.input.value = "";
-  els.inputCount.textContent = "0 字";
+  updateInputCount();
   state.liveTrace = [];
   setBusy(true);
   const progressId = addProgressMessage("正在分析这个需求", "我会先判断是否需要查业务系统、数据库或外部工具。");
@@ -346,17 +353,105 @@ function safeFileName(value) {
 function renderTools(tools) {
   els.toolList.innerHTML = "";
   for (const tool of tools) {
-    const item = document.createElement("div");
+    const item = document.createElement("button");
+    item.type = "button";
     item.className = "tool-item";
+    item.dataset.toolName = tool.name;
+    item.title = "点击后将工具调用意图填入输入框";
+    if (state.selectedToolName === tool.name) {
+      item.classList.add("active");
+    }
     const title = document.createElement("strong");
     title.textContent = tool.title || tool.name;
     const desc = document.createElement("span");
     desc.textContent = `${tool.name}${tool.description ? ` - ${tool.description}` : ""}`;
-    item.append(title, desc);
+    const params = document.createElement("small");
+    params.textContent = summarizeToolParams(tool);
+    item.append(title, desc, params);
+    item.addEventListener("click", () => selectTool(tool));
     els.toolList.appendChild(item);
   }
 }
 
+function selectTool(tool) {
+  state.selectedToolName = tool.name;
+  els.toolList.querySelectorAll(".tool-item").forEach((node) => {
+    node.classList.toggle("active", node.dataset.toolName === tool.name);
+  });
+
+  els.input.value = buildToolPrompt(tool);
+  updateInputCount();
+  els.input.focus();
+  addAssistantMessage(buildToolSelectionMessage(tool));
+}
+
+function summarizeToolParams(tool) {
+  const schema = tool.inputSchema || {};
+  const properties = schema.properties || {};
+  const keys = Object.keys(properties);
+  if (!keys.length) {
+    return "无参数,点击后可直接发送";
+  }
+  const required = new Set(schema.required || []);
+  const requiredKeys = keys.filter((key) => required.has(key));
+  const optionalKeys = keys.filter((key) => !required.has(key));
+  const parts = [];
+  if (requiredKeys.length) {
+    parts.push(`必填:${requiredKeys.join("、")}`);
+  }
+  if (optionalKeys.length) {
+    parts.push(`可选:${optionalKeys.join("、")}`);
+  }
+  return parts.join(";");
+}
+
+function buildToolPrompt(tool) {
+  const schema = tool.inputSchema || {};
+  const properties = schema.properties || {};
+  const keys = Object.keys(properties);
+  const title = tool.title || tool.name;
+  if (!keys.length) {
+    return `使用「${title}」`;
+  }
+  const required = new Set(schema.required || []);
+  const params = keys.map((key) => {
+    const label = required.has(key) ? "必填" : "可选";
+    const description = properties[key]?.description || "";
+    return `${key}(${label}:${description})=`;
+  });
+  return `使用「${title}」,${params.join(";")}`;
+}
+
+function buildToolSelectionMessage(tool) {
+  const schema = tool.inputSchema || {};
+  const properties = schema.properties || {};
+  const keys = Object.keys(properties);
+  const title = tool.title || tool.name;
+  const lines = [
+    `已选择工具:${title}`,
+    tool.description || "",
+    `工具名:${tool.name}`
+  ].filter(Boolean);
+
+  if (keys.length) {
+    const required = new Set(schema.required || []);
+    const params = keys.map((key) => {
+      const label = required.has(key) ? "必填" : "可选";
+      const description = properties[key]?.description || properties[key]?.type || "";
+      return `- ${key}(${label}):${description}`;
+    });
+    lines.push(`参数:\n${params.join("\n")}`);
+    lines.push("我已把参数模板放到输入框,补齐后发送即可。");
+  } else {
+    lines.push("这个工具不需要参数,我已把可直接发送的指令放到输入框。");
+  }
+  return lines.join("\n\n");
+}
+
+function updateInputCount() {
+  els.inputCount.textContent = `${els.input.value.trim().length} 字`;
+}
+
 function renderTrace(trace, toolCalls) {
   els.traceList.innerHTML = "";
   const rows = trace.length ? trace : toolCalls.map((item) => ({

+ 0 - 58
smqjh-agent-runtime/public/index.html

@@ -13,7 +13,6 @@
         <img src="/assets/logo.png" alt="" />
         <div>
           <strong>市民请集合</strong>
-          <span>Python LangGraph Agent</span>
         </div>
       </div>
       <nav class="nav">
@@ -31,52 +30,10 @@
     </header>
 
     <main class="shell">
-      <section class="hero">
-        <div class="hero-copy">
-          <span class="eyebrow">安静的业务智能体</span>
-          <h1>市民请集合智能助手</h1>
-          <p>以 Python + LangGraph 编排后台业务 Agent。通过 MCP 调用 DeepSeek、只读数据库和 smqjh 业务工具,把管理员的问题整理成结论、依据与表格。</p>
-          <div class="hero-actions">
-            <button id="refreshBtn" class="chrome-button" type="button" title="刷新状态">刷新状态</button>
-            <span class="system-pill">MCP · 只读数据库 · DeepSeek</span>
-          </div>
-        </div>
-        <div class="hero-panel" aria-hidden="true">
-          <div class="seal-mark">
-            <img src="/assets/logo.png" alt="" />
-          </div>
-          <div class="brush-line"></div>
-          <div class="mini-display">
-            <span>运行中</span>
-            <strong>可用</strong>
-          </div>
-        </div>
-      </section>
-
-      <section class="feature-strip" aria-label="Agent capabilities">
-        <article class="feature-card">
-          <strong>智能查询</strong>
-          <p>商品、订单、会员、物流与经营数据自动判断工具。</p>
-        </article>
-        <article class="feature-card">
-          <strong>只读安全</strong>
-          <p>数据库查询走 MCP 白名单和 SELECT 校验。</p>
-        </article>
-        <article class="feature-card">
-          <strong>表格输出</strong>
-          <p>结构化 rows/columns 直接渲染,可下载 CSV。</p>
-        </article>
-        <article class="feature-card">
-          <strong>月结扩展</strong>
-          <p>企业月结、核对、人工确认流程继续接入。</p>
-        </article>
-      </section>
-
       <section class="workspace">
         <section class="chat-card retro-window">
           <div class="window-bar">
             <span></span>
-            <strong>业务对话室</strong>
           </div>
           <div id="messages" class="messages"></div>
           <form id="chatForm" class="composer">
@@ -96,21 +53,6 @@
         </section>
 
         <aside class="inspector">
-          <section class="panel player-panel">
-            <div class="panel-title">
-              <h2>执行上下文</h2>
-            </div>
-            <div class="player-screen">
-              <span>Runtime</span>
-              <strong>Python + LangGraph</strong>
-            </div>
-            <dl class="kv">
-              <div><dt>运行模式</dt><dd>Web 测试</dd></div>
-              <div><dt>框架</dt><dd>Python + LangGraph</dd></div>
-              <div><dt>MCP</dt><dd id="mcpEndpoint">-</dd></div>
-              <div><dt>工具</dt><dd id="toolCount">-</dd></div>
-            </dl>
-          </section>
           <section class="panel" id="toolsPanel">
             <div class="panel-title">
               <h2>可用工具</h2>

+ 29 - 314
smqjh-agent-runtime/public/styles.css

@@ -117,7 +117,6 @@ button:focus-visible {
   letter-spacing: 0;
 }
 
-.brand span,
 .session-card span {
   display: block;
   margin-top: 4px;
@@ -198,18 +197,6 @@ button:focus-visible {
   padding: 24px 0 32px;
 }
 
-.hero {
-  min-height: 176px;
-  display: grid;
-  grid-template-columns: minmax(0, 1fr) 280px;
-  gap: 16px;
-  align-items: stretch;
-  margin-bottom: 16px;
-}
-
-.hero-copy,
-.hero-panel,
-.feature-card,
 .chat-card,
 .panel {
   border: 1px solid var(--line);
@@ -217,197 +204,6 @@ button:focus-visible {
   box-shadow: var(--shadow);
 }
 
-.hero-copy {
-  position: relative;
-  overflow: hidden;
-  border-radius: 8px;
-  padding: 28px 32px;
-}
-
-.hero-copy::after {
-  content: "";
-  position: absolute;
-  right: 28px;
-  bottom: 24px;
-  width: 190px;
-  height: 1px;
-  background: linear-gradient(90deg, transparent, rgba(63, 77, 63, 0.42), transparent);
-}
-
-.eyebrow {
-  display: inline-flex;
-  align-items: center;
-  min-height: 28px;
-  padding: 3px 10px;
-  border: 1px solid rgba(139, 119, 98, 0.45);
-  border-radius: 3px;
-  background: rgba(244, 240, 231, 0.72);
-  color: var(--clay);
-  font-size: 13px;
-  font-weight: 600;
-}
-
-.hero h1 {
-  margin: 18px 0 10px;
-  max-width: 860px;
-  font-size: 36px;
-  line-height: 1.18;
-  font-weight: 700;
-  letter-spacing: 0;
-  color: var(--charcoal);
-}
-
-.hero p {
-  margin: 0;
-  max-width: 760px;
-  color: #44483f;
-  font-size: 15px;
-  line-height: 1.85;
-}
-
-.hero-actions {
-  display: flex;
-  flex-wrap: wrap;
-  align-items: center;
-  gap: 12px;
-  margin-top: 22px;
-}
-
-.chrome-button,
-.system-pill {
-  border-radius: 3px;
-  font-weight: 600;
-}
-
-.chrome-button {
-  min-height: 38px;
-  padding: 0 18px;
-  color: #f7f3eb;
-  border: 1px solid var(--moss-deep);
-  background: var(--moss-deep);
-  cursor: pointer;
-}
-
-.chrome-button:hover,
-.chrome-button:focus-visible {
-  background: #354236;
-}
-
-.system-pill {
-  display: inline-flex;
-  align-items: center;
-  min-height: 38px;
-  padding: 0 14px;
-  border: 1px solid var(--line);
-  color: var(--muted);
-  background: rgba(255, 252, 244, 0.52);
-  font-size: 13px;
-}
-
-.hero-panel {
-  position: relative;
-  overflow: hidden;
-  border-radius: 8px;
-  padding: 24px;
-  display: grid;
-  grid-template-rows: 1fr auto auto;
-  align-items: center;
-  justify-items: center;
-  background:
-    linear-gradient(150deg, rgba(255, 252, 244, 0.78), rgba(224, 219, 205, 0.58)),
-    var(--paper-deep);
-}
-
-.seal-mark {
-  width: 104px;
-  height: 104px;
-  border-radius: 7px;
-  display: grid;
-  place-items: center;
-  border: 1px solid rgba(83, 69, 55, 0.24);
-  background: rgba(255, 252, 244, 0.56);
-  transform: rotate(-1.5deg);
-}
-
-.seal-mark img {
-  width: 76px;
-  height: 76px;
-  border-radius: 50%;
-  object-fit: cover;
-  filter: saturate(0.68) contrast(0.94);
-}
-
-.brush-line {
-  width: 84%;
-  height: 2px;
-  margin-top: 14px;
-  background: linear-gradient(90deg, transparent, rgba(63, 77, 63, 0.46), rgba(139, 119, 98, 0.34), transparent);
-  opacity: 0.8;
-}
-
-.mini-display {
-  margin-top: 14px;
-  width: 100%;
-  padding: 12px 14px;
-  border-top: 1px solid rgba(82, 83, 72, 0.16);
-  color: var(--charcoal);
-}
-
-.mini-display span,
-.mini-display strong {
-  display: block;
-  text-align: center;
-}
-
-.mini-display span {
-  color: var(--muted);
-  font-size: 13px;
-}
-
-.mini-display strong {
-  margin-top: 4px;
-  font-size: 22px;
-  font-weight: 700;
-}
-
-.feature-strip {
-  display: grid;
-  grid-template-columns: repeat(4, minmax(0, 1fr));
-  gap: 12px;
-  margin-bottom: 16px;
-}
-
-.feature-card {
-  position: relative;
-  border-radius: 8px;
-  padding: 18px 18px 16px;
-  min-height: 104px;
-}
-
-.feature-card::before {
-  content: "";
-  position: absolute;
-  left: 18px;
-  top: 0;
-  width: 42px;
-  height: 2px;
-  background: rgba(108, 118, 102, 0.5);
-}
-
-.feature-card strong {
-  display: block;
-  margin-top: 4px;
-  font-size: 18px;
-  font-weight: 700;
-}
-
-.feature-card p {
-  margin: 8px 0 0;
-  color: var(--muted);
-  font-size: 13px;
-  line-height: 1.65;
-}
-
 .workspace {
   display: grid;
   grid-template-columns: minmax(620px, 1fr) 360px;
@@ -446,13 +242,6 @@ button:focus-visible {
   transform: rotate(4deg);
 }
 
-.window-bar strong {
-  color: var(--muted);
-  font-size: 13px;
-  font-weight: 600;
-  letter-spacing: 0.02em;
-}
-
 .messages {
   padding: 22px;
   overflow: auto;
@@ -735,55 +524,6 @@ tbody tr:nth-child(even) {
   font-weight: 700;
 }
 
-.player-screen {
-  margin-bottom: 14px;
-  border-radius: 7px;
-  padding: 14px;
-  background: rgba(47, 48, 44, 0.94);
-  color: #f4f0e7;
-  border: 1px solid rgba(47, 48, 44, 0.14);
-}
-
-.player-screen span,
-.player-screen strong {
-  display: block;
-}
-
-.player-screen span {
-  color: #c8c0b0;
-  font-size: 12px;
-  font-weight: 500;
-}
-
-.player-screen strong {
-  margin-top: 5px;
-  font-size: 18px;
-  font-weight: 700;
-}
-
-.kv {
-  display: grid;
-  gap: 12px;
-  margin: 0;
-}
-
-.kv div {
-  display: grid;
-  grid-template-columns: 86px minmax(0, 1fr);
-  gap: 8px;
-}
-
-.kv dt {
-  color: var(--muted);
-  font-weight: 500;
-}
-
-.kv dd {
-  margin: 0;
-  font-weight: 700;
-  word-break: break-all;
-}
-
 .tool-list,
 .trace-list {
   display: grid;
@@ -798,6 +538,24 @@ tbody tr:nth-child(even) {
   background: rgba(255, 252, 244, 0.52);
 }
 
+.tool-item {
+  width: 100%;
+  text-align: left;
+  color: inherit;
+  cursor: pointer;
+}
+
+.tool-item:hover,
+.tool-item:focus-visible {
+  background: rgba(249, 245, 235, 0.86);
+  border-color: rgba(63, 77, 63, 0.36);
+}
+
+.tool-item.active {
+  border-color: rgba(63, 77, 63, 0.54);
+  background: rgba(232, 226, 213, 0.58);
+}
+
 .tool-item strong,
 .trace-item strong {
   display: block;
@@ -815,6 +573,17 @@ tbody tr:nth-child(even) {
   line-height: 1.5;
 }
 
+.tool-item small {
+  display: inline-block;
+  margin-top: 8px;
+  padding: 3px 7px;
+  border-radius: 999px;
+  color: var(--moss-deep);
+  background: rgba(225, 232, 218, 0.64);
+  font-size: 11px;
+  line-height: 1.4;
+}
+
 .phase-error strong {
   color: var(--danger);
 }
@@ -833,19 +602,10 @@ tbody tr:nth-child(even) {
     overflow-x: auto;
   }
 
-  .hero,
   .workspace {
     grid-template-columns: 1fr;
   }
 
-  .hero-panel {
-    min-height: 180px;
-  }
-
-  .feature-strip {
-    grid-template-columns: repeat(2, minmax(0, 1fr));
-  }
-
   .inspector {
     max-height: none;
   }
@@ -862,45 +622,6 @@ tbody tr:nth-child(even) {
     padding-top: 12px;
   }
 
-  .hero-copy {
-    padding: 18px;
-  }
-
-  .hero h1 {
-    font-size: 30px;
-  }
-
-  .hero p {
-    font-size: 14px;
-    line-height: 1.7;
-  }
-
-  .hero-actions {
-    margin-top: 14px;
-  }
-
-  .hero-panel {
-    display: none;
-  }
-
-  .feature-strip {
-    grid-template-columns: repeat(2, minmax(0, 1fr));
-    gap: 10px;
-  }
-
-  .feature-card {
-    min-height: auto;
-    padding: 14px;
-  }
-
-  .feature-card strong {
-    font-size: 16px;
-  }
-
-  .feature-card p {
-    font-size: 12px;
-  }
-
   .chat-card {
     min-height: 620px;
   }
@@ -921,9 +642,3 @@ tbody tr:nth-child(even) {
     grid-row: auto;
   }
 }
-
-@media (max-width: 480px) {
-  .feature-strip {
-    grid-template-columns: 1fr;
-  }
-}