feat: loading animation + tool descriptions in system prompt

1. Loading indicator: three-dot bouncing animation appears after
   sending a message and disappears when server starts responding.

2. Tool descriptions: resolve_skill_routing now appends available
   tools (name + description + parameters) to the system prompt so
   the LLM knows what tools it can call.
This commit is contained in:
chiguyong 2026-06-11 22:25:21 +08:00
parent 55421dd126
commit 44f19fcf14
2 changed files with 62 additions and 0 deletions

View File

@ -210,9 +210,31 @@ async def resolve_skill_routing(
result.model = default_model
result.agent_name = default_agent_name
# Append available tools to system prompt so LLM knows what it can call
if result.tools:
tools_desc = _build_tools_description(result.tools)
if result.system_prompt:
result.system_prompt += f"\n\n## Available Tools\n{tools_desc}"
else:
result.system_prompt = f"## Available Tools\n{tools_desc}"
return result
def _build_tools_description(tools: list) -> str:
"""Build a text description of tools for the system prompt."""
lines = []
for tool in tools:
desc = getattr(tool, 'description', '')
lines.append(f"- **{tool.name}**: {desc}")
schema = getattr(tool, 'input_schema', None)
if schema and "properties" in schema:
params = list(schema["properties"].keys())
if params:
lines[-1] += f" (parameters: {', '.join(params)})"
return "\n".join(lines)
# ---------------------------------------------------------------------------
# CostAwareRouter - 三层成本感知路由
# ---------------------------------------------------------------------------

View File

@ -164,6 +164,14 @@ html,body{height:100%;font-family:var(--font);background:var(--bg);color:var(--t
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
@keyframes slideInRight{from{opacity:0;transform:translateX(12px)}to{opacity:1;transform:translateX(0)}}
/* ── Loading indicator ───────────────────────────────────────── */
.loading-msg{display:flex;flex-direction:column;max-width:72%;align-self:flex-start;animation:msgIn .35s cubic-bezier(.16,1,.3,1)}
.loading-msg .loading-bubble{padding:8px 20px;background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius-lg) var(--radius-lg) var(--radius-lg) var(--radius-sm);display:inline-flex;align-items:center;gap:6px;box-shadow:var(--shadow-xs)}
.loading-msg .loading-dot{width:8px;height:8px;border-radius:50%;background:var(--text3);animation:loadingDot 1.4s infinite ease-in-out}
.loading-msg .loading-dot:nth-child(2){animation-delay:.2s}
.loading-msg .loading-dot:nth-child(3){animation-delay:.4s}
@keyframes loadingDot{0%,80%,100%{transform:scale(.4);opacity:.3}40%{transform:scale(1);opacity:1}}
/* ── Mobile ──────────────────────────────────────────────────── */
@media(max-width:768px){
.sidebar{position:fixed;left:-100%;z-index:10;transition:left .3s cubic-bezier(.16,1,.3,1);width:85vw;max-width:320px;box-shadow:var(--shadow-lg)}
@ -262,6 +270,7 @@ let activeSessionId = null;
let ws = null;
let isStreaming = false;
let currentAgentBubble = null;
let loadingEl = null;
let skills = [];
const API = '/api/v1/chat';
const SKILLS_API = '/api/v1/skills';
@ -380,7 +389,12 @@ function handleWsMessage(msg) {
case 'connected':
setConnStatus('已连接', true);
break;
case 'routing':
removeLoading();
appendStep(`路由: ${msg.skill || 'default'} (${msg.method}, ${Math.round((msg.confidence || 0) * 100)}%)`);
break;
case 'token':
removeLoading();
if (!currentAgentBubble) {
currentAgentBubble = appendMessage('agent', '');
isStreaming = true;
@ -390,6 +404,7 @@ function handleWsMessage(msg) {
scrollToBottom();
break;
case 'final_answer':
removeLoading();
if (currentAgentBubble) {
const current = currentAgentBubble.textContent || '';
const final = msg.content || '';
@ -405,16 +420,19 @@ function handleWsMessage(msg) {
scrollToBottom();
break;
case 'step':
removeLoading();
if (msg.data?.event_type === 'tool_call') {
appendStep(`使用工具: ${msg.data?.data?.tool_name || 'tool'}`);
}
break;
case 'skill_match':
removeLoading();
if (msg.data?.skill) {
appendStep(`技能: ${msg.data.skill} (${msg.data.method}, ${Math.round((msg.data.confidence || 0) * 100)}%)`);
}
break;
case 'error':
removeLoading();
appendMessage('agent', `[错误] ${msg.data?.message || '未知错误'}`);
currentAgentBubble = null;
isStreaming = false;
@ -423,6 +441,27 @@ function handleWsMessage(msg) {
}
}
// ── Loading indicator ──────────────────────────────────────────
function showLoading() {
if (loadingEl) return;
hideWelcome();
const container = document.getElementById('messages');
const div = document.createElement('div');
div.className = 'loading-msg';
div.id = 'loadingIndicator';
div.innerHTML = '<div class="loading-bubble"><div class="loading-dot"></div><div class="loading-dot"></div><div class="loading-dot"></div></div>';
container.appendChild(div);
loadingEl = div;
scrollToBottom();
}
function removeLoading() {
if (loadingEl) {
loadingEl.remove();
loadingEl = null;
}
}
// ── Send message ───────────────────────────────────────────────
async function sendMessage() {
const input = document.getElementById('input');
@ -464,6 +503,7 @@ async function sendMessage() {
currentAgentBubble = null;
isStreaming = true;
updateSendBtn();
showLoading();
}
function handleKey(e) {