Files
wehub-resource-sync e071084ebe
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:33 +08:00

356 lines
13 KiB
HTML

{{define "content"}}
<style>
#agent-chat {
display: flex;
flex-direction: column;
height: calc(100vh - 160px);
max-height: 800px;
}
#agent-messages {
flex: 1;
overflow-y: auto;
padding: 1em 0;
scroll-behavior: smooth;
}
.msg { padding: 0.7em 1em; border-radius: 8px; margin-bottom: 0.6em; line-height: 1.6; }
.msg-user { background: #f0f4ff; border: 1px solid #d0d8f0; }
.msg-user b { color: #336; }
.msg-assistant { background: #fff; border: 1px solid #ddd; }
.msg-assistant b { color: #333; }
.msg-error { background: #fff5f5; border: 1px solid #e88; color: #a00; }
.msg-thinking { background: #fafafa; border: 1px solid #e5e5e5; color: #888; font-style: italic; }
.tool-call {
background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 8px;
margin-bottom: 0.6em; font-size: 0.93em; overflow: hidden;
}
.tool-header {
padding: 0.5em 1em; cursor: pointer; display: flex; align-items: center;
gap: 0.5em; user-select: none; font-weight: 600; color: #555;
}
.tool-header:hover { background: #f0f0f0; }
.tool-header .arrow { transition: transform 0.2s; display: inline-block; }
.tool-header .arrow.open { transform: rotate(90deg); }
.tool-body { padding: 0 1em 0.8em 1em; display: none; }
.tool-body.open { display: block; }
.tool-body pre {
background: #f5f5f5; padding: 0.6em; border-radius: 4px;
overflow-x: auto; font-size: 0.9em; margin: 0.4em 0;
}
.tool-body .tool-label { font-weight: 600; color: #666; font-size: 0.85em; margin-top: 0.5em; }
.tool-status { font-size: 0.8em; font-weight: normal; margin-left: auto; }
.tool-status.ok { color: #2a2; }
.tool-status.err { color: #c00; }
#prompt-bar {
display: flex; gap: 0.5em; align-items: center;
padding: 0.8em 0 0 0; border-top: 1px solid #eee;
}
#prompt-bar input {
flex: 1; margin-bottom: 0; padding: 0.6em 0.8em;
border: 1px solid #ccc; border-radius: 6px; font-size: 1em;
}
#prompt-bar button { padding: 0.6em 1.5em; border-radius: 6px; font-size: 1em; white-space: nowrap; }
#prompt-bar button:disabled { opacity: 0.5; cursor: not-allowed; }
.tools-bar {
display: flex; gap: 0.5em; align-items: center;
padding: 0.5em 0; font-size: 0.85em; color: #888;
}
.tools-bar .tool-count { font-weight: 600; color: #555; }
.settings-toggle {
background: none; border: 1px solid #ddd; border-radius: 6px;
padding: 0.3em 0.8em; font-size: 0.85em; cursor: pointer; color: #555;
}
.settings-toggle:hover { background: #f5f5f5; }
#settings-panel {
display: none; background: #fafafa; border: 1px solid #e5e5e5;
border-radius: 8px; padding: 1em 1.2em; margin-bottom: 1em;
}
#settings-panel.open { display: block; }
#settings-panel label { display: block; font-weight: 600; margin-top: 0.6em; font-size: 0.9em; }
#settings-panel input, #settings-panel select {
width: 100%; padding: 0.4em 0.6em; font-size: 0.9em;
border: 1px solid #ddd; border-radius: 4px; margin-top: 0.2em;
}
#settings-panel .settings-row { display: flex; gap: 1em; }
#settings-panel .settings-row > div { flex: 1; }
.clear-btn {
background: none; border: none; color: #999; cursor: pointer;
font-size: 0.85em; padding: 0.3em 0.5em;
}
.clear-btn:hover { color: #c00; }
.empty-state {
display: flex; flex-direction: column; align-items: center;
justify-content: center; height: 100%; color: #aaa; text-align: center;
}
.empty-state .icon { font-size: 3em; margin-bottom: 0.3em; }
.empty-state p { margin: 0.3em 0; max-width: 400px; }
</style>
<div id="agent-chat">
<div class="tools-bar">
<span>Tools: <span class="tool-count" id="tool-count">...</span></span>
<button class="settings-toggle" onclick="toggleSettings()">Settings</button>
<button class="clear-btn" onclick="clearChat()">Clear chat</button>
</div>
<div id="settings-panel">
<div class="settings-row">
<div>
<label>Provider</label>
<select id="provider">
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="gemini">Google Gemini</option>
<option value="atlascloud">Atlas Cloud</option>
<option value="groq">Groq</option>
<option value="mistral">Mistral</option>
<option value="together">Together AI</option>
</select>
</div>
<div>
<label>Model (optional)</label>
<input type="text" id="model-name" placeholder="e.g. gpt-4o, claude-sonnet-4-20250514">
</div>
</div>
<label>API Key</label>
<input type="password" id="api-key" placeholder="sk-... or API key">
<label>Base URL (optional)</label>
<input type="text" id="base-url" placeholder="Leave blank for default">
<div style="margin-top:0.8em; display:flex; gap:0.5em; align-items:center;">
<button onclick="saveSettings()" style="padding:0.4em 1em; font-size:0.9em;">Save</button>
<span id="settings-status" style="color:#888; font-size:0.85em;"></span>
</div>
</div>
<div id="agent-messages">
<div class="empty-state" id="empty-state">
<div class="icon">&#129302;</div>
<p><b>Chat with your services</b></p>
<p>Ask the agent to interact with your microservices. It will discover and call the right tools automatically.</p>
<p id="empty-tools" style="font-size:0.85em; color:#bbb; margin-top:0.8em;"></p>
<div id="setup-hint" style="margin-top:1.2em; padding:1em; background:#f8f9fa; border-radius:8px; border:1px solid #e5e5e5; text-align:left; font-size:0.88em; max-width:400px; margin-left:auto; margin-right:auto;">
<p style="margin:0 0 0.5em; font-weight:600;">Getting started:</p>
<p style="margin:0 0 0.3em;">1. Click <b>Settings</b> above</p>
<p style="margin:0 0 0.3em;">2. Choose a provider and enter your API key</p>
<p style="margin:0 0 0.3em;">3. Type a prompt like <em>"list all services"</em></p>
<p style="margin:0.6em 0 0; color:#888; font-size:0.9em;">Or use the CLI: <code style="background:#e9ecef; padding:2px 5px; border-radius:3px;">micro chat --provider anthropic</code></p>
</div>
</div>
</div>
<div id="prompt-bar">
<input type="text" id="prompt-input" placeholder="Ask the agent to call your services..." autofocus>
<button id="prompt-btn" onclick="sendPrompt()">Send</button>
</div>
</div>
<script>
(function() {
var tools = [];
var toolCount = document.getElementById('tool-count');
loadSettings();
loadTools();
function loadSettings() {
fetch('/api/agent/settings')
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.provider) document.getElementById('provider').value = data.provider;
if (data.api_key) document.getElementById('api-key').value = data.api_key;
if (data.model) document.getElementById('model-name').value = data.model;
if (data.base_url) document.getElementById('base-url').value = data.base_url;
// Hide setup hint if API key is configured
if (data.api_key) {
var hint = document.getElementById('setup-hint');
if (hint) hint.style.display = 'none';
}
// Auto-show settings if no API key configured
if (!data.api_key) {
document.getElementById('settings-panel').classList.add('open');
}
})
.catch(function() {
document.getElementById('settings-panel').classList.add('open');
});
}
window.toggleSettings = function() {
document.getElementById('settings-panel').classList.toggle('open');
};
window.saveSettings = function() {
var status = document.getElementById('settings-status');
status.textContent = 'Saving...';
fetch('/api/agent/settings', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
provider: document.getElementById('provider').value,
api_key: document.getElementById('api-key').value,
model: document.getElementById('model-name').value,
base_url: document.getElementById('base-url').value
})
})
.then(function(r) { return r.json(); })
.then(function() {
status.textContent = 'Saved';
setTimeout(function() { status.textContent = ''; }, 2000);
})
.catch(function(err) { status.textContent = 'Error: ' + err; });
};
function loadTools() {
fetch('/mcp/tools')
.then(function(r) { return r.json(); })
.then(function(data) {
tools = data.tools || [];
toolCount.textContent = tools.length;
var emptyTools = document.getElementById('empty-tools');
if (tools.length > 0) {
var names = tools.slice(0, 5).map(function(t) { return t.name; });
var suffix = tools.length > 5 ? ' and ' + (tools.length - 5) + ' more' : '';
emptyTools.textContent = 'Available: ' + names.join(', ') + suffix;
} else {
emptyTools.textContent = 'No tools found. Start some services first.';
}
})
.catch(function() {
toolCount.textContent = '0';
});
}
window.clearChat = function() {
var container = document.getElementById('agent-messages');
container.innerHTML = '';
var emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.id = 'empty-state';
emptyState.innerHTML = '<div class="icon">&#129302;</div><p><b>Chat with your services</b></p><p>Ask the agent to interact with your microservices.</p>';
container.appendChild(emptyState);
};
window.sendPrompt = function() {
var input = document.getElementById('prompt-input');
var text = input.value.trim();
if (!text) return;
input.value = '';
// Remove empty state on first message
var emptyState = document.getElementById('empty-state');
if (emptyState) emptyState.remove();
addMessage('user', escapeHtml(text));
// Show thinking indicator
var thinkingId = 'thinking-' + Date.now();
addMessage('thinking', 'Agent is thinking...', thinkingId);
var btn = document.getElementById('prompt-btn');
btn.disabled = true;
btn.textContent = 'Sending...';
var startTime = Date.now();
fetch('/api/agent/prompt', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({prompt: text})
})
.then(function(r) { return r.json(); })
.then(function(data) {
var elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
btn.disabled = false;
btn.textContent = 'Send';
input.focus();
// Remove thinking indicator
var thinking = document.getElementById(thinkingId);
if (thinking) thinking.remove();
if (data.error) {
addMessage('error', escapeHtml(data.error));
return;
}
if (data.reply) {
addMessage('assistant', escapeHtml(data.reply));
}
if (data.tool_calls && data.tool_calls.length > 0) {
for (var i = 0; i < data.tool_calls.length; i++) {
var tc = data.tool_calls[i];
addToolCall(tc, elapsed);
}
}
if (data.answer) {
addMessage('assistant', escapeHtml(data.answer));
}
})
.catch(function(err) {
btn.disabled = false;
btn.textContent = 'Send';
input.focus();
var thinking = document.getElementById(thinkingId);
if (thinking) thinking.remove();
addMessage('error', 'Error: ' + escapeHtml(String(err)));
});
};
function addToolCall(tc, elapsed) {
var container = document.getElementById('agent-messages');
var div = document.createElement('div');
div.className = 'tool-call';
var hasError = tc.result && tc.result.error;
var statusClass = hasError ? 'err' : 'ok';
var statusText = hasError ? 'error' : elapsed + 's';
var inputJson = JSON.stringify(tc.input, null, 2);
var resultJson = JSON.stringify(tc.result, null, 2);
div.innerHTML =
'<div class="tool-header" onclick="this.querySelector(\'.arrow\').classList.toggle(\'open\'); this.nextElementSibling.classList.toggle(\'open\');">' +
'<span class="arrow">&#9654;</span> ' +
'<code>' + escapeHtml(tc.tool) + '</code>' +
'<span class="tool-status ' + statusClass + '">' + statusText + '</span>' +
'</div>' +
'<div class="tool-body">' +
'<div class="tool-label">Input</div>' +
'<pre>' + escapeHtml(inputJson) + '</pre>' +
'<div class="tool-label">Result</div>' +
'<pre>' + escapeHtml(resultJson) + '</pre>' +
'</div>';
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
document.getElementById('prompt-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); window.sendPrompt(); }
});
function addMessage(type, html, id) {
var container = document.getElementById('agent-messages');
var div = document.createElement('div');
div.className = 'msg msg-' + type;
if (id) div.id = id;
if (type === 'user') {
div.innerHTML = '<b>You</b><br>' + html;
} else if (type === 'assistant' || type === 'answer') {
div.innerHTML = '<b>Agent</b><br>' + html;
} else if (type === 'thinking') {
div.innerHTML = html;
} else if (type === 'error') {
div.innerHTML = html;
}
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function escapeHtml(str) {
var d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
})();
</script>
{{end}}