orca.ai/internal/web/html.go

522 lines
16 KiB
Go

package web
const indexHTML = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>orca.agent</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1b26;
color: #a9b1d6;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
height: 100vh;
overflow: hidden;
}
.header {
background: #1a1b26;
border-bottom: 1px solid #414868;
padding: 8px 16px;
display: flex;
align-items: center;
gap: 8px;
}
.header h1 {
font-size: 16px;
color: #7aa2f7;
font-weight: 600;
}
.header .version {
font-size: 12px;
color: #565f89;
}
.container {
display: flex;
height: calc(100vh - 37px);
}
.left-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.chat-box {
flex: 1;
overflow-y: auto;
padding: 16px;
border-right: 1px solid #414868;
}
.message {
margin-bottom: 20px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.message-user {
background: #24283b;
border: 1px solid #414868;
border-radius: 12px;
padding: 12px 16px;
}
.message-user .label {
color: #7dcfff;
font-weight: 600;
font-size: 13px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.message-user .label::before {
content: '';
width: 8px;
height: 8px;
background: #7dcfff;
border-radius: 50%;
}
.message-assistant {
background: #1f2335;
border: 1px solid #2f3349;
border-radius: 12px;
padding: 12px 16px;
}
.message-assistant .label {
color: #9ece6a;
font-weight: 600;
font-size: 13px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.message-assistant .label::before {
content: '';
width: 8px;
height: 8px;
background: #9ece6a;
border-radius: 50%;
}
.message-system {
background: #2a1f1f;
border: 1px solid #5a3a3a;
border-radius: 12px;
padding: 12px 16px;
}
.message-system .label {
color: #e0af68;
font-weight: 600;
font-size: 13px;
margin-bottom: 8px;
}
.message-content {
color: #c0caf5;
line-height: 1.7;
font-size: 14px;
}
.message-content p {
margin-bottom: 12px;
}
.message-content p:last-child {
margin-bottom: 0;
}
.message-content h1,
.message-content h2,
.message-content h3,
.message-content h4 {
color: #7aa2f7;
margin: 16px 0 12px 0;
font-weight: 600;
}
.message-content h1 { font-size: 20px; }
.message-content h2 { font-size: 18px; }
.message-content h3 { font-size: 16px; }
.message-content h4 { font-size: 14px; }
.message-content ul,
.message-content ol {
margin: 8px 0 8px 20px;
padding-left: 0;
}
.message-content li {
margin-bottom: 6px;
}
.message-content code {
background: #24283b;
color: #bb9af7;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 13px;
}
.message-content pre {
background: #16161e;
border: 1px solid #414868;
border-radius: 8px;
padding: 16px;
margin: 12px 0;
overflow-x: auto;
}
.message-content pre code {
background: transparent;
color: #c0caf5;
padding: 0;
border-radius: 0;
font-size: 13px;
line-height: 1.5;
}
.message-content blockquote {
border-left: 3px solid #7aa2f7;
background: #24283b;
padding: 12px 16px;
margin: 12px 0;
border-radius: 0 8px 8px 0;
color: #a9b1d6;
}
.message-content table {
width: 100%;
border-collapse: collapse;
margin: 12px 0;
font-size: 13px;
}
.message-content th,
.message-content td {
border: 1px solid #414868;
padding: 8px 12px;
text-align: left;
}
.message-content th {
background: #24283b;
color: #7aa2f7;
font-weight: 600;
}
.message-content tr:nth-child(even) {
background: #1f2335;
}
.message-content a {
color: #7aa2f7;
text-decoration: none;
}
.message-content a:hover {
text-decoration: underline;
}
.message-content hr {
border: none;
border-top: 1px solid #414868;
margin: 16px 0;
}
.input-box {
padding: 12px 16px;
border-right: 1px solid #414868;
border-top: 1px solid #414868;
display: flex;
gap: 8px;
}
.input-box input {
flex: 1;
background: #24283b;
border: 1px solid #414868;
border-radius: 6px;
padding: 8px 12px;
color: #c0caf5;
font-size: 14px;
outline: none;
}
.input-box input:focus {
border-color: #7aa2f7;
}
.input-box input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.input-box button {
background: #7aa2f7;
color: #1a1b26;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.input-box button:hover {
background: #bb9af7;
}
.input-box button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.right-panel {
width: 280px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
}
.panel-box {
background: #24283b;
border: 1px solid #414868;
border-radius: 8px;
padding: 12px;
}
.panel-box h3 {
color: #7aa2f7;
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #414868;
}
.stat-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
font-size: 13px;
}
.stat-label { color: #565f89; }
.stat-value { color: #7aa2f7; font-weight: 600; }
.agent-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
font-size: 13px;
border-bottom: 1px solid #2f3349;
}
.agent-row:last-child { border-bottom: none; }
.agent-name { color: #c0caf5; }
.agent-status {
font-size: 11px;
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
}
.status-idle { background: #2f3349; color: #565f89; }
.status-running { background: #2b3a2b; color: #9ece6a; }
.typing-indicator {
display: none;
padding: 8px 16px;
color: #e0af68;
font-size: 13px;
font-style: italic;
}
.typing-indicator.active { display: block; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #414868; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #565f89; }
</style>
</head>
<body>
<div class="header">
<h1>orca.agent</h1>
<span class="version">v0.1.0</span>
</div>
<div class="container">
<div class="left-panel">
<div class="chat-box" id="chatBox"></div>
<div class="typing-indicator" id="typingIndicator">Processing...</div>
<div class="input-box">
<input type="text" id="messageInput" placeholder="Type a message and press Enter..." autocomplete="off">
<button id="sendBtn" onclick="sendMessage()">Send</button>
</div>
</div>
<div class="right-panel">
<div class="panel-box">
<h3>Statistics</h3>
<div class="stat-row">
<span class="stat-label">Tools:</span>
<span class="stat-value" id="statTools">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Skills:</span>
<span class="stat-value" id="statSkills">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Agents:</span>
<span class="stat-value" id="statAgents">0</span>
</div>
</div>
<div class="panel-box">
<h3>Active Agents</h3>
<div id="agentsList"></div>
</div>
</div>
</div>
<script>
const chatBox = document.getElementById('chatBox');
const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
const typingIndicator = document.getElementById('typingIndicator');
const statTools = document.getElementById('statTools');
const statSkills = document.getElementById('statSkills');
const statAgents = document.getElementById('statAgents');
const agentsList = document.getElementById('agentsList');
let currentAssistantDiv = null;
let eventSource = null;
let currentContent = '';
marked.setOptions({
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(code, { language: lang }).value;
} catch (e) {}
}
return hljs.highlightAuto(code).value;
},
breaks: true,
gfm: true
});
function connectSSE() {
eventSource = new EventSource('/api/stream');
eventSource.addEventListener('connected', function(e) {
console.log('SSE connected:', e.data);
});
eventSource.onmessage = function(e) {
if (currentAssistantDiv) {
currentContent += e.data;
const content = currentAssistantDiv.querySelector('.message-content');
content.innerHTML = marked.parse(currentContent);
hljs.highlightAll();
chatBox.scrollTop = chatBox.scrollHeight;
}
};
eventSource.onerror = function(e) {
console.log('SSE error, reconnecting...');
setTimeout(connectSSE, 3000);
};
}
connectSSE();
function addMessage(role, content, agent) {
const div = document.createElement('div');
div.className = 'message message-' + role;
const label = document.createElement('div');
label.className = 'label';
if (role === 'user') {
label.textContent = 'You';
} else if (role === 'assistant' && agent) {
label.textContent = '[' + agent + ']';
} else if (role === 'assistant') {
label.textContent = 'Assistant';
} else {
label.textContent = 'System';
}
const text = document.createElement('div');
text.className = 'message-content';
if (role === 'user') {
text.textContent = content;
} else {
text.innerHTML = marked.parse(content);
}
div.appendChild(label);
div.appendChild(text);
chatBox.appendChild(div);
chatBox.scrollTop = chatBox.scrollHeight;
if (role !== 'user') {
hljs.highlightAll();
}
return div;
}
async function sendMessage() {
const message = messageInput.value.trim();
if (!message) return;
messageInput.value = '';
messageInput.disabled = true;
sendBtn.disabled = true;
typingIndicator.classList.add('active');
addMessage('user', message);
currentAssistantDiv = addMessage('assistant', '', '');
currentContent = '';
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: message })
});
const data = await response.json();
if (data.error) {
addMessage('system', 'Error: ' + data.error);
}
} catch (err) {
addMessage('system', 'Error: ' + err.message);
} finally {
messageInput.disabled = false;
sendBtn.disabled = false;
typingIndicator.classList.remove('active');
messageInput.focus();
currentAssistantDiv = null;
currentContent = '';
updateStats();
updateAgents();
}
}
async function updateStats() {
try {
const response = await fetch('/api/stats');
const data = await response.json();
statTools.textContent = data.tools;
statSkills.textContent = data.skills;
statAgents.textContent = data.agents;
} catch (e) {
console.log('Failed to update stats');
}
}
async function updateAgents() {
try {
const response = await fetch('/api/agents');
const data = await response.json();
agentsList.innerHTML = '';
data.forEach(agent => {
const div = document.createElement('div');
div.className = 'agent-row';
const statusClass = agent.status === 'running' ? 'status-running' : 'status-idle';
div.innerHTML = '<span class="agent-name">' + agent.id + '</span><span class="agent-status ' + statusClass + '">' + agent.status + '</span>';
agentsList.appendChild(div);
});
} catch (e) {
console.log('Failed to update agents');
}
}
messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
updateStats();
updateAgents();
setInterval(updateStats, 5000);
setInterval(updateAgents, 2000);
</script>
</body>
</html>`