mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 09:58:06 +00:00
fix: LLM 工具循环 400、中断消息注入、ConPTY 终端支持
- agent: 工具轮请求尾部补 user 占位(zen 网关强制),tool 消息正确配对 - agent: 工具提醒/中断以 system 角色注入并带 [中断消息] 前缀,不进用户履历;系统提示词说明中断消息格式 - agentcli: 基于 ConPTY 的交互式终端(ptywin fork),terminal_create/read/write/resize/close/watch - webui: server 输出通道适配器(保留 reasoning_content/disable_thinking) - GUI: 沉浸式标题栏、icon 圆角重制、mascot 等打磨
This commit is contained in:
@ -6,6 +6,7 @@ let state = {
|
||||
meta: {},
|
||||
pluginMeta: {},
|
||||
settingsPlugins: ['core'],
|
||||
currentView: 'chat',
|
||||
selectedSection: 'core',
|
||||
messages: [],
|
||||
chatLoading: false,
|
||||
@ -17,6 +18,9 @@ let state = {
|
||||
chatHistory: [],
|
||||
terminals: [],
|
||||
cmdHistory: [],
|
||||
termScreens: {},
|
||||
chatStick: true,
|
||||
pendingTools: [],
|
||||
eventSource: null,
|
||||
lang: localStorage.getItem('ha-lang') || 'zh',
|
||||
connections: [], currentConn: null,
|
||||
@ -111,10 +115,16 @@ function applyI18n() {
|
||||
}
|
||||
|
||||
// ===== Theme =====
|
||||
var ICON_SUN_GUI =
|
||||
'<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4m11.4-11.4 1.4-1.4"/></svg>';
|
||||
var ICON_MOON_GUI =
|
||||
'<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>';
|
||||
|
||||
function setTheme(name) {
|
||||
document.documentElement.setAttribute('data-theme', name);
|
||||
localStorage.setItem('ha-theme', name);
|
||||
document.getElementById('theme-btn').textContent = name === 'light' ? '☀️' : '🌙';
|
||||
var btn = document.getElementById('theme-btn');
|
||||
if (btn) btn.innerHTML = name === 'light' ? ICON_SUN_GUI : ICON_MOON_GUI;
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
@ -149,8 +159,36 @@ function toast(m, isError) {
|
||||
}
|
||||
|
||||
// ===== API =====
|
||||
async function cliRequest(line) {
|
||||
var conn = state.currentConn;
|
||||
if (!conn) throw new Error(__('未选择连接','No connection selected'));
|
||||
if (!window.homeagent || !window.homeagent.cli) throw new Error('cli bridge unavailable');
|
||||
var resp = await window.homeagent.cli.request(conn.socketPath || conn.url, conn.apiKey, line);
|
||||
if (resp && resp.error) throw new Error(resp.error);
|
||||
return resp;
|
||||
}
|
||||
|
||||
// CLI 传输映射:将 REST 路径转换为 cli 内置命令或直接对话
|
||||
function cliMap(path, o) {
|
||||
o = o || {};
|
||||
var m = o.method || 'GET';
|
||||
if (m === 'POST' && path.indexOf('/chat') !== -1) {
|
||||
var body = {};
|
||||
try { body = JSON.parse(o.body || '{}'); } catch (e) {}
|
||||
return cliRequest(body.message || '');
|
||||
}
|
||||
if (path === '/status') return cliRequest('/status');
|
||||
if (path === '/kernel') return cliRequest('/kernel');
|
||||
if (path === '/settings') return cliRequest('/settings');
|
||||
if (path === '/chat/history') return Promise.resolve({ messages: [] });
|
||||
return Promise.reject(new Error(__('CLI 连接不支持此功能','Not supported on CLI connection')));
|
||||
}
|
||||
|
||||
async function api(p, o) {
|
||||
if (!state.currentConn) throw new Error(__('未选择连接','No connection selected'));
|
||||
if (state.currentConn.type === 'cli') {
|
||||
return cliMap(p, o);
|
||||
}
|
||||
var opts = o || {};
|
||||
var headers = { 'Content-Type': 'application/json', ...(opts.headers || {}) };
|
||||
if (state.currentConn.apiKey) headers['X-API-Key'] = state.currentConn.apiKey;
|
||||
@ -163,13 +201,19 @@ async function api(p, o) {
|
||||
}
|
||||
|
||||
// ===== Navigation =====
|
||||
function switchTab(n) {
|
||||
document.querySelectorAll('.tab-content').forEach(function(e) { e.classList.remove('active') });
|
||||
var el = document.getElementById('tab-' + n);
|
||||
function switchView(n) {
|
||||
document.querySelectorAll('.view').forEach(function(e) { e.classList.remove('active') });
|
||||
var el = document.getElementById('view-' + n);
|
||||
if (el) el.classList.add('active');
|
||||
document.querySelectorAll('nav a').forEach(function(e) { e.classList.remove('active') });
|
||||
var match = document.querySelector('nav a[onclick*="' + n + '"]');
|
||||
if (match) match.classList.add('active');
|
||||
document.querySelectorAll('.rail-btn').forEach(function(e) { e.classList.remove('active') });
|
||||
var rb = document.getElementById('rail-' + n);
|
||||
if (rb) rb.classList.add('active');
|
||||
state.currentView = n;
|
||||
if (n === 'chat') {
|
||||
state.chatStick = true;
|
||||
var msgsEl = document.getElementById('chat-msgs');
|
||||
if (msgsEl) { try { msgsEl.scrollTo({ top: msgsEl.scrollHeight, behavior: 'smooth' }) } catch(e) { msgsEl.scrollTop = msgsEl.scrollHeight } }
|
||||
}
|
||||
renderAll();
|
||||
}
|
||||
|
||||
@ -270,17 +314,41 @@ function renderOverview() {
|
||||
+ statCard(__('内存','Memory'), k?.runtime?.memory_mb ? k.runtime.memory_mb + ' MB' : '-', '')
|
||||
+ statCard('Go ' + __('版本','Version'), k?.runtime?.go_version || '-', '')
|
||||
+ '</div></div>';
|
||||
document.getElementById('tab-overview').innerHTML = html;
|
||||
document.getElementById('view-overview').innerHTML = html;
|
||||
}
|
||||
|
||||
// ===== Chat =====
|
||||
var _chatLayoutBuilt = false;
|
||||
|
||||
function buildChatLayout() {
|
||||
var cont = document.getElementById('tab-chat');
|
||||
var cont = document.getElementById('view-chat');
|
||||
var k = state.kernel || {};
|
||||
var html = '<div class="chat-layout"><div class="chat-main">';
|
||||
html += '<div class="card"><h2>' + __('对话','Chat') + ' <span id="chat-stage" class="badge" style="font-size:10px;font-weight:400;display:' + (state.chatLoading ? 'inline' : 'none') + '">' + escHtml(state.chatStage || '') + '</span></h2><div class="chat-messages" id="chat-msgs">';
|
||||
var html = '<div class="chat-layout">';
|
||||
html += '<div class="chat-tabs">'
|
||||
+ '<span class="active" onclick="switchChatPanel(\'chat\',this)">' + __('对话','Chat') + '</span>'
|
||||
+ '<span onclick="switchChatPanel(\'starmap\',this)">' + __('星图','Star Map') + '</span>'
|
||||
+ '<span onclick="switchChatPanel(\'terminal\',this)">' + __('终端','Terminal') + '</span>'
|
||||
+ '<span onclick="switchChatPanel(\'cmd\',this)">' + __('运行中命令','Running Commands') + '</span>'
|
||||
+ '<span onclick="switchChatPanel(\'memory\',this)">' + __('记忆','Memory') + '</span>'
|
||||
+ '<span onclick="switchChatPanel(\'context\',this)">' + __('上下文','Context') + '</span>'
|
||||
+ '<span onclick="switchChatPanel(\'knowledge\',this)">' + __('知识','Knowledge') + '</span>'
|
||||
+ '</div>';
|
||||
if (!state.currentConn) {
|
||||
html += '<div class="chat-panel active" id="chat-panel-chat"><div class="card setup-card">'
|
||||
+ '<svg class="setup-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>'
|
||||
+ '<h2>' + __('未配置后端','No backend configured') + '</h2>'
|
||||
+ '<p>' + __('连接 HomeAgent 服务端后即可开始对话。请在设置中添加后端连接。','Connect to a HomeAgent server to start chatting. Add a backend connection in Settings.') + '</p>'
|
||||
+ '<button class="btn btn-primary" onclick="goSettingsConn()">' + __('前往设置添加后端','Go to Settings to add backend') + '</button>'
|
||||
+ '</div></div>';
|
||||
for (var p2 = 0; p2 < 6; p2++) {
|
||||
html += '<div class="chat-panel" id="chat-panel-' + ['starmap','terminal','cmd','memory','context','knowledge'][p2] + '"><div class="card"><p style="color:var(--text-muted)">' + __('请先在设置中添加后端连接','Add a backend connection in Settings first') + '</p></div></div>';
|
||||
}
|
||||
cont.innerHTML = html;
|
||||
_chatLayoutBuilt = true;
|
||||
return;
|
||||
}
|
||||
html += '<div class="chat-panel active" id="chat-panel-chat"><div class="chat-main">';
|
||||
html += '<div class="card"><h2>' + __('对话','Chat') + ' <span id="chat-stage" class="badge" style="font-size:10px;font-weight:400;display:none">' + escHtml(state.chatStage || '') + '</span></h2><div class="chat-messages" id="chat-msgs">';
|
||||
if (state.messages.length === 0) {
|
||||
html += '<div class="empty-state" style="flex:1;display:flex;align-items:center;justify-content:center"><p>' + __('开始对话以测试 Agent 回复','Start a conversation to test Agent replies') + '</p></div>';
|
||||
}
|
||||
@ -288,35 +356,28 @@ function buildChatLayout() {
|
||||
+ '<div class="chat-input-row">'
|
||||
+ '<input id="chat-input" placeholder="' + __('输入消息...','Type a message...') + '" onkeydown="if(event.key==\'Enter\')sendChat()">'
|
||||
+ '<button class="btn btn-primary" onclick="sendChat()" id="chat-send-btn">' + __('发送','Send') + '</button>'
|
||||
+ '</div></div>';
|
||||
html += '</div><div class="chat-sidebar">'
|
||||
+ '<div class="card" style="padding:12px"><h2 style="font-size:13px;margin-bottom:8px">' + __('星图','Star Map') + '</h2>'
|
||||
+ '<div id="sm-container-chat" style="height:160px;display:flex;align-items:center;justify-content:center"><div class="loading-spinner"></div></div></div>'
|
||||
+ '<div class="card" style="padding:12px"><h2 style="font-size:13px;margin-bottom:8px">' + __('终端','Terminal') + ' <span id="term-count-badge" class="badge badge-blue">0</span></h2>'
|
||||
+ '<div id="term-list" style="max-height:160px;overflow-y:auto;font-size:11px"></div></div>'
|
||||
+ '<div class="card" style="padding:12px"><h2 style="font-size:13px;margin-bottom:8px">' + __('命令历史','Command History') + ' <span id="cmd-count-badge" class="badge badge-blue">0</span></h2>'
|
||||
+ '<div id="cmd-list" style="max-height:120px;overflow-y:auto;font-size:11px"></div></div>'
|
||||
+ '<div class="card" style="padding:12px">'
|
||||
+ '<div class="sidebar-subnav">'
|
||||
+ '<span class="active" onclick="switchChatSub(\'memory\',this)">' + __('记忆','Memory') + '</span>'
|
||||
+ '<span onclick="switchChatSub(\'context\',this)">' + __('上下文','Context') + '</span>'
|
||||
+ '<span onclick="switchChatSub(\'knowledge\',this)">' + __('知识','Knowledge') + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div id="chat-sub-memory">'
|
||||
+ '</div></div></div></div>';
|
||||
html += '<div class="chat-panel" id="chat-panel-starmap"><div class="card"><h2>' + __('星图','Star Map') + '</h2>'
|
||||
+ '<div id="sm-container-chat" style="display:flex;align-items:center;justify-content:center;min-height:480px"><div class="loading-spinner"></div></div></div></div>';
|
||||
html += '<div class="chat-panel" id="chat-panel-terminal"><div class="card"><h2>' + __('终端','Terminal') + ' <span id="term-count-badge" class="badge badge-blue">0</span></h2>'
|
||||
+ '<div id="term-list" style="max-height:60vh;overflow-y:auto;font-size:12px"></div></div></div>';
|
||||
html += '<div class="chat-panel" id="chat-panel-cmd"><div class="card"><h2>' + __('运行中命令','Running Commands') + ' <span id="cmd-count-badge" class="badge badge-blue">0</span></h2>'
|
||||
+ '<div id="cmd-list" style="max-height:60vh;overflow-y:auto;font-size:12px"></div></div></div>';
|
||||
html += '<div class="chat-panel" id="chat-panel-memory"><div class="card"><h2>' + __('记忆','Memory') + '</h2>'
|
||||
+ '<div class="kv-row"><span class="key">' + __('实体','Entities') + '</span><span class="val">' + (k?.memory?.entity_count || '-') + '</span></div>'
|
||||
+ '<div class="kv-row"><span class="key">' + __('关系','Relations') + '</span><span class="val">' + (k?.memory?.relation_count || '-') + '</span></div>'
|
||||
+ '<div style="margin-top:8px">'
|
||||
+ '<input id="mem-query" placeholder="' + __('关键词查询','Keyword query') + '">'
|
||||
+ '<button class="btn btn-primary btn-sm" onclick="queryMemoryChat()">' + __('查询','Query') + '</button>'
|
||||
+ '</div><div id="mem-result-chat" style="margin-top:8px;max-height:180px;overflow:auto"></div>'
|
||||
+ '</div>'
|
||||
+ '<div id="chat-sub-context" style="display:none">'
|
||||
+ '</div></div>';
|
||||
html += '<div class="chat-panel" id="chat-panel-context"><div class="card"><h2>' + __('上下文','Context') + '</h2>'
|
||||
+ '<div style="margin-top:8px">'
|
||||
+ '<input id="ctx-query" placeholder="' + __('输入当前话题','Enter current topic') + '">'
|
||||
+ '<button class="btn btn-primary btn-sm" onclick="queryMemoryContext()">' + __('获取上下文','Get Context') + '</button>'
|
||||
+ '</div><div id="ctx-result" style="margin-top:8px;max-height:200px;overflow:auto"></div>'
|
||||
+ '</div>'
|
||||
+ '<div id="chat-sub-knowledge" style="display:none">'
|
||||
+ '</div></div>';
|
||||
html += '<div class="chat-panel" id="chat-panel-knowledge"><div class="card"><h2>' + __('知识','Knowledge') + '</h2>'
|
||||
+ '<div class="kv-row"><span class="key">' + __('项目','Items') + '</span><span class="val">' + (k?.knowledge?.item_count || '-') + '</span></div>'
|
||||
+ '<div style="margin-top:8px">'
|
||||
+ '<input id="know-query" placeholder="' + __('搜索知识','Search knowledge') + '">'
|
||||
@ -326,16 +387,67 @@ function buildChatLayout() {
|
||||
+ '<input id="know-name" placeholder="' + __('知识名称','Knowledge name') + '" style="margin-bottom:4px">'
|
||||
+ '<textarea id="know-content" placeholder="' + __('内容','Content') + '" style="min-height:50px;margin-bottom:4px"></textarea>'
|
||||
+ '<button class="btn btn-primary btn-sm" onclick="createKnowledgeChat()">' + __('创建','Create') + '</button>'
|
||||
+ '</div></div></div></div></div>';
|
||||
+ '</div></div></div>';
|
||||
cont.innerHTML = html;
|
||||
_chatLayoutBuilt = true;
|
||||
}
|
||||
|
||||
var CHAN_COLORS = ['#e08a5f', '#5f9fe0', '#6bbf8f', '#c06bbf', '#d9a13b', '#5fb3bf', '#b06b6b', '#7f8ce0'];
|
||||
|
||||
function chanColor(src) {
|
||||
var h = 0;
|
||||
for (var i = 0; i < src.length; i++) h = (h * 31 + src.charCodeAt(i)) >>> 0;
|
||||
return CHAN_COLORS[h % CHAN_COLORS.length];
|
||||
}
|
||||
|
||||
function chanLetter(src) {
|
||||
var s = (src || '').trim();
|
||||
if (!s) return 'C';
|
||||
var ch = s.charAt(0).toUpperCase();
|
||||
return /[A-Za-z0-9]/.test(ch) ? ch : 'C';
|
||||
}
|
||||
|
||||
function renderChat() {
|
||||
if (!_chatLayoutBuilt) { buildChatLayout(); renderChatStarmap(); renderTerminals(); renderCmdHistory() }
|
||||
var msgsEl = document.getElementById('chat-msgs');
|
||||
if (!msgsEl) return;
|
||||
if (!msgsEl._stickBound) {
|
||||
msgsEl._stickBound = true;
|
||||
msgsEl.addEventListener('scroll', function() {
|
||||
state.chatStick = msgsEl.scrollHeight - msgsEl.scrollTop - msgsEl.clientHeight < 80;
|
||||
}, { passive: true });
|
||||
}
|
||||
var msgs = state.messages;
|
||||
var sig = msgs.map(function(m) {
|
||||
var c = m.content || '';
|
||||
return (m.role || '') + ':' + c.length + ':' + c.slice(-40) + ':' + (m.tool_calls || []).map(function(t) { return (t.tool || t.name || '') + '/' + (t.status || '') }).join(',');
|
||||
}).join('|') + '|L' + (state.chatLoading ? '1' : '0') + '|P' + (state.pendingTools || []).join(',');
|
||||
if (msgsEl._chatSig === sig && msgsEl.childElementCount > 0) { return; }
|
||||
msgsEl._chatSig = sig;
|
||||
var prevPending = msgsEl._lastPending || [];
|
||||
var newPending = (state.pendingTools || []).slice();
|
||||
var lastM = msgs.length ? msgs[msgs.length - 1] : null;
|
||||
if (state.chatLoading && lastM && lastM.role === 'assistant') {
|
||||
(lastM.tool_calls || []).forEach(function(tc) {
|
||||
if (!tc.result && tc.status !== 'denied') {
|
||||
var nm = tc.tool || tc.name || '';
|
||||
if (newPending.indexOf(nm) === -1) newPending.push(nm);
|
||||
}
|
||||
});
|
||||
}
|
||||
var newlyDone = prevPending.filter(function(n) { return newPending.indexOf(n) === -1; });
|
||||
msgsEl._lastPending = newPending;
|
||||
var streamingLast = !!(state.chatLoading && lastM && lastM.role === 'assistant' && !lastM._final);
|
||||
function pillHtml() {
|
||||
var s = '';
|
||||
newPending.forEach(function(nm) {
|
||||
var anim = prevPending.indexOf(nm) !== -1 ? '' : ' pill-in';
|
||||
s += '<span class="thinking-tool' + anim + '" data-tool="' + escHtml(nm) + '">'
|
||||
+ '<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M14.7 6.3a4 4 0 0 0-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 0 0 5.4-5.4l-2.9 2.9-2.5-.6-.6-2.5z"/></svg>'
|
||||
+ escHtml(nm) + '</span>';
|
||||
});
|
||||
return s;
|
||||
}
|
||||
var html = '';
|
||||
if (msgs.length === 0) {
|
||||
html = '<div class="empty-state" style="flex:1;display:flex;align-items:center;justify-content:center"><p>' + __('开始对话以测试 Agent 回复','Start a conversation to test Agent replies') + '</p></div>';
|
||||
@ -350,44 +462,77 @@ function renderChat() {
|
||||
} else {
|
||||
c = escHtml(c);
|
||||
}
|
||||
var isChan = !!(m.source && m.source !== 'webui');
|
||||
var rc = '';
|
||||
if (m.reasoning_content) {
|
||||
var rcBody = (typeof marked !== 'undefined' ? marked.parse(m.reasoning_content) : escHtml(m.reasoning_content));
|
||||
rc = '<div class="reasoning">'
|
||||
+ '<div class="reasoning-title" onclick="var n=this.nextElementSibling;n.style.display=n.style.display===\'none\'?\'block\':\'none\';this.textContent=this.textContent===\'' + __('收起思考','Collapse') + '\'?\'' + __('展开思考','Expand') + '\':\'' + __('收起思考','Collapse') + '\'">' + __('收起思考','Collapse') + '</div>'
|
||||
+ '<div class="reasoning-body" style="display:none">' + rcBody + '</div></div>';
|
||||
rc = '<div class="msg-bubble"><div class="reasoning">'
|
||||
+ '<div class="reasoning-title" onclick="var n=this.nextElementSibling;n.style.display=n.style.display===\'none\'?\'block\':\'none\';this.textContent=this.textContent===\'' + __('展开思考','Expand') + '\'?\'' + __('收起思考','Collapse') + '\':\'' + __('展开思考','Expand') + '\'">' + __('展开思考','Expand') + '</div>'
|
||||
+ '<div class="reasoning-body" style="display:none">' + rcBody + '</div></div></div>';
|
||||
}
|
||||
var tcs = '';
|
||||
if (m.tool_calls && m.tool_calls.length > 0) {
|
||||
m.tool_calls.forEach(function(tc) {
|
||||
var argsStr = typeof tc.args === 'object' ? JSON.stringify(tc.args, null, 1) : (tc.args || '');
|
||||
var resultStr = tc.result ? (typeof tc.result === 'object' ? JSON.stringify(tc.result, null, 1).substring(0, 200) : String(tc.result).substring(0, 200)) : '';
|
||||
var statusIcon = tc.status === 'denied' ? '⛔' : '🔧';
|
||||
tcs += '<div class="tool-call">'
|
||||
+ '<div><span class="tc-name">' + statusIcon + ' ' + escHtml(tc.tool || tc.name || '') + '</span></div>'
|
||||
var resultStr = tc.result ? (typeof tc.result === 'object' ? JSON.stringify(tc.result, null, 1) : String(tc.result)) : '';
|
||||
var statusIcon = tc.status === 'denied'
|
||||
? '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="color:var(--error);vertical-align:-1px"><circle cx="12" cy="12" r="9"/><path d="M5.6 5.6l12.8 12.8"/></svg>'
|
||||
: '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="color:var(--accent);vertical-align:-1px"><path d="M14.7 6.3a4 4 0 0 0-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 0 0 5.4-5.4l-2.9 2.9-2.5-.6-.6-2.5z"/></svg>';
|
||||
tcs += '<div class="msg-bubble"><div class="tool-call' + (newlyDone.indexOf(tc.tool || tc.name || '') !== -1 ? ' tool-drip-in' : '') + '" onclick="toggleToolCall(this)">'
|
||||
+ '<div class="tc-line"><span class="tc-ico">' + statusIcon + '</span><span class="tc-name">' + escHtml(tc.tool || tc.name || '') + '</span>'
|
||||
+ (tc.status === 'denied'
|
||||
? '<span class="tc-state tc-deny">' + __('已拒绝','Denied') + '</span>'
|
||||
: (resultStr
|
||||
? '<span class="tc-state tc-done">' + __('完成','Done') + '</span>'
|
||||
: '<span class="tc-state tc-run">' + __('调用中','Running') + '</span>'))
|
||||
+ '<span class="tc-caret">▾</span></div>'
|
||||
+ '<div class="tc-detail" style="display:none">'
|
||||
+ (argsStr && argsStr !== '{}' ? '<div class="tc-args">' + escHtml(argsStr) + '</div>' : '')
|
||||
+ (resultStr ? '<div class="tc-result">→ ' + escHtml(resultStr) + '</div>' : '')
|
||||
+ '</div>';
|
||||
+ (resultStr ? '<div class="tc-result">' + escHtml(resultStr) + '</div>' : '')
|
||||
+ '</div></div></div>';
|
||||
});
|
||||
}
|
||||
var body = rc + tcs + '<div class="text">' + c + '</div>';
|
||||
if (m.source && m.source !== 'webui') {
|
||||
body = '<div class="msg-source">' + escHtml(__('通道','Channel')) + ': ' + escHtml(m.source) + '</div>' + body;
|
||||
var body = rc + tcs;
|
||||
var growCls = m._grow ? ' grow-in' : '';
|
||||
if (m._grow) m._grow = false;
|
||||
var isStreamingLast = i === msgs.length - 1 && streamingLast;
|
||||
if (isStreamingLast) {
|
||||
var liveRow = '<span class="live-spinner"></span>' + (newPending.length ? '<span class="thinking-tools">' + pillHtml() + '</span>' : '');
|
||||
if (c) {
|
||||
body += '<div class="msg-bubble' + growCls + '">' + liveRow + '<div class="text">' + c + '</div></div>';
|
||||
c = '';
|
||||
} else {
|
||||
body += '<div class="msg-bubble">' + liveRow + '</div>';
|
||||
}
|
||||
} else if (c) {
|
||||
body += '<div class="msg-bubble' + growCls + '"><div class="text">' + c + '</div></div>';
|
||||
}
|
||||
if (role === 'system') {
|
||||
html += '<div class="msg msg-system"><div class="msg-bubble">' + body + '</div></div>';
|
||||
html += '<div class="msg msg-system"><div class="msg-bubble">' + (c || '') + '</div></div>';
|
||||
} else if (isChan) {
|
||||
html += '<div class="msg msg-channel">'
|
||||
+ '<div class="msg-avatar chan-avatar" style="background:' + chanColor(m.source) + '">' + chanLetter(m.source) + '</div>'
|
||||
+ '<div class="msg-content"><div class="msg-chan-name">' + escHtml(m.source) + '</div>' + body + '</div>'
|
||||
+ '</div>';
|
||||
} else {
|
||||
var userAvatar = '<svg viewBox="0 0 24 24" style="width:16px;height:16px" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 4-6 8-6s8 2 8 6"/></svg>';
|
||||
var aiAvatar = '<img src="mascot.svg" style="width:28px;height:28px;border-radius:50%;object-fit:cover" alt="' + __('小宅','Agent') + '">';
|
||||
var aiAvatar = '<img src="mascot.webp" style="width:28px;height:28px;border-radius:50%;object-fit:cover" alt="' + __('小宅','Agent') + '">';
|
||||
html += '<div class="msg msg-' + role + '">'
|
||||
+ '<div class="msg-avatar">' + (role === 'user' ? userAvatar : aiAvatar) + '</div>'
|
||||
+ '<div class="msg-content"><div class="msg-bubble">' + body + '</div></div>'
|
||||
+ '<div class="msg-content">' + body + '</div>'
|
||||
+ '</div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
if (state.chatLoading && !streamingLast) {
|
||||
var aiAvatar2 = '<img src="mascot.webp" style="width:28px;height:28px;border-radius:50%;object-fit:cover" alt="' + __('小宅','Agent') + '">';
|
||||
html += '<div class="msg msg-assistant"><div class="msg-avatar">' + aiAvatar2 + '</div><div class="msg-content"><div class="msg-bubble">'
|
||||
+ '<span class="live-spinner"></span>'
|
||||
+ (newPending.length ? '<span class="thinking-tools">' + pillHtml() + '</span>' : '')
|
||||
+ '</div></div></div>';
|
||||
}
|
||||
msgsEl.innerHTML = html;
|
||||
msgsEl.scrollTop = msgsEl.scrollHeight;
|
||||
if (state.chatStick !== false) { try { msgsEl.scrollTo({ top: msgsEl.scrollHeight, behavior: 'smooth' }) } catch(e) { msgsEl.scrollTop = msgsEl.scrollHeight } }
|
||||
updateChatBadge();
|
||||
}
|
||||
|
||||
@ -395,11 +540,19 @@ function updateChatBadge() {
|
||||
var badge = document.getElementById('chat-stage');
|
||||
if (!badge) return;
|
||||
badge.textContent = state.chatStage || '';
|
||||
badge.style.display = state.chatLoading ? 'inline' : 'none';
|
||||
badge.style.display = 'none';
|
||||
}
|
||||
|
||||
function rerenderChat() { renderChat(); renderChatStarmap(); renderTerminals(); renderCmdHistory() }
|
||||
|
||||
function toggleToolCall(el) {
|
||||
var d = el.querySelector('.tc-detail');
|
||||
if (!d) return;
|
||||
var open = d.style.display !== 'none';
|
||||
d.style.display = open ? 'none' : 'block';
|
||||
if (open) { el.classList.remove('open'); } else { el.classList.add('open'); }
|
||||
}
|
||||
|
||||
function renderChatStarmap() {
|
||||
var cont = document.getElementById('sm-container-chat');
|
||||
if (!cont) return;
|
||||
@ -674,6 +827,7 @@ async function sendChat() {
|
||||
var btn = document.getElementById('chat-send-btn');
|
||||
var text = inp.value.trim();
|
||||
if (!text || state.chatLoading) return;
|
||||
state.chatStick = true;
|
||||
state.messages.push({ role: 'user', content: text });
|
||||
inp.value = '';
|
||||
rerenderChat();
|
||||
@ -690,7 +844,8 @@ async function sendChat() {
|
||||
if (last && last.role === 'assistant' && last._streaming) {
|
||||
console.log('[sendChat] updating existing streaming msg, tool_calls before:', last.tool_calls?.length);
|
||||
last.content = r.response || __('(无响应)','(no response)');
|
||||
last.reasoning_content = r.reasoning_content || '';
|
||||
last._grow = true;
|
||||
if (!last.reasoning_content) last.reasoning_content = r.reasoning_content || '';
|
||||
last._final = true;
|
||||
delete last._streaming;
|
||||
} else {
|
||||
@ -699,7 +854,8 @@ async function sendChat() {
|
||||
content: r.response || __('(无响应)','(no response)'),
|
||||
reasoning_content: r.reasoning_content,
|
||||
tool_calls: last && last.role === 'assistant' && last.tool_calls ? last.tool_calls : [],
|
||||
_final: true
|
||||
_final: true,
|
||||
_grow: true
|
||||
});
|
||||
}
|
||||
rerenderChat();
|
||||
@ -786,15 +942,19 @@ async function createKnowledgeChat() {
|
||||
}
|
||||
}
|
||||
|
||||
function switchChatSub(tab, el) {
|
||||
var cards = {
|
||||
'memory': document.getElementById('chat-sub-memory'),
|
||||
'context': document.getElementById('chat-sub-context'),
|
||||
'knowledge': document.getElementById('chat-sub-knowledge')
|
||||
function switchChatPanel(tab, el) {
|
||||
var panels = {
|
||||
'chat': document.getElementById('chat-panel-chat'),
|
||||
'starmap': document.getElementById('chat-panel-starmap'),
|
||||
'terminal': document.getElementById('chat-panel-terminal'),
|
||||
'cmd': document.getElementById('chat-panel-cmd'),
|
||||
'memory': document.getElementById('chat-panel-memory'),
|
||||
'context': document.getElementById('chat-panel-context'),
|
||||
'knowledge': document.getElementById('chat-panel-knowledge')
|
||||
};
|
||||
Object.keys(cards).forEach(function(k) {
|
||||
var c = cards[k];
|
||||
if (c) c.style.display = k === tab ? 'block' : 'none';
|
||||
Object.keys(panels).forEach(function(k) {
|
||||
var p = panels[k];
|
||||
if (p) p.classList.toggle('active', k === tab);
|
||||
});
|
||||
if (el) {
|
||||
var parent = el.parentElement;
|
||||
@ -803,7 +963,12 @@ function switchChatSub(tab, el) {
|
||||
el.classList.add('active');
|
||||
}
|
||||
}
|
||||
if (tab === 'starmap') { renderChatStarmap(); onStarmapResize(); }
|
||||
if (tab === 'terminal') renderTerminals();
|
||||
if (tab === 'cmd') renderCmdHistory();
|
||||
if (tab === 'memory') queryMemoryChat();
|
||||
if (tab === 'context') queryMemoryContext();
|
||||
if (tab === 'knowledge') searchKnowledgeChat();
|
||||
}
|
||||
|
||||
async function loadChatHistory() {
|
||||
@ -818,6 +983,15 @@ async function loadCmdHistory() {
|
||||
try { var data = await api('/cmd/history'); if (data && data.history) state.cmdHistory = data.history } catch(e) {}
|
||||
}
|
||||
|
||||
function appendTermBuf(el, text) {
|
||||
if (!text) return;
|
||||
el.textContent += text;
|
||||
if (el.textContent.length > 262144) {
|
||||
el.textContent = el.textContent.slice(el.textContent.length - 262144);
|
||||
}
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
|
||||
function renderTerminals() {
|
||||
var r = document.getElementById('term-list');
|
||||
var cnt = document.getElementById('term-count-badge');
|
||||
@ -831,23 +1005,26 @@ function renderTerminals() {
|
||||
var html = '';
|
||||
list.forEach(function(t, i) {
|
||||
var detailId = 'term-detail-' + i;
|
||||
var scr = (state.termScreens && state.termScreens[t.id]) || null;
|
||||
var running = scr ? scr.running : !!t.running;
|
||||
var fullOut = scr ? scr.output : t.output || '';
|
||||
if (!fullOut) {
|
||||
fullOut = '<span style="color:#5c6672">' + __('[终端暂无输出]','[No terminal output]') + '</span>';
|
||||
} else {
|
||||
fullOut = escHtml(fullOut);
|
||||
}
|
||||
html += '<div style="border:1px solid var(--border-color);border-radius:6px;margin-bottom:4px;font-size:11px">';
|
||||
html += '<div style="display:flex;align-items:center;gap:6px;padding:6px 8px;cursor:pointer;background:var(--bg-hover)" onclick="var d=document.getElementById(\'' + detailId + '\');d.style.display=d.style.display===\'none\'?\'block\':\'none\'">';
|
||||
html += '<span style="font-family:monospace;font-size:10px;flex:1">' + escHtml(t.id || '-') + '</span>';
|
||||
html += '<span style="flex:1;color:var(--text-muted)">' + escHtml(t.command || '') + '</span>';
|
||||
html += '<span class="badge ' + (t.running ? 'badge-green' : 'badge-red') + '">' + (t.running ? __('运行中','Running') : __('已关闭','Closed')) + '</span>';
|
||||
html += '<span style="flex:1;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(t.command || '') + '</span>';
|
||||
html += '<span class="badge ' + (running ? 'badge-green' : 'badge-red') + '">' + (running ? __('运行中','Running') : __('已关闭','Closed')) + '</span>';
|
||||
html += '<span style="color:var(--text-muted);font-size:10px">' + escHtml(t.created_at || '') + '</span>';
|
||||
html += '</div>';
|
||||
html += '<div id="' + detailId + '" style="display:none;padding:8px;border-top:1px solid var(--border-color);background:var(--bg-input)">';
|
||||
html += '<div class="kv-row"><span class="key">ID</span><span class="val" style="font-family:monospace">' + escHtml(t.id || '-') + '</span></div>';
|
||||
html += '<div class="kv-row"><span class="key">' + __('命令','Command') + '</span><span class="val">' + escHtml(t.command || '-') + '</span></div>';
|
||||
html += '<div class="kv-row"><span class="key">' + __('状态','Status') + '</span><span class="val">' + (t.running ? __('运行中','Running') : __('已关闭','Closed')) + '</span></div>';
|
||||
html += '<div class="kv-row"><span class="key">' + __('创建时间','Created') + '</span><span class="val">' + escHtml(t.created_at || '-') + '</span></div>';
|
||||
if (t.uptime) html += '<div class="kv-row"><span class="key">' + __('运行时长','Uptime') + '</span><span class="val">' + escHtml(t.uptime) + '</span></div>';
|
||||
if (t.output) {
|
||||
html += '<div class="kv-row"><span class="key">' + __('输出预览','Output') + '</span><span class="val"><pre style="font-size:10px;margin:0;max-height:100px;overflow:auto">' + escHtml((t.output || '').substring(0, 500)) + '</pre></span></div>';
|
||||
}
|
||||
html += '</div></div>';
|
||||
html += '<div class="term-screen">';
|
||||
html += '<div class="term-head"><span class="term-dot' + (running ? '' : ' stopped') + '" id="term-dot-' + escHtml(t.id) + '"></span><span style="font-weight:600">' + escHtml(t.id) + '</span><span>' + escHtml(t.command || '') + '</span><span style="flex:1"></span><span>' + escHtml(t.uptime || '') + '</span></div>';
|
||||
html += '<pre class="term-buf" id="term-buf-' + escHtml(t.id) + '">' + fullOut + '</pre>';
|
||||
html += '</div></div></div>';
|
||||
});
|
||||
r.innerHTML = html;
|
||||
}
|
||||
@ -856,19 +1033,24 @@ function renderCmdHistory() {
|
||||
var r = document.getElementById('cmd-list');
|
||||
var cnt = document.getElementById('cmd-count-badge');
|
||||
if (!r) return;
|
||||
var list = state.cmdHistory || [];
|
||||
if (cnt) cnt.textContent = list.length;
|
||||
if (list.length === 0) {
|
||||
r.innerHTML = '<p style="color:var(--text-muted);padding:8px;text-align:center;font-size:11px">' + __('暂无命令记录','No command history') + '</p>';
|
||||
var running = (state.terminals || []).filter(function(t) { return t.running; });
|
||||
if (cnt) cnt.textContent = running.length;
|
||||
if (running.length === 0) {
|
||||
r.innerHTML = '<p style="color:var(--text-muted);padding:8px;text-align:center;font-size:11px">' + __('暂无运行中的命令','No running commands') + '</p>';
|
||||
return;
|
||||
}
|
||||
var html = '<table style="font-size:10px"><tr><th>' + __('命令','Command') + '</th><th>' + __('状态','Status') + '</th><th>' + __('时间','Time') + '</th></tr>';
|
||||
list.slice().reverse().slice(0, 50).forEach(function(c) {
|
||||
var html = '<table style="font-size:10px"><tr><th>' + __('命令','Command') + '</th><th>' + __('状态','Status') + '</th><th>' + __('运行时长','Uptime') + '</th></tr>';
|
||||
running.forEach(function(t) {
|
||||
var scr = (state.termScreens && state.termScreens[t.id]) || null;
|
||||
var out = scr ? scr.output : t.output || '';
|
||||
html += '<tr>'
|
||||
+ '<td style="font-family:monospace;max-width:180px;overflow:hidden;text-overflow:ellipsis">' + escHtml(c.command || '') + '</td>'
|
||||
+ '<td><span class="badge ' + (c.status === 'ok' ? 'badge-green' : 'badge-red') + '">' + escHtml(c.status || '') + '</span></td>'
|
||||
+ '<td style="color:var(--text-muted);white-space:nowrap">' + escHtml((c.time || '').substring(0, 19)) + '</td>'
|
||||
+ '<td style="font-family:monospace;max-width:200px;overflow:hidden;text-overflow:ellipsis">' + escHtml(t.command || t.id || '') + '</td>'
|
||||
+ '<td><span class="badge badge-green">' + __('运行中','Running') + '</span></td>'
|
||||
+ '<td style="color:var(--text-muted);white-space:nowrap">' + escHtml(t.uptime || '-') + '</td>'
|
||||
+ '</tr>';
|
||||
if (out) {
|
||||
html += '<tr><td colspan="3" style="padding:0"><pre style="margin:0;padding:4px 8px;max-height:120px;overflow:auto;background:var(--bg-input);border-radius:4px;font-size:10px;color:var(--text-secondary)">' + escHtml(out.substring(0, 2000)) + '</pre></td></tr>';
|
||||
}
|
||||
});
|
||||
html += '</table>';
|
||||
r.innerHTML = html;
|
||||
@ -936,7 +1118,7 @@ function renderPlugins() {
|
||||
html += '<p style="color:var(--text-muted);font-size:13px">' + __('点击上方按钮运行','Click the button above to run') + '</p>';
|
||||
}
|
||||
html += '</div></div>';
|
||||
document.getElementById('tab-plugins').innerHTML = html;
|
||||
document.getElementById('view-plugins').innerHTML = html;
|
||||
}
|
||||
|
||||
async function loadInstalledPlugins() {
|
||||
@ -1029,7 +1211,7 @@ function renderHealthResult(r) {
|
||||
// ===== Kernel =====
|
||||
function renderKernel() {
|
||||
var k = state.kernel;
|
||||
if (!k) { document.getElementById('tab-kernel').innerHTML = '<div class="card"><p style="color:var(--text-muted)">' + __('内核未响应','Kernel not responding') + '</p></div>'; return }
|
||||
if (!k) { document.getElementById('view-kernel').innerHTML = '<div class="card"><p style="color:var(--text-muted)">' + __('内核未响应','Kernel not responding') + '</p></div>'; return }
|
||||
var html = '<div class="card"><h2>' + __('运行时','Runtime') + '</h2><div class="grid-3">'
|
||||
+ statCard('Goroutines', k?.runtime?.goroutines || '-', '')
|
||||
+ statCard(__('内存','Memory'), k?.runtime?.memory_mb ? k.runtime.memory_mb + ' MB' : '-', '')
|
||||
@ -1053,7 +1235,7 @@ function renderKernel() {
|
||||
html += '<p style="color:var(--text-muted)">' + __('无','None') + '</p>';
|
||||
}
|
||||
html += '</div>';
|
||||
document.getElementById('tab-kernel').innerHTML = html;
|
||||
document.getElementById('view-kernel').innerHTML = html;
|
||||
}
|
||||
|
||||
// ===== Star Map =====
|
||||
@ -1224,16 +1406,16 @@ function pluginDisplayName(p) {
|
||||
return name;
|
||||
}
|
||||
|
||||
function renderSettingsSidebar() {
|
||||
var el = document.querySelector('.settings-sidebar');
|
||||
function renderSettingsTabs() {
|
||||
var el = document.getElementById('settings-tabs');
|
||||
if (!el) return;
|
||||
el.innerHTML = '';
|
||||
state.settingsPlugins.forEach(function(p) {
|
||||
var a = document.createElement('a');
|
||||
a.textContent = pluginDisplayName(p);
|
||||
if (p === state.selectedSection) a.className = 'active';
|
||||
a.onclick = function() { state.selectedSection = p; renderOneSettings() };
|
||||
el.appendChild(a);
|
||||
var s = document.createElement('span');
|
||||
s.textContent = pluginDisplayName(p);
|
||||
if (p === state.selectedSection) s.className = 'active';
|
||||
s.onclick = function() { state.selectedSection = p; renderOneSettings() };
|
||||
el.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1262,7 +1444,10 @@ function renderOneSettings() {
|
||||
var regularKeys = filtered.filter(function(k) {
|
||||
return !k.startsWith('core.llm.sources.') && hideTopLlms.indexOf(k) === -1 && !k.startsWith('plugin.mcp.servers.') && k !== 'plugin.mcp.servers';
|
||||
});
|
||||
var html = '<div class="settings-layout"><div class="settings-sidebar"></div><div class="settings-content">';
|
||||
var html = '<div class="card"><h2>' + __('后端连接','Backend Connections') + '</h2>'
|
||||
+ '<div id="conn-manager"></div></div>'
|
||||
+ '<div class="settings-tabs" id="settings-tabs"></div>'
|
||||
+ '<div class="settings-content">';
|
||||
if (regularKeys.length === 0 && Object.keys(sourceMap).length === 0 && Object.keys(mcpServerMap).length === 0 && state.selectedSection !== 'plugin.mcp') {
|
||||
html += '<div class="card"><h2>' + escHtml(state.selectedSection) + '</h2><p style="color:var(--text-muted)">' + __('暂无设置项','No settings') + '</p></div>';
|
||||
} else {
|
||||
@ -1371,8 +1556,9 @@ function renderOneSettings() {
|
||||
}
|
||||
}
|
||||
html += '</div></div>';
|
||||
document.getElementById('tab-settings').innerHTML = html;
|
||||
renderSettingsSidebar();
|
||||
document.getElementById('view-settings').innerHTML = html;
|
||||
renderSettingsTabs();
|
||||
renderConnSection();
|
||||
}
|
||||
|
||||
function markDirty(k) {
|
||||
@ -1401,7 +1587,7 @@ async function saveSetting(k) {
|
||||
}
|
||||
|
||||
function renderConfigDisabled() {
|
||||
document.getElementById('tab-settings').innerHTML = '<div class="card"><h2>' + __('设置','Settings') + '</h2><p style="color:var(--text-muted)">' + __('设置面板已加载','Settings panel loaded') + '</p></div>';
|
||||
document.getElementById('view-settings').innerHTML = '<div class="card"><h2>' + __('设置','Settings') + '</h2><p style="color:var(--text-muted)">' + __('设置面板已加载','Settings panel loaded') + '</p></div>';
|
||||
renderOneSettings();
|
||||
}
|
||||
|
||||
@ -1458,8 +1644,7 @@ async function deleteMCPServer(name) {
|
||||
|
||||
// ===== Adapters =====
|
||||
async function renderAdapters() {
|
||||
var html = '<div class="card"><h2>' + __('Lua 适配器管理','Lua Adapter Management') + '</h2>'
|
||||
+ '<p style="color:var(--text-muted);font-size:12px;margin-bottom:12px">' + __('上传自定义 Lua 适配器脚本以支持新的 LLM 提供商。脚本文件将保存到适配器目录并自动加载到 Lua VM。','Upload custom Lua adapter scripts to support new LLM providers. Scripts are saved to the adapter directory and auto-loaded into the Lua VM.') + '</p></div>';
|
||||
var html = '';
|
||||
try {
|
||||
var r = await api('/adapters');
|
||||
var adapters = r.adapters || [];
|
||||
@ -1485,7 +1670,7 @@ async function renderAdapters() {
|
||||
} catch(e) {
|
||||
html += '<div class="card"><p style="color:var(--text-muted)">' + __('加载适配器失败: ','Failed to load adapters: ') + escHtml(e.message) + '</p></div>';
|
||||
}
|
||||
document.getElementById('tab-adapters').innerHTML = html;
|
||||
document.getElementById('view-adapters').innerHTML = html;
|
||||
}
|
||||
|
||||
async function uploadAdapter() {
|
||||
@ -1515,32 +1700,125 @@ async function deleteAdapter(name) {
|
||||
state.connections = data.connections || [];
|
||||
if (data.currentId) state.currentConn = state.connections.find(function(c) { return c.id === data.currentId }) || null;
|
||||
if (state.currentConn) {
|
||||
document.getElementById('app').style.display = 'block';
|
||||
connectSSE();
|
||||
await loadChatHistory();
|
||||
doRenderAll();
|
||||
startUptimeTicker();
|
||||
setInterval(doRenderAll, 15000);
|
||||
} else {
|
||||
document.getElementById('conn-overlay').style.display = 'flex';
|
||||
renderAll();
|
||||
updateConnIndicator();
|
||||
}
|
||||
renderConnList();
|
||||
})();
|
||||
|
||||
// ===== Connection Management =====
|
||||
function updateConnIndicator() {
|
||||
var el = document.getElementById('conn-name-display');
|
||||
var dot = document.getElementById('conn-dot');
|
||||
var rdot = document.getElementById('rail-conn-dot');
|
||||
if (state.currentConn) {
|
||||
el.textContent = state.currentConn.name;
|
||||
dot.className = 'status-dot ' + (state.status.status === 'running' ? 'dot-green pulse' : 'dot-yellow');
|
||||
var cls = state.status.status === 'running' ? 'dot-green pulse' : 'dot-yellow';
|
||||
dot.className = 'status-dot ' + cls;
|
||||
if (rdot) rdot.className = 'conn-dot ' + (state.status.status === 'running' ? 'dot-green' : 'dot-yellow');
|
||||
} else {
|
||||
el.textContent = '未连接';
|
||||
el.textContent = __('未连接','Not connected');
|
||||
dot.className = 'status-dot dot-gray';
|
||||
if (rdot) rdot.className = 'conn-dot';
|
||||
}
|
||||
}
|
||||
|
||||
function openConnManager() { renderConnList(); document.getElementById('conn-overlay').style.display = 'flex'; }
|
||||
function goSettingsConn() {
|
||||
switchView('settings');
|
||||
renderConnSection();
|
||||
}
|
||||
|
||||
function openConnManager() { renderConnSection(); switchView('settings'); }
|
||||
|
||||
function renderConnSection() {
|
||||
var cont = document.getElementById('conn-manager');
|
||||
if (!cont) return;
|
||||
cont.innerHTML = '';
|
||||
if (state.connections.length === 0) {
|
||||
cont.innerHTML += '<div class="card"><p style="color:var(--text-muted)">' + __('暂无后端连接,添加一个以开始使用','No backend connections yet. Add one to get started.') + '</p></div>';
|
||||
} else {
|
||||
state.connections.forEach(function(c) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'conn-item ' + (state.currentConn && state.currentConn.id === c.id ? 'active' : '');
|
||||
div.innerHTML = '<span class="status-dot ' + (state.currentConn && state.currentConn.id === c.id ? 'dot-green' : 'dot-gray') + '"></span>'
|
||||
+ '<div class="conn-info"><div class="conn-name">' + escHtml(c.name) + '</div><div class="conn-url">' + escHtml(c.url) + '</div></div>'
|
||||
+ '<div class="conn-actions">'
|
||||
+ '<button class="btn btn-ghost btn-sm" onclick="selectConnection(\'' + c.id + '\')">' + __('连接','Connect') + '</button> '
|
||||
+ '<button class="btn btn-ghost btn-sm" onclick="editConnection(\'' + c.id + '\', event)">' + __('编辑','Edit') + '</button> '
|
||||
+ '<button class="btn btn-danger btn-sm" onclick="deleteConnection(\'' + c.id + '\', event)">' + __('删除','Delete') + '</button></div>';
|
||||
cont.appendChild(div);
|
||||
});
|
||||
}
|
||||
var form = document.createElement('div');
|
||||
form.className = 'conn-form';
|
||||
form.id = 'conn-form';
|
||||
form.style.display = 'none';
|
||||
form.innerHTML = '<h3 id="conn-form-title">' + __('添加连接','Add Connection') + '</h3>'
|
||||
+ '<label>' + __('名称','Name') + '</label><input id="conn-name" placeholder="My HomeAgent">'
|
||||
+ '<label>' + __('连接类型','Type') + '</label><select id="conn-type" onchange="toggleConnType()">'
|
||||
+ '<option value="webui">WebUI (HTTP)</option>'
|
||||
+ '<option value="cli">CLI (unix socket)</option></select>'
|
||||
+ '<div id="conn-addr-webui"><label>' + __('地址','URL') + '</label><input id="conn-url" placeholder="http://localhost:18080"></div>'
|
||||
+ '<div id="conn-addr-cli" style="display:none"><label>' + __('Socket 路径','Socket Path') + '</label><input id="conn-sock" placeholder="C:\\path\\to\\cli.sock"></div>'
|
||||
+ '<label>' + __('API 密钥','API Key') + ' <span style="color:var(--text-muted);font-weight:400">(' + __('可选','optional') + ')</span></label>'
|
||||
+ '<input id="conn-key" type="password" placeholder="sk-...">'
|
||||
+ '<div class="conn-form-actions">'
|
||||
+ '<button class="btn btn-ghost" onclick="cancelConnForm()">' + __('取消','Cancel') + '</button>'
|
||||
+ '<button class="btn btn-primary" onclick="saveConnForm()" id="conn-save-btn">' + __('保存','Save') + '</button></div>';
|
||||
cont.appendChild(form);
|
||||
var addBtn = document.createElement('button');
|
||||
addBtn.className = 'btn btn-primary';
|
||||
addBtn.id = 'conn-add-btn';
|
||||
addBtn.textContent = '+ ' + __('添加连接','Add Connection');
|
||||
addBtn.style.marginTop = '8px';
|
||||
addBtn.onclick = showConnForm;
|
||||
cont.appendChild(addBtn);
|
||||
}
|
||||
|
||||
function toggleConnType() {
|
||||
var t = document.getElementById('conn-type').value;
|
||||
document.getElementById('conn-addr-webui').style.display = t === 'cli' ? 'none' : 'block';
|
||||
document.getElementById('conn-addr-cli').style.display = t === 'cli' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function showConnForm() {
|
||||
editingConnId = null;
|
||||
document.getElementById('conn-form-title').textContent = __('添加连接','Add Connection');
|
||||
document.getElementById('conn-name').value = '';
|
||||
document.getElementById('conn-url').value = 'http://localhost:18080';
|
||||
document.getElementById('conn-sock').value = '';
|
||||
document.getElementById('conn-key').value = '';
|
||||
document.getElementById('conn-type').value = 'webui';
|
||||
toggleConnType();
|
||||
document.getElementById('conn-form').style.display = 'block';
|
||||
document.getElementById('conn-add-btn').style.display = 'none';
|
||||
}
|
||||
|
||||
function editConnection(id, e) {
|
||||
if (e) e.stopPropagation();
|
||||
var c = state.connections.find(function(x) { return x.id === id });
|
||||
if (!c) return;
|
||||
editingConnId = id;
|
||||
document.getElementById('conn-form-title').textContent = __('编辑连接','Edit Connection');
|
||||
document.getElementById('conn-name').value = c.name;
|
||||
document.getElementById('conn-url').value = c.url || 'http://localhost:18080';
|
||||
document.getElementById('conn-sock').value = c.socketPath || '';
|
||||
document.getElementById('conn-key').value = c.apiKey;
|
||||
document.getElementById('conn-type').value = c.type === 'cli' ? 'cli' : 'webui';
|
||||
toggleConnType();
|
||||
document.getElementById('conn-form').style.display = 'block';
|
||||
document.getElementById('conn-add-btn').style.display = 'none';
|
||||
}
|
||||
|
||||
function cancelConnForm() {
|
||||
document.getElementById('conn-form').style.display = 'none';
|
||||
document.getElementById('conn-add-btn').style.display = 'block';
|
||||
}
|
||||
|
||||
async function selectConnection(id) {
|
||||
if (state.eventSource) { state.eventSource.close(); state.eventSource = null; }
|
||||
@ -1548,18 +1826,18 @@ async function selectConnection(id) {
|
||||
state.currentConn = data.connections.find(function(c) { return c.id === id }) || null;
|
||||
state.connections = data.connections;
|
||||
state.messages = [];
|
||||
document.getElementById('app').style.display = 'block';
|
||||
document.getElementById('conn-overlay').style.display = 'none';
|
||||
updateConnIndicator();
|
||||
connectSSE();
|
||||
await loadChatHistory();
|
||||
doRenderAll();
|
||||
startUptimeTicker();
|
||||
switchView('chat');
|
||||
renderConnSection();
|
||||
}
|
||||
|
||||
async function deleteConnection(id, e) {
|
||||
e.stopPropagation();
|
||||
if (!confirm('确定删除此连接?')) return;
|
||||
if (e) e.stopPropagation();
|
||||
if (!confirm(__('确定删除此连接?','Delete this connection?'))) return;
|
||||
var wasCurrent = state.currentConn && state.currentConn.id === id;
|
||||
var data = await window.homeagent.connections.delete(id);
|
||||
state.connections = data.connections;
|
||||
@ -1568,89 +1846,67 @@ async function deleteConnection(id, e) {
|
||||
if (state.currentConn) {
|
||||
updateConnIndicator(); doRenderAll(); connectSSE();
|
||||
} else {
|
||||
document.getElementById('app').style.display = 'none';
|
||||
document.getElementById('conn-overlay').style.display = 'flex';
|
||||
updateConnIndicator();
|
||||
}
|
||||
renderConnList();
|
||||
}
|
||||
|
||||
function renderConnList() {
|
||||
var list = document.getElementById('conn-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = state.connections.map(function(c) {
|
||||
return '<div class="conn-item ' + (state.currentConn && state.currentConn.id === c.id ? 'active' : '') + '" onclick="selectConnection(\'' + c.id + '\')">'
|
||||
+ '<span class="status-dot ' + (state.currentConn && state.currentConn.id === c.id ? 'dot-green' : 'dot-gray') + '"></span>'
|
||||
+ '<div class="conn-info"><div class="conn-name">' + escHtml(c.name) + '</div><div class="conn-url">' + escHtml(c.url) + '</div></div>'
|
||||
+ '<div class="conn-actions">'
|
||||
+ '<button class="btn btn-ghost btn-sm" onclick="editConnection(\'' + c.id + '\', event)">' + __('编辑','Edit') + '</button>'
|
||||
+ '<button class="btn btn-danger btn-sm" onclick="deleteConnection(\'' + c.id + '\', event)">' + __('删除','Delete') + '</button></div></div>';
|
||||
}).join('');
|
||||
renderConnSection();
|
||||
}
|
||||
|
||||
var editingConnId = null;
|
||||
|
||||
function showConnForm() {
|
||||
editingConnId = null;
|
||||
document.getElementById('conn-form-title').textContent = __('添加连接','Add Connection');
|
||||
document.getElementById('conn-name').value = '';
|
||||
document.getElementById('conn-url').value = 'http://localhost:8080';
|
||||
document.getElementById('conn-key').value = '';
|
||||
document.getElementById('conn-form').style.display = 'block';
|
||||
document.getElementById('conn-add-btn').style.display = 'none';
|
||||
}
|
||||
|
||||
function editConnection(id, e) {
|
||||
e.stopPropagation();
|
||||
var c = state.connections.find(function(x) { return x.id === id });
|
||||
if (!c) return;
|
||||
editingConnId = id;
|
||||
document.getElementById('conn-form-title').textContent = __('编辑连接','Edit Connection');
|
||||
document.getElementById('conn-name').value = c.name;
|
||||
document.getElementById('conn-url').value = c.url;
|
||||
document.getElementById('conn-key').value = c.apiKey;
|
||||
document.getElementById('conn-form').style.display = 'block';
|
||||
document.getElementById('conn-add-btn').style.display = 'none';
|
||||
document.querySelectorAll('.conn-item').forEach(function(el) { el.style.opacity = '0.4' });
|
||||
}
|
||||
|
||||
function cancelConnForm() {
|
||||
document.getElementById('conn-form').style.display = 'none';
|
||||
document.getElementById('conn-add-btn').style.display = 'block';
|
||||
document.querySelectorAll('.conn-item').forEach(function(el) { el.style.opacity = '1' });
|
||||
}
|
||||
|
||||
async function saveConnForm() {
|
||||
var name = document.getElementById('conn-name').value.trim();
|
||||
var ctype = document.getElementById('conn-type').value;
|
||||
var url = document.getElementById('conn-url').value.trim().replace(/\/+$/, '');
|
||||
var sock = document.getElementById('conn-sock').value.trim();
|
||||
var apiKey = document.getElementById('conn-key').value.trim();
|
||||
if (!name || !url) { toast(__('名称和地址不能为空','Name and URL required'), true); return; }
|
||||
if (ctype === 'cli') {
|
||||
if (!name || !sock) { toast(__('名称和 Socket 路径不能为空','Name and Socket Path required'), true); return; }
|
||||
} else {
|
||||
if (!name || !url) { toast(__('名称和地址不能为空','Name and URL required'), true); return; }
|
||||
}
|
||||
var testBtn = document.querySelector('#conn-form .btn-primary');
|
||||
testBtn.textContent = __('测试中...','Testing...'); testBtn.disabled = true;
|
||||
try {
|
||||
var testR = await fetch(url + '/api/v1/status', { headers: apiKey ? { 'X-API-Key': apiKey } : {} });
|
||||
if (!testR.ok) { toast(__('连接测试失败: HTTP ','Connection test failed: HTTP ') + testR.status, true); testBtn.textContent = __('保存 / Save','Save'); testBtn.disabled = false; return; }
|
||||
if (ctype === 'cli') {
|
||||
if (!window.homeagent.cli) throw new Error('cli bridge unavailable');
|
||||
var testR = await window.homeagent.cli.request(sock, apiKey, '/status');
|
||||
if (testR.error || testR.type === 'error') {
|
||||
toast(__('CLI 连接测试失败: ','CLI test failed: ') + (testR.error || testR.type), true);
|
||||
testBtn.textContent = __('保存','Save'); testBtn.disabled = false; return;
|
||||
}
|
||||
} else {
|
||||
var testR = await fetch(url + '/api/v1/status', { headers: apiKey ? { 'X-API-Key': apiKey } : {} });
|
||||
if (!testR.ok) { toast(__('连接测试失败: HTTP ','Connection test failed: HTTP ') + testR.status, true); testBtn.textContent = __('保存','Save'); testBtn.disabled = false; return; }
|
||||
}
|
||||
} catch(e) {
|
||||
toast(__('无法连接到 ','Cannot connect to ') + url + ': ' + e.message, true);
|
||||
testBtn.textContent = __('保存 / Save','Save'); testBtn.disabled = false; return;
|
||||
toast(__('无法连接到 ','Cannot connect to ') + (ctype === 'cli' ? sock : url) + ': ' + e.message, true);
|
||||
testBtn.textContent = __('保存','Save'); testBtn.disabled = false; return;
|
||||
}
|
||||
testBtn.textContent = __('保存 / Save','Save'); testBtn.disabled = false;
|
||||
testBtn.textContent = __('保存','Save'); testBtn.disabled = false;
|
||||
var connData = ctype === 'cli'
|
||||
? { name: name, type: 'cli', socketPath: sock, url: '', apiKey: apiKey }
|
||||
: { name: name, type: 'webui', url: url, apiKey: apiKey };
|
||||
var data;
|
||||
if (editingConnId) {
|
||||
data = await window.homeagent.connections.update(editingConnId, { name: name, url: url, apiKey: apiKey });
|
||||
data = await window.homeagent.connections.update(editingConnId, connData);
|
||||
} else {
|
||||
data = await window.homeagent.connections.add({ name: name, url: url, apiKey: apiKey });
|
||||
data = await window.homeagent.connections.add(connData);
|
||||
}
|
||||
state.connections = data.connections;
|
||||
var cur = data.connections.find(function(c) { return c.id === data.currentId });
|
||||
var switched = !!cur && (!state.currentConn || state.currentConn.id !== cur.id);
|
||||
if (cur) {
|
||||
state.currentConn = cur;
|
||||
if (!document.getElementById('app').style.display || document.getElementById('app').style.display === 'none') {
|
||||
document.getElementById('app').style.display = 'block';
|
||||
document.getElementById('conn-overlay').style.display = 'none';
|
||||
if (switched) {
|
||||
if (state.eventSource) { state.eventSource.close(); state.eventSource = null; }
|
||||
state.messages = [];
|
||||
updateConnIndicator(); connectSSE(); await loadChatHistory(); doRenderAll(); startUptimeTicker();
|
||||
} else { updateConnIndicator(); if (editingConnId) doRenderAll(); }
|
||||
switchView('chat');
|
||||
} else {
|
||||
updateConnIndicator(); doRenderAll();
|
||||
}
|
||||
}
|
||||
cancelConnForm(); renderConnList();
|
||||
cancelConnForm(); renderConnSection();
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
@ -1661,6 +1917,8 @@ document.addEventListener('keydown', function(e) {
|
||||
connectSSE = function() {
|
||||
if (state.eventSource) { state.eventSource.close(); state.eventSource = null; }
|
||||
if (!state.currentConn) return;
|
||||
// CLI 连接无 SSE 通道,聊天走同步 cli:request
|
||||
if (state.currentConn.type === 'cli') return;
|
||||
connectFetchSSE(state.currentConn.url + '/api/v1/chat/events');
|
||||
};
|
||||
|
||||
@ -1688,39 +1946,72 @@ async function connectFetchSSE(url) {
|
||||
var ev = JSON.parse(raw); var p = ev.payload || {};
|
||||
if (type === 'agent_output') {
|
||||
state.chatStage = __('AI 回复中...','AI replying...');
|
||||
if (state.messages.length > 0 && state.messages[state.messages.length - 1].role === 'assistant' && !state.messages[state.messages.length - 1]._final) {
|
||||
state.messages[state.messages.length - 1].content += (p.content || '');
|
||||
if (p.kind === 'channel_output') {
|
||||
state.messages.push({ role: 'assistant', content: p.content || '', source: p.channel || '', _final: true, _grow: true });
|
||||
rerenderChatIfActive(); return;
|
||||
}
|
||||
state.messages.push({ role: 'assistant', content: p.content || '', _streaming: true });
|
||||
var last = state.messages.length > 0 ? state.messages[state.messages.length - 1] : null;
|
||||
if (last && last.role === 'assistant' && !last._final) {
|
||||
last._grow = true;
|
||||
last.content += (p.content || '');
|
||||
rerenderChatIfActive(); return;
|
||||
}
|
||||
if (last && last.role === 'assistant' && last._final) { return; }
|
||||
state.messages.push({ role: 'assistant', content: p.content || '', _streaming: true, _grow: true });
|
||||
rerenderChatIfActive();
|
||||
} else if (type === 'reasoning') {
|
||||
if (p.content && state.messages.length > 0) {
|
||||
var last = state.messages[state.messages.length - 1];
|
||||
if (last.role === 'assistant') {
|
||||
state.chatStage = __('AI 思考中...','AI thinking...');
|
||||
last.reasoning_content = (last.reasoning_content || '') + (p.content || '');
|
||||
rerenderChatIfActive();
|
||||
if (p.content) {
|
||||
state.chatStage = __('AI 思考中...','AI thinking...');
|
||||
var last = state.messages.length > 0 ? state.messages[state.messages.length - 1] : null;
|
||||
if (!last || last.role !== 'assistant' || last._final) {
|
||||
state.messages.push({ role: 'assistant', content: '', reasoning_content: '', tool_calls: [], _streaming: true });
|
||||
last = state.messages[state.messages.length - 1];
|
||||
}
|
||||
last.reasoning_content = (last.reasoning_content || '') + (p.content || '');
|
||||
rerenderChatIfActive();
|
||||
}
|
||||
} else if (type === 'tool_call') {
|
||||
if (!p.tool) return;
|
||||
var last = state.messages.length > 0 ? state.messages[state.messages.length - 1] : null;
|
||||
if (!last || last.role !== 'assistant') {
|
||||
if (!last || last.role !== 'assistant' || last._final) {
|
||||
state.messages.push({ role: 'assistant', content: '', tool_calls: [], _streaming: true });
|
||||
last = state.messages[state.messages.length - 1];
|
||||
}
|
||||
if (!last.tool_calls) last.tool_calls = [];
|
||||
last.tool_calls.push({ tool: p.tool, name: p.tool, args: p.args || {}, result: p.result || '', status: p.status || 'ok', plugin: p.plugin || '' });
|
||||
var pidx = (state.pendingTools || []).indexOf(p.tool);
|
||||
if (pidx !== -1) state.pendingTools.splice(pidx, 1);
|
||||
state.chatStage = __('工具调用: ','Tool: ') + (p.tool || '');
|
||||
rerenderChatIfActive();
|
||||
} else if (type === 'terminal_output') {
|
||||
if (!p.terminal_id) return;
|
||||
var tid = p.terminal_id;
|
||||
if (!state.termScreens) state.termScreens = {};
|
||||
var scr = state.termScreens[tid] || (state.termScreens[tid] = { output: '', running: true });
|
||||
if (p.output) scr.output += p.output;
|
||||
if (typeof p.running === 'boolean') scr.running = p.running;
|
||||
var bufel = document.getElementById('term-buf-' + tid);
|
||||
if (bufel) {
|
||||
appendTermBuf(bufel, p.output || '');
|
||||
var dot = document.getElementById('term-dot-' + tid);
|
||||
if (dot) dot.className = 'term-dot' + (scr.running ? '' : ' stopped');
|
||||
}
|
||||
} else if (type === 'stage') {
|
||||
var phase = p.phase || ''; var tool = p.tool || '';
|
||||
if (phase === 'pre_action') state.chatStage = __('AI 思考中...','AI thinking...');
|
||||
else if (phase === 'before_toolcall') state.chatStage = __('工具调用: ','Tool: ') + (tool || '');
|
||||
else if (phase === 'before_output') state.chatStage = __('生成回复中...','Generating response...');
|
||||
if (p.channel !== '_consolidation_') {
|
||||
if (phase === 'pre_action') state.chatStage = __('AI 思考中...','AI thinking...');
|
||||
else if (phase === 'before_toolcall') {
|
||||
state.chatStage = __('工具调用: ','Tool: ') + (tool || '');
|
||||
if (tool && (state.pendingTools || []).indexOf(tool) === -1) {
|
||||
if (!state.pendingTools) state.pendingTools = [];
|
||||
state.pendingTools.push(tool);
|
||||
rerenderChatIfActive();
|
||||
}
|
||||
}
|
||||
else if (phase === 'before_output') state.chatStage = __('生成回复中...','Generating response...');
|
||||
}
|
||||
var badge = document.getElementById('chat-stage');
|
||||
if (badge) { badge.textContent = state.chatStage || ''; badge.style.display = state.chatLoading ? 'inline' : 'none' }
|
||||
if (badge) { badge.textContent = state.chatStage || ''; badge.style.display = 'none'; }
|
||||
}
|
||||
} catch(err) {}
|
||||
}
|
||||
@ -1735,7 +2026,7 @@ async function connectFetchSSE(url) {
|
||||
}
|
||||
|
||||
function rerenderChatIfActive() {
|
||||
var tab = document.getElementById('tab-chat');
|
||||
var tab = document.getElementById('view-chat');
|
||||
if (tab && tab.classList.contains('active')) { renderChat(); renderChatStarmap(); renderTerminals(); renderCmdHistory(); }
|
||||
}
|
||||
|
||||
|
||||
28
cmd/gui/renderer/icon.svg
Normal file
28
cmd/gui/renderer/icon.svg
Normal file
@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="400" height="400">
|
||||
<rect width="400" height="400" rx="60" ry="60" fill="#F8FAFC"/>
|
||||
|
||||
<g transform="translate(200,200)">
|
||||
<circle cx="0" cy="0" r="105" fill="none" stroke="#E2E8F0" stroke-width="2.5" stroke-dasharray="8,6"/>
|
||||
<rect x="-130" y="-170" width="36" height="340" rx="8" fill="#3B82F6"/>
|
||||
<rect x="94" y="-170" width="36" height="140" rx="8" fill="#CBD5E1"/>
|
||||
<rect x="94" y="70" width="36" height="100" rx="8" fill="#CBD5E1"/>
|
||||
|
||||
<rect x="-94" y="-18" width="188" height="36" rx="8" fill="#38BDF8"/>
|
||||
|
||||
<polygon points="0,-28 24.2,14 -24.2,14" fill="#F59E0B" stroke="#F59E0B" stroke-width="8" stroke-linejoin="round"/>
|
||||
|
||||
<circle cx="74" cy="-74" r="14" fill="#DBEAFE" stroke="#3B82F6" stroke-width="2.5"/>
|
||||
<line x1="28" y1="-28" x2="63" y2="-63" stroke="#3B82F6" stroke-width="3" stroke-dasharray="6,4" opacity="0.6"/>
|
||||
|
||||
<circle cx="-74" cy="-74" r="14" fill="#EDE9FE" stroke="#8B5CF6" stroke-width="2.5"/>
|
||||
<line x1="-28" y1="-28" x2="-63" y2="-63" stroke="#8B5CF6" stroke-width="3" stroke-dasharray="6,4" opacity="0.6"/>
|
||||
|
||||
<circle cx="74" cy="74" r="14" fill="#CFFAFE" stroke="#06B6D4" stroke-width="2.5"/>
|
||||
<line x1="28" y1="28" x2="63" y2="63" stroke="#06B6D4" stroke-width="3" stroke-dasharray="6,4" opacity="0.6"/>
|
||||
|
||||
<circle cx="-74" cy="74" r="14" fill="#FEF3C7" stroke="#F59E0B" stroke-width="2.5"/>
|
||||
<line x1="-28" y1="28" x2="-63" y2="63" stroke="#F59E0B" stroke-width="3" stroke-dasharray="6,4" opacity="0.6"/>
|
||||
|
||||
<circle cx="0" cy="0" r="42" fill="none" stroke="#F59E0B" stroke-width="4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HomeAgent</title>
|
||||
<link rel="icon" type="image/svg+xml" href="icon.svg">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js" onerror="window._THREE_FAILED=true"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js" onerror="window._THREE_FAILED=true"></script>
|
||||
@ -12,56 +13,67 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Connection Manager Overlay -->
|
||||
<div id="conn-overlay" class="overlay">
|
||||
<div class="overlay-content conn-manager">
|
||||
<h2>连接管理 / Connections</h2>
|
||||
<div class="conn-list" id="conn-list"></div>
|
||||
<div class="conn-form" id="conn-form" style="display:none">
|
||||
<h3 id="conn-form-title">添加连接 / Add Connection</h3>
|
||||
<label>名称 / Name</label>
|
||||
<input id="conn-name" placeholder="My HomeAgent">
|
||||
<label>地址 / URL</label>
|
||||
<input id="conn-url" placeholder="http://localhost:8080">
|
||||
<label>API 密钥 / API Key <span style="color:var(--text-muted);font-weight:400">(可选)</span></label>
|
||||
<input id="conn-key" type="password" placeholder="sk-...">
|
||||
<div class="conn-form-actions">
|
||||
<button class="btn btn-ghost" onclick="cancelConnForm()">取消 / Cancel</button>
|
||||
<button class="btn btn-primary" onclick="saveConnForm()">保存 / Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="showConnForm()" id="conn-add-btn" style="margin-top:12px">+ 添加连接 / Add Connection</button>
|
||||
<div class="titlebar" id="titlebar">
|
||||
<div class="titlebar-controls">
|
||||
<button class="tb-btn" id="tb-min" title="最小化" onclick="window.homeagent&&homeagent.win.minimize()">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><line x1="4" y1="12" x2="20" y2="12"/></svg>
|
||||
</button>
|
||||
<button class="tb-btn" id="tb-max" title="最大化" onclick="window.homeagent&&homeagent.win.toggleMaximize()">
|
||||
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><rect x="5" y="5" width="14" height="14" rx="1.5"/></svg>
|
||||
</button>
|
||||
<button class="tb-btn tb-close" id="tb-close" title="关闭" onclick="window.homeagent&&homeagent.win.close()">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main App -->
|
||||
<div id="app" style="display:none">
|
||||
<nav>
|
||||
<h1>HomeAgent</h1>
|
||||
<a class="active" onclick="switchTab('overview')" data-i18n="navOverview">概览</a>
|
||||
<a onclick="switchTab('chat')" data-i18n="navChat">对话</a>
|
||||
<a onclick="switchTab('plugins')" data-i18n="navPlugins">插件</a>
|
||||
<a onclick="switchTab('settings')" data-i18n="navSettings">设置</a>
|
||||
<a onclick="switchTab('adapters')" data-i18n="navAdapters">适配器</a>
|
||||
<a onclick="switchTab('kernel')" data-i18n="navKernel">内核</a>
|
||||
<div style="margin-left:auto;display:flex;align-items:center;gap:8px">
|
||||
<span id="conn-status" class="conn-indicator" onclick="openConnManager()" title="点击管理连接 / Click to manage connections">
|
||||
<div id="app">
|
||||
<!-- 左侧图标栏:主页=对话,其余为二级页面 -->
|
||||
<aside class="rail" id="rail">
|
||||
<div class="rail-logo"><img src="icon.svg" alt="HomeAgent"></div>
|
||||
<button class="rail-btn active" id="rail-chat" onclick="switchView('chat')" title="对话">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>
|
||||
</button>
|
||||
<button class="rail-btn" id="rail-overview" onclick="switchView('overview')" title="概览">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
||||
</button>
|
||||
<button class="rail-btn" id="rail-plugins" onclick="switchView('plugins')" title="插件">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
</button>
|
||||
<button class="rail-btn" id="rail-adapters" onclick="switchView('adapters')" title="适配器">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
|
||||
</button>
|
||||
<button class="rail-btn" id="rail-kernel" onclick="switchView('kernel')" title="内核">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 1v4M15 1v4M9 19v4M15 19v4M1 9h4M1 15h4M19 9h4M19 15h4"/></svg>
|
||||
</button>
|
||||
<div class="rail-spacer"></div>
|
||||
<button class="rail-btn" id="rail-settings" onclick="switchView('settings')" title="设置">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
</button>
|
||||
<button class="rail-btn" onclick="toggleTheme()" title="切换亮色/暗色模式" id="theme-btn"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg></button>
|
||||
<span class="conn-dot dot-gray" id="rail-conn-dot" title="连接状态"></span>
|
||||
</aside>
|
||||
|
||||
<div class="main">
|
||||
<div class="topbar">
|
||||
<h1>HomeAgent</h1>
|
||||
<span class="conn-indicator" id="conn-indicator" onclick="goSettingsConn()" title="点击管理后端连接">
|
||||
<span class="status-dot dot-gray" id="conn-dot"></span>
|
||||
<span id="conn-name-display">未连接</span>
|
||||
<span style="font-size:10px;margin-left:4px;opacity:0.6">▼</span>
|
||||
<span style="font-size:10px;margin-left:4px;opacity:.6">▼</span>
|
||||
</span>
|
||||
<span id="lang-btn" class="theme-btn" onclick="toggleLang()" style="font-size:13px;min-width:28px;cursor:pointer;padding:4px 8px;border:1px solid var(--border-color);border-radius:6px;text-align:center">EN</span>
|
||||
<button class="theme-btn" onclick="toggleTheme()" id="theme-btn" title="切换亮色/暗色模式">🌙</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="lang-btn" onclick="toggleLang()" id="lang-btn">EN</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container" id="app-content">
|
||||
<div id="tab-overview" class="tab-content active"></div>
|
||||
<div id="tab-chat" class="tab-content"></div>
|
||||
<div id="tab-plugins" class="tab-content"></div>
|
||||
<div id="tab-settings" class="tab-content"></div>
|
||||
<div id="tab-adapters" class="tab-content"></div>
|
||||
<div id="tab-kernel" class="tab-content"></div>
|
||||
<div class="container">
|
||||
<div id="view-chat" class="view active"></div>
|
||||
<div id="view-overview" class="view"></div>
|
||||
<div id="view-plugins" class="view"></div>
|
||||
<div id="view-adapters" class="view"></div>
|
||||
<div id="view-kernel" class="view"></div>
|
||||
<div id="view-settings" class="view"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<circle cx="50" cy="50" r="48" fill="#3B82F6"/>
|
||||
<circle cx="35" cy="40" r="6" fill="white"/>
|
||||
<circle cx="65" cy="40" r="6" fill="white"/>
|
||||
<circle cx="35" cy="40" r="3" fill="#1E3A5F"/>
|
||||
<circle cx="65" cy="40" r="3" fill="#1E3A5F"/>
|
||||
<path d="M35 65 Q50 80 65 65" stroke="white" stroke-width="3" fill="none" stroke-linecap="round"/>
|
||||
<ellipse cx="50" cy="58" rx="8" ry="4" fill="#F59E0B"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
BIN
cmd/gui/renderer/mascot.webp
Normal file
BIN
cmd/gui/renderer/mascot.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
@ -1,316 +1,496 @@
|
||||
:root {
|
||||
--bg-primary: #0f172a;
|
||||
--bg-secondary: #1e293b;
|
||||
--bg-card: #1e293b;
|
||||
--bg-input: #0f172a;
|
||||
--bg-hover: rgba(15,23,42,0.25);
|
||||
--text-primary: #e2e8f0;
|
||||
--text-secondary: #94a3b8;
|
||||
--text-muted: #64748b;
|
||||
--border-color: #334155;
|
||||
--accent: #38bdf8;
|
||||
--accent-bg: #1e3a5f;
|
||||
--toast-bg: #166534;
|
||||
--toast-color: #86efac;
|
||||
--toast-error-bg: #7f1d1d;
|
||||
--toast-error-color: #fca5a5;
|
||||
--pre-color: #a5b4fc;
|
||||
--pre-bg: #0f172a;
|
||||
--chat-bg: #0f172a;
|
||||
--msg-user-bg: #1e3a5f;
|
||||
--msg-user-color: #93c5fd;
|
||||
--msg-assistant-bg: #1a3a2a;
|
||||
--msg-assistant-color: #86efac;
|
||||
--msg-system-bg: #3b1a3a;
|
||||
--msg-system-color: #f0abfc;
|
||||
--kv-border: #1e293b;
|
||||
--btn-ghost-border: #334155;
|
||||
--btn-ghost-hover-bg: #1e293b;
|
||||
--save-btn-border: #eab308;
|
||||
--loading-border: #334155;
|
||||
--loading-top: #38bdf8;
|
||||
--sakura-100: #ffe4e9;
|
||||
--sakura-200: #ffcdd9;
|
||||
--sakura-300: #ff9eb5;
|
||||
--sakura-400: #ff7fac;
|
||||
--sakura-500: #f33b7c;
|
||||
--sakura-600: #c92462;
|
||||
--sakura-700: #991b4b;
|
||||
--frost-100: #d7e8ee;
|
||||
--frost-200: #a9d6e3;
|
||||
--frost-300: #88c0d0;
|
||||
--frost-400: #5ea8bf;
|
||||
--frost-500: #3f88a3;
|
||||
--success: #17a964;
|
||||
--warning: #d99a2b;
|
||||
--error: #db3694;
|
||||
--info: #3f6ef5;
|
||||
--glass-bg: rgba(13, 18, 34, 0.6);
|
||||
--glass-bg-strong: rgba(13, 18, 34, 0.82);
|
||||
--glass-blur: 14px;
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--glass-hover: rgba(255, 255, 255, 0.05);
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.25);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.28);
|
||||
--shadow-lg: 0 12px 36px rgba(0, 0, 0, 0.38);
|
||||
--shadow-glow: 0 0 0 1px rgba(255, 127, 172, 0.4), 0 4px 20px rgba(255, 127, 172, 0.18);
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 14px;
|
||||
--radius-pill: 999px;
|
||||
--font-sans: "Segoe UI", "Segoe UI Variable Text", -apple-system, BlinkMacSystemFont, Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif;
|
||||
--font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", "SF Mono", "Menlo", "Consolas", monospace;
|
||||
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--dur-micro: 150ms;
|
||||
--dur-normal: 300ms;
|
||||
--bg-primary: #0b1020;
|
||||
--bg-secondary: rgba(17, 24, 44, 0.72);
|
||||
--bg-card: rgba(17, 24, 44, 0.6);
|
||||
--bg-input: rgba(13, 18, 34, 0.75);
|
||||
--bg-hover: rgba(255, 255, 255, 0.06);
|
||||
--text-primary: #eef1f8;
|
||||
--text-secondary: #a7b0c4;
|
||||
--text-muted: #77809a;
|
||||
--border-color: rgba(255, 255, 255, 0.09);
|
||||
--accent: #ff7fac;
|
||||
--accent-bg: rgba(255, 127, 172, 0.14);
|
||||
--toast-bg: rgba(23, 169, 100, 0.16);
|
||||
--toast-color: #6ee7a8;
|
||||
--toast-error-bg: rgba(219, 54, 148, 0.18);
|
||||
--toast-error-color: #ff9ec6;
|
||||
--pre-color: #cdd3f5;
|
||||
--pre-bg: rgba(10, 14, 28, 0.85);
|
||||
--chat-bg: rgba(10, 14, 28, 0.7);
|
||||
--msg-user-bg: rgba(255, 127, 172, 0.16);
|
||||
--msg-user-color: #ffb9d0;
|
||||
--msg-assistant-bg: rgba(136, 192, 208, 0.14);
|
||||
--msg-assistant-color: #a9d6e3;
|
||||
--msg-system-bg: rgba(63, 110, 245, 0.16);
|
||||
--msg-system-color: #a3b8ff;
|
||||
--msg-bubble-bg: #161b2e;
|
||||
--msg-bubble-color: #e9edf6;
|
||||
--msg-bubble-border: rgba(255, 255, 255, 0.1);
|
||||
--kv-border: rgba(255, 255, 255, 0.07);
|
||||
--btn-ghost-border: rgba(255, 255, 255, 0.14);
|
||||
--btn-ghost-hover-bg: rgba(255, 255, 255, 0.07);
|
||||
--save-btn-border: #d99a2b;
|
||||
--loading-border: rgba(255, 255, 255, 0.12);
|
||||
--loading-top: #ff7fac;
|
||||
}
|
||||
[data-theme=light] {
|
||||
--bg-primary: #f8fafc;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-card: #ffffff;
|
||||
--bg-input: #f1f5f9;
|
||||
--bg-hover: rgba(241,245,249,0.8);
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--text-muted: #94a3b8;
|
||||
--border-color: #e2e8f0;
|
||||
--accent: #2563eb;
|
||||
--accent-bg: #dbeafe;
|
||||
--toast-bg: #166534;
|
||||
--toast-color: #86efac;
|
||||
--toast-error-bg: #7f1d1d;
|
||||
--toast-error-color: #fca5a5;
|
||||
--pre-color: #1e293b;
|
||||
--pre-bg: #f1f5f9;
|
||||
--chat-bg: #f1f5f9;
|
||||
--msg-user-bg: #dbeafe;
|
||||
--msg-user-color: #1e40af;
|
||||
--msg-assistant-bg: #dcfce7;
|
||||
--msg-assistant-color: #166534;
|
||||
--msg-system-bg: #f3e8ff;
|
||||
--msg-system-color: #7c3aed;
|
||||
--kv-border: #e2e8f0;
|
||||
--btn-ghost-border: #e2e8f0;
|
||||
--btn-ghost-hover-bg: #f1f5f9;
|
||||
--save-btn-border: #eab308;
|
||||
--loading-border: #e2e8f0;
|
||||
--loading-top: #2563eb;
|
||||
--glass-bg: rgba(255, 255, 255, 0.66);
|
||||
--glass-bg-strong: rgba(255, 255, 255, 0.88);
|
||||
--glass-border: rgba(255, 127, 172, 0.22);
|
||||
--glass-hover: rgba(255, 127, 172, 0.07);
|
||||
--shadow-sm: 0 1px 3px rgba(153, 27, 75, 0.08);
|
||||
--shadow-md: 0 6px 20px rgba(153, 27, 75, 0.1);
|
||||
--shadow-lg: 0 16px 40px rgba(153, 27, 75, 0.14);
|
||||
--shadow-glow: 0 0 0 1px rgba(243, 59, 124, 0.3), 0 6px 22px rgba(243, 59, 124, 0.14);
|
||||
--bg-primary: #fdf3f7;
|
||||
--bg-secondary: rgba(255, 255, 255, 0.78);
|
||||
--bg-card: rgba(255, 255, 255, 0.72);
|
||||
--bg-input: rgba(255, 224, 233, 0.55);
|
||||
--bg-hover: rgba(255, 127, 172, 0.08);
|
||||
--text-primary: #3b2030;
|
||||
--text-secondary: #7a5c6b;
|
||||
--text-muted: #a48a96;
|
||||
--border-color: rgba(201, 36, 98, 0.14);
|
||||
--accent: #c92462;
|
||||
--accent-bg: #ffe4e9;
|
||||
--toast-bg: rgba(23, 169, 100, 0.14);
|
||||
--toast-color: #128a52;
|
||||
--toast-error-bg: rgba(219, 54, 148, 0.12);
|
||||
--toast-error-color: #c2185b;
|
||||
--pre-color: #5c4060;
|
||||
--pre-bg: #fff0f5;
|
||||
--chat-bg: #fff0f5;
|
||||
--msg-user-bg: #ffe4e9;
|
||||
--msg-user-color: #c2185b;
|
||||
--msg-assistant-bg: #e3f2f6;
|
||||
--msg-assistant-color: #2f7188;
|
||||
--msg-bubble-bg: #ffffff;
|
||||
--msg-bubble-color: #262a33;
|
||||
--msg-bubble-border: rgba(201, 36, 98, 0.14);
|
||||
--msg-system-bg: #e6ecfe;
|
||||
--msg-system-color: #3f6ef5;
|
||||
--kv-border: rgba(201, 36, 98, 0.1);
|
||||
--btn-ghost-border: rgba(201, 36, 98, 0.22);
|
||||
--btn-ghost-hover-bg: #ffe4e9;
|
||||
--save-btn-border: #d99a2b;
|
||||
--loading-border: rgba(201, 36, 98, 0.18);
|
||||
--loading-top: #c92462;
|
||||
}
|
||||
* { margin:0; padding:0; box-sizing:border-box; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif }
|
||||
body { background:var(--bg-primary); color:var(--text-primary); min-height:100vh; overflow-x:hidden; transition:background .2s,color .2s }
|
||||
nav { background:var(--bg-secondary); padding:0 24px; display:flex; align-items:center; gap:4px; border-bottom:1px solid var(--border-color); height:48px; position:sticky; top:0; z-index:100; transition:background .2s,border .2s }
|
||||
nav h1 { font-size:16px; font-weight:700; color:var(--accent); margin-right:24px; white-space:nowrap }
|
||||
nav a { padding:12px 16px; color:var(--text-secondary); text-decoration:none; font-size:13px; cursor:pointer; border-bottom:2px solid transparent; transition:color .12s,border-color .12s }
|
||||
nav a:hover { color:var(--text-primary) }
|
||||
nav a.active { color:var(--accent); border-bottom-color:var(--accent) }
|
||||
.theme-btn { background:none; border:1px solid var(--border-color); color:var(--text-secondary); cursor:pointer; padding:4px 8px; border-radius:6px; font-size:14px; line-height:1; margin-right:8px; transition:all .15s }
|
||||
.theme-btn:hover { color:var(--accent); border-color:var(--accent) }
|
||||
.container { padding:20px 24px; max-width:1440px; margin:0 auto }
|
||||
.card { background:var(--bg-card); border:1px solid var(--border-color); border-radius:10px; padding:20px; margin-bottom:16px; transition:background .2s,border .2s }
|
||||
.card h2 { font-size:15px; font-weight:600; margin-bottom:12px; color:var(--text-primary) }
|
||||
.card h3 { font-size:13px; font-weight:600; color:var(--text-secondary); margin:16px 0 8px }
|
||||
* { margin:0; padding:0; box-sizing:border-box; font-family:var(--font-sans) }
|
||||
.selectable, .msg .text, .msg .tc-args, .msg .tc-result, .msg .reasoning-body, pre, input, textarea, .term-buf {
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
body {
|
||||
background:
|
||||
radial-gradient(900px 700px at 85% -10%, rgba(255,127,172,0.14), transparent 60%),
|
||||
radial-gradient(800px 600px at -10% 20%, rgba(136,192,208,0.12), transparent 60%),
|
||||
radial-gradient(700px 500px at 50% 110%, rgba(243,59,124,0.1), transparent 60%),
|
||||
var(--bg-primary);
|
||||
background-attachment: fixed;
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
transition: background .2s, color .2s;
|
||||
font-size: 14px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
#app { display:flex; height:100vh; overflow:hidden }
|
||||
|
||||
/* ===== 沉浸式标题栏 ===== */
|
||||
.titlebar {
|
||||
position: fixed; top: 0; left: 0; right: 0; height: 36px; z-index: 1100;
|
||||
display: flex; align-items: center;
|
||||
background: var(--bg-secondary);
|
||||
backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
-webkit-app-region: drag; user-select: none;
|
||||
}
|
||||
.titlebar-title { font-size: 12px; font-weight: 600; color: var(--text-muted); letter-spacing: .02em; padding-left: 12px; display: flex; align-items: center; gap: 8px }
|
||||
.titlebar-logo { width: 18px; height: 18px; border-radius: 50%; object-fit: cover; flex-shrink: 0 }
|
||||
.titlebar-controls { margin-left: auto; display: flex; height: 36px; -webkit-app-region: no-drag }
|
||||
.tb-btn {
|
||||
width: 46px; height: 36px; border: none; background: transparent;
|
||||
color: var(--text-secondary); display: flex; align-items: center; justify-content: center;
|
||||
cursor: pointer; transition: background .13s;
|
||||
}
|
||||
.tb-btn:hover { background: var(--glass-hover); color: var(--text-primary) }
|
||||
.tb-btn.tb-close:hover { background: #e81123; color: #fff }
|
||||
body.maximized .tb-max svg { transform: scale(.85) }
|
||||
|
||||
/* ===== Icon Rail (主视图导航) ===== */
|
||||
.rail {
|
||||
width: 56px;
|
||||
flex-shrink: 0;
|
||||
background: var(--bg-secondary);
|
||||
backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
|
||||
border-right: 1px solid var(--glass-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
gap: 4px;
|
||||
position: sticky;
|
||||
top: 36px;
|
||||
height: calc(100% - 36px);
|
||||
z-index: 100;
|
||||
}
|
||||
.rail .rail-logo { width:34px; height:34px; margin-bottom:10px; overflow:hidden }
|
||||
.rail .rail-logo img { width:100%; height:100%; border-radius:50%; object-fit:cover }
|
||||
.rail .rail-btn {
|
||||
width: 38px; height: 38px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border: none; background: transparent; color: var(--text-muted);
|
||||
cursor: pointer; border-radius: var(--radius-md);
|
||||
transition: all .15s; position: relative;
|
||||
}
|
||||
.rail .rail-btn svg { width:20px; height:20px }
|
||||
.rail .rail-btn:hover { color: var(--text-primary); background: var(--glass-hover) }
|
||||
.rail .rail-btn.active { color: var(--accent); background: var(--accent-bg) }
|
||||
.rail .rail-btn.active::before {
|
||||
content: ""; position: absolute; left: -8px; top: 8px; bottom: 8px; width: 3px;
|
||||
background: var(--accent); border-radius: var(--radius-pill);
|
||||
}
|
||||
.rail .rail-btn[title]:hover::after {
|
||||
content: attr(title);
|
||||
position: absolute; left: 46px; top: 50%; transform: translateY(-50%);
|
||||
background: var(--glass-bg-strong); backdrop-filter: blur(8px);
|
||||
color: var(--text-primary); font-size: 12px; padding: 4px 10px;
|
||||
border-radius: var(--radius-sm); border: 1px solid var(--glass-border);
|
||||
white-space: nowrap; z-index: 200; pointer-events: none;
|
||||
}
|
||||
.rail .rail-spacer { flex: 1 }
|
||||
.rail .conn-dot {
|
||||
width: 10px; height: 10px; border-radius: 50%;
|
||||
background: var(--text-muted); margin-bottom: 4px;
|
||||
}
|
||||
.rail .conn-dot.dot-green { background: var(--success); box-shadow: 0 0 8px rgba(23,169,100,.6) }
|
||||
.rail .conn-dot.dot-red { background: var(--error) }
|
||||
.rail .conn-dot.dot-yellow { background: var(--warning) }
|
||||
|
||||
/* ===== Main ===== */
|
||||
.main {
|
||||
flex: 1; min-width: 0; min-height: 0;
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding-top: 36px;
|
||||
}
|
||||
.topbar {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 10px 24px;
|
||||
background: var(--bg-secondary);
|
||||
backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
position: sticky; top: 0; z-index: 50;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.topbar h1 { font-size: 16px; font-weight: 700; color: var(--accent); white-space: nowrap }
|
||||
.topbar .conn-indicator {
|
||||
display: flex; align-items: center; gap: 6px; cursor: pointer;
|
||||
padding: 4px 10px; border-radius: var(--radius-pill);
|
||||
font-size: 12px; color: var(--text-secondary);
|
||||
border: 1px solid var(--glass-border);
|
||||
transition: all .15s; user-select: none;
|
||||
}
|
||||
.topbar .conn-indicator:hover { background: var(--glass-hover); color: var(--text-primary) }
|
||||
.topbar .spacer { flex: 1 }
|
||||
.theme-btn {
|
||||
background: none; border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary); cursor: pointer;
|
||||
padding: 4px 8px; border-radius: var(--radius-sm); font-size: 14px; line-height: 1;
|
||||
transition: all .15s;
|
||||
}
|
||||
.theme-btn:hover { color: var(--accent); border-color: var(--accent) }
|
||||
.lang-btn { font-size: 13px; min-width: 28px; cursor: pointer; padding: 4px 8px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); text-align: center; background:none; color:var(--text-secondary) }
|
||||
.lang-btn:hover { color: var(--accent); border-color: var(--accent) }
|
||||
.container { padding: 16px 24px; flex: 1; min-width: 0; min-height: 0; overflow: hidden }
|
||||
.view { display: none; height: 100%; overflow-y: auto }
|
||||
.view.active { display: block; animation: viewIn .18s var(--ease-out) }
|
||||
@keyframes viewIn { from { opacity: 0; transform: translateY(6px) } to { opacity: 1; transform: translateY(0) } }
|
||||
|
||||
/* ===== Cards ===== */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(var(--glass-blur)) saturate(1.3);
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.3);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: background .2s, border .2s, box-shadow .2s;
|
||||
}
|
||||
.card:hover { box-shadow: var(--shadow-md) }
|
||||
.card h2 { font-size: 15px; font-weight: 600; margin-bottom: 12px; color: var(--text-primary) }
|
||||
.card h3 { font-size: 13px; font-weight: 600; color: var(--text-secondary); margin: 16px 0 8px }
|
||||
.grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:16px }
|
||||
.grid-3 { display:grid; grid-template-columns:1fr 1fr 1fr; gap:16px }
|
||||
.grid-4 { display:grid; grid-template-columns:repeat(4,1fr); gap:16px }
|
||||
.stat-value { font-size:26px; font-weight:700; color:var(--accent) }
|
||||
.stat-label { font-size:11px; color:var(--text-muted); margin-top:2px }
|
||||
.stat-card { padding:16px 20px; transition:transform .12s }
|
||||
.stat-card:hover { transform:translateY(-2px) }
|
||||
.card:hover h2 { transition:color .12s }
|
||||
.stat-value { font-size: 26px; font-weight: 700; color: var(--accent) }
|
||||
.stat-label { font-size: 11px; color: var(--text-muted); margin-top: 2px }
|
||||
.stat-card { padding: 16px 20px; transition: transform .12s var(--ease-out) }
|
||||
.stat-card:hover { transform: translateY(-2px) }
|
||||
.status-dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px }
|
||||
.dot-green { background:#22c55e }
|
||||
.dot-green.pulse { animation:pulseDot 2s ease-in-out infinite }
|
||||
.dot-yellow { background:#eab308 }
|
||||
.dot-red { background:#ef4444 }
|
||||
.dot-gray { background:#475569 }
|
||||
@keyframes pulseDot {
|
||||
0%,100% { opacity:1; transform:scale(1) }
|
||||
50% { opacity:.6; transform:scale(1.3) }
|
||||
}
|
||||
.dot-green { background: var(--success) }
|
||||
.dot-green.pulse { animation: pulseDot 2s ease-in-out infinite }
|
||||
.dot-yellow { background: var(--warning) }
|
||||
.dot-red { background: var(--error) }
|
||||
.dot-gray { background: #475569 }
|
||||
@keyframes pulseDot { 0%,100% { opacity:1; transform:scale(1) } 50% { opacity:.6; transform:scale(1.3) } }
|
||||
table { width:100%; border-collapse:collapse; font-size:13px }
|
||||
th { text-align:left; padding:8px 10px; color:var(--text-muted); font-weight:500; border-bottom:1px solid var(--border-color); font-size:11px; text-transform:uppercase; letter-spacing:.5px }
|
||||
td { padding:8px 10px; border-bottom:1px solid var(--kv-border) }
|
||||
tr:hover td { background:var(--bg-hover) }
|
||||
tr:hover td { background: var(--bg-hover) }
|
||||
.badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:11px; font-weight:500 }
|
||||
.badge-green { background:#166534; color:#86efac }
|
||||
.badge-red { background:#7f1d1d; color:#fca5a5 }
|
||||
.badge-yellow { background:#713f12; color:#fde68a }
|
||||
.badge-blue { background:#1e3a5f; color:#93c5fd }
|
||||
.btn { padding:6px 14px; border-radius:6px; border:none; font-size:12px; cursor:pointer; font-weight:500; transition:background .12s,color .12s,border-color .12s }
|
||||
.btn:active { transform:scale(.97) }
|
||||
.btn-primary { background:var(--accent); color:#fff }
|
||||
.btn-primary:hover { background:#60c8f8 }
|
||||
.btn-danger { background:#dc2626; color:#fff }
|
||||
.btn-danger:hover { background:#b91c1c }
|
||||
.btn-sm { padding:4px 10px; font-size:11px }
|
||||
.btn-ghost { background:transparent; border:1px solid var(--btn-ghost-border); color:var(--text-secondary) }
|
||||
.btn-ghost:hover { background:var(--btn-ghost-hover-bg); color:var(--text-primary) }
|
||||
.tab-content { display:none }
|
||||
.tab-content.active { display:block; animation:tabIn .15s ease }
|
||||
@keyframes tabIn { from { opacity:0 } to { opacity:1 } }
|
||||
input,textarea,select { background:var(--bg-input); border:1px solid var(--border-color); border-radius:6px; padding:8px 12px; color:var(--text-primary); font-size:13px; width:100%; margin-bottom:10px; outline:none; transition:border .15s,background .2s,color .2s }
|
||||
input:focus,textarea:focus,select:focus { border-color:var(--accent) }
|
||||
textarea { resize:vertical; min-height:80px; font-family:monospace; font-size:12px }
|
||||
label { display:block; font-size:11px; color:var(--text-secondary); margin-bottom:3px; font-weight:500 }
|
||||
pre { background:var(--pre-bg); border-radius:6px; padding:12px; font-size:12px; overflow-x:auto; color:var(--pre-color); font-family:monospace; max-height:400px; overflow-y:auto }
|
||||
code { font-family:monospace; font-size:12px; color:var(--pre-color) }
|
||||
.settings-layout { display:flex; gap:20px; min-height:60vh }
|
||||
.settings-sidebar { width:200px; flex-shrink:0; background:var(--bg-card); border:1px solid var(--border-color); border-radius:10px; padding:8px 0; overflow-y:auto; max-height:70vh }
|
||||
.settings-sidebar a { display:block; padding:9px 16px; color:var(--text-secondary); font-size:13px; cursor:pointer; text-decoration:none; border-left:3px solid transparent; transition:all .1s }
|
||||
.settings-sidebar a:hover { background:var(--bg-primary); color:var(--text-primary) }
|
||||
.settings-sidebar a.active { background:var(--bg-primary); color:var(--accent); border-left-color:var(--accent) }
|
||||
.settings-content { flex:1; min-width:0 }
|
||||
.settings-key { font-family:monospace; font-size:11px; color:var(--text-muted); margin-bottom:2px }
|
||||
.reasoning { border-left:2px solid #888; padding-left:12px; margin:8px 0; font-size:12px; color:#999 }
|
||||
.reasoning-title { cursor:pointer; font-size:11px; color:#666; font-weight:600; user-select:none; margin-bottom:4px }
|
||||
.reasoning-body { color:#999; line-height:1.5 }
|
||||
.reasoning-body p { margin:4px 0 }
|
||||
.msg-content .text h1,
|
||||
.msg-content .text h2,
|
||||
.msg-content .text h3 { font-size:1em; margin:8px 0 4px; color:var(--text-primary) }
|
||||
.msg-content .text p { margin:4px 0; line-height:1.5 }
|
||||
.msg-content .text ul,
|
||||
.msg-content .text ol { padding-left:20px; margin:4px 0 }
|
||||
.msg-content .text li { margin:2px 0 }
|
||||
.msg-content .text code { background:var(--pre-bg); padding:1px 4px; border-radius:3px; font-size:11px }
|
||||
.msg-content .text pre { background:var(--pre-bg); border-radius:6px; padding:10px; margin:8px 0; overflow-x:auto; font-size:11px; max-height:300px }
|
||||
.msg-content .text pre code { background:none; padding:0 }
|
||||
.msg-content .text blockquote { border-left:3px solid var(--border-color); padding-left:10px; margin:8px 0; color:var(--text-secondary) }
|
||||
.msg-content .text table { border-collapse:collapse; margin:8px 0; font-size:12px; width:100% }
|
||||
.msg-content .text th,
|
||||
.msg-content .text td { border:1px solid var(--border-color); padding:4px 8px; text-align:left }
|
||||
.msg-content .text img { max-width:100%; border-radius:6px }
|
||||
.toast { position:fixed; bottom:20px; right:20px; background:var(--toast-bg); color:var(--toast-color); padding:10px 20px; border-radius:8px; font-size:13px; display:none; z-index:100; box-shadow:0 4px 12px rgba(0,0,0,.3); animation:toastIn .15s ease }
|
||||
.toast.error { background:var(--toast-error-bg); color:var(--toast-error-color) }
|
||||
@keyframes toastIn {
|
||||
from { opacity:0; transform:translateX(30px) }
|
||||
to { opacity:1; transform:translateX(0) }
|
||||
.badge-green { background: rgba(23,169,100,.18); color: #6ee7a8 }
|
||||
.badge-red { background: rgba(219,54,148,.18); color: #ff9ec6 }
|
||||
.badge-yellow { background: rgba(217,154,43,.18); color: #fcd9a0 }
|
||||
.badge-blue { background: rgba(63,110,245,.18); color: #a3b8ff }
|
||||
.btn {
|
||||
padding: 6px 14px; border-radius: var(--radius-md); border: none;
|
||||
font-size: 12px; cursor: pointer; font-weight: 500;
|
||||
transition: all .12s var(--ease-out);
|
||||
}
|
||||
.empty-state { text-align:center; padding:40px 20px; color:var(--text-muted) }
|
||||
.empty-state p { font-size:14px; margin-bottom:8px }
|
||||
.empty-state .icon { font-size:36px; margin-bottom:12px; opacity:.5 }
|
||||
.chat-layout { display:flex; gap:16px; height:calc(100vh - 100px); min-height:60vh; overflow:hidden }
|
||||
.chat-main { flex:2; min-width:0; min-height:0; display:flex; flex-direction:column }
|
||||
.chat-main .card { flex:1; display:flex; flex-direction:column; margin-bottom:0; min-height:0 }
|
||||
.chat-main .card h2 { flex-shrink:0 }
|
||||
.chat-messages { flex:1; overflow-y:auto; padding:12px; border:1px solid var(--border-color); border-radius:8px; background:var(--chat-bg); margin-bottom:0; display:flex; flex-direction:column; gap:4px; min-height:0 }
|
||||
.msg { display:flex; gap:8px; margin-bottom:2px; align-items:flex-start; max-width:85%; animation:msgIn .15s ease both }
|
||||
.msg-user { flex-direction:row-reverse; align-self:flex-end }
|
||||
.msg-assistant { align-self:flex-start }
|
||||
.msg-system { align-self:center; max-width:90% }
|
||||
@keyframes msgIn {
|
||||
from { opacity:0; transform:translateY(6px) }
|
||||
to { opacity:1; transform:translateY(0) }
|
||||
.btn:active { transform: scale(.97) }
|
||||
.btn-primary { background: var(--accent); color: #fff }
|
||||
.btn-primary:hover { background: var(--sakura-500) }
|
||||
.btn-danger { background: var(--error); color: #fff }
|
||||
.btn-danger:hover { background: var(--sakura-700) }
|
||||
.btn-sm { padding: 4px 10px; font-size: 11px }
|
||||
.btn-ghost { background: transparent; border: 1px solid var(--btn-ghost-border); color: var(--text-secondary) }
|
||||
.btn-ghost:hover { background: var(--btn-ghost-hover-bg); color: var(--text-primary) }
|
||||
input, textarea, select {
|
||||
background: var(--bg-input); border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm); padding: 8px 12px;
|
||||
color: var(--text-primary); font-size: 13px; width: 100%;
|
||||
margin-bottom: 10px; outline: none;
|
||||
transition: border .15s, background .2s, color .2s;
|
||||
}
|
||||
.msg-avatar { width:28px; height:28px; border-radius:6px; display:flex; align-items:center; justify-content:center; font-size:12px; flex-shrink:0 }
|
||||
.msg-avatar img { width:28px; height:28px; border-radius:50%; object-fit:cover; display:block }
|
||||
.msg-avatar svg { display:block; width:16px; height:16px }
|
||||
.msg-user .msg-avatar { background:var(--msg-user-bg); color:var(--msg-user-color) }
|
||||
.msg-assistant .msg-avatar { background:transparent }
|
||||
.msg-system .msg-avatar { background:var(--msg-system-bg); color:var(--msg-system-color) }
|
||||
.msg-content { min-width:0 }
|
||||
.msg-bubble { padding:8px 12px; border-radius:10px; font-size:13px; line-height:1.5; word-break:break-word; position:relative }
|
||||
.msg-source { font-size:11px; opacity:.55; margin-bottom:4px; color:inherit }
|
||||
.msg-user .msg-bubble { background:var(--msg-user-bg); color:var(--msg-user-color); border-bottom-right-radius:4px }
|
||||
.msg-assistant .msg-bubble { background:var(--msg-assistant-bg); color:var(--msg-assistant-color); border-bottom-left-radius:4px }
|
||||
.msg-system .msg-bubble { background:var(--msg-system-bg); color:var(--msg-system-color); text-align:center; font-size:12px }
|
||||
.msg-bubble .text { white-space:pre-wrap }
|
||||
.msg-bubble .text p { margin:4px 0 }
|
||||
.msg-bubble .text h1,.msg-bubble .text h2,.msg-bubble .text h3 { font-size:1em; margin:8px 0 4px }
|
||||
.msg-bubble .text ul,.msg-bubble .text ol { padding-left:20px; margin:4px 0 }
|
||||
.msg-bubble .text pre { background:var(--pre-bg); border-radius:6px; padding:8px; margin:4px 0; overflow-x:auto; font-size:11px; max-height:200px }
|
||||
.msg-bubble .text code { background:var(--pre-bg); padding:1px 4px; border-radius:3px; font-size:11px }
|
||||
.msg-bubble .text pre code { background:none; padding:0 }
|
||||
.msg-bubble .text blockquote { border-left:3px solid var(--border-color); padding-left:8px; margin:4px 0; opacity:0.8 }
|
||||
.msg-bubble .text table { border-collapse:collapse; margin:4px 0; font-size:12px; width:100% }
|
||||
.msg-bubble .text th,.msg-bubble .text td { border:1px solid var(--border-color); padding:3px 6px; text-align:left }
|
||||
.msg-bubble .text img { max-width:100%; border-radius:6px }
|
||||
.msg-bubble .reasoning { border-left:2px solid rgba(255,255,255,0.2); padding-left:8px; margin:6px 0; font-size:11px; opacity:0.7 }
|
||||
.msg-bubble .reasoning-title { cursor:pointer; font-size:10px; font-weight:600; user-select:none; margin-bottom:2px; opacity:0.6 }
|
||||
.msg-bubble .reasoning-body { line-height:1.4 }
|
||||
.msg-bubble .tool-call { background:rgba(0,0,0,0.15); border-radius:6px; padding:6px 8px; margin:4px 0; font-size:11px; border-left:2px solid var(--accent) }
|
||||
.msg-bubble .tool-call .tc-name { font-weight:600; color:var(--accent) }
|
||||
.msg-bubble .tool-call .tc-args { font-family:monospace; font-size:10px; opacity:0.7; white-space:pre-wrap; word-break:break-all; margin-top:2px }
|
||||
.msg-bubble .tool-call .tc-result { font-family:monospace; font-size:10px; opacity:0.6; white-space:pre-wrap; word-break:break-all; margin-top:2px; max-height:80px; overflow-y:auto }
|
||||
.chat-input-row { display:flex; gap:8px; flex-shrink:0; padding-top:10px }
|
||||
.chat-input-row input { flex:1; margin-bottom:0 }
|
||||
.chat-input-row button { flex-shrink:0; margin-bottom:0 }
|
||||
.chat-sidebar { flex:1; min-width:240px; max-width:340px; overflow-y:auto; display:flex; flex-direction:column; gap:12px }
|
||||
.chat-sidebar .card { margin-bottom:0 }
|
||||
#sm-container-chat { height:180px }
|
||||
.loading { display:inline-block; width:16px; height:16px; border:2px solid var(--loading-border); border-radius:50%; border-top-color:var(--loading-top); animation:spin .6s linear infinite }
|
||||
@keyframes spin { to { transform:rotate(360deg) } }
|
||||
.monaco-like { font-family:monospace; font-size:12px; background:var(--bg-input); border:1px solid var(--border-color); border-radius:6px }
|
||||
.kv-row { display:flex; padding:6px 0; border-bottom:1px solid var(--kv-border); font-size:13px }
|
||||
.kv-row .key { color:var(--text-muted); width:180px; flex-shrink:0 }
|
||||
.kv-row .val { color:var(--text-primary); word-break:break-all }
|
||||
.tool-badge { display:inline-block; padding:1px 6px; border-radius:3px; font-size:10px; background:var(--accent-bg); color:var(--accent); margin:1px }
|
||||
.check-pass { color:#86efac }
|
||||
.check-fail { color:#fca5a5 }
|
||||
.check-skip { color:var(--text-secondary) }
|
||||
.memory-graph { width:100%; height:300px; background:var(--bg-input); border-radius:8px; border:1px solid var(--border-color); position:relative; overflow:hidden; display:flex; align-items:center; justify-content:center; color:var(--text-muted); font-size:13px }
|
||||
.health-panel { display:grid; gap:8px }
|
||||
.health-item { display:flex; align-items:center; gap:10px; padding:8px 12px; background:var(--bg-input); border-radius:6px; font-size:13px }
|
||||
.health-item .check-name { flex:1 }
|
||||
.health-item .check-status { font-size:11px; font-weight:500 }
|
||||
.fade-in { animation:fadeIn .2s ease }
|
||||
@keyframes fadeIn { from { opacity:0; transform:translateY(4px) } to { opacity:1; transform:translateY(0) } }
|
||||
#starmap-container { width:100%; height:calc(100vh - 88px); position:relative; overflow:hidden; border-radius:10px; border:1px solid var(--border-color); background:var(--bg-input) }
|
||||
#starmap-container canvas { display:block }
|
||||
#starmap-stats { position:absolute; top:16px; left:16px; background:rgba(10,10,26,0.85); padding:12px 16px; border-radius:8px; border:1px solid rgba(100,100,255,0.3); font-size:13px; z-index:10; backdrop-filter:blur(10px); color:#ccc }
|
||||
#starmap-stats h3 { margin-bottom:6px; color:#4488ff; font-size:14px }
|
||||
#starmap-stats p { margin:2px 0; color:#888; font-size:12px }
|
||||
#starmap-stats span { color:#fff; font-weight:700 }
|
||||
#starmap-info { position:absolute; top:16px; right:16px; background:rgba(10,10,26,0.9); padding:12px 16px; border-radius:8px; border:1px solid rgba(100,100,255,0.3); font-size:13px; z-index:10; display:none; backdrop-filter:blur(10px); color:#ccc; max-width:260px }
|
||||
#starmap-info h3 { color:#44ff88; margin-bottom:6px; font-size:14px }
|
||||
#starmap-info p { margin:2px 0; color:#888; font-size:12px }
|
||||
#starmap-info .label { color:#666 }
|
||||
#starmap-loading { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); font-size:18px; color:#4488ff; z-index:20 }
|
||||
.starmap-toggle { position:absolute; bottom:16px; left:50%; transform:translateX(-50%); display:flex; gap:8px; z-index:10 }
|
||||
.starmap-toggle button { background:rgba(10,10,26,0.85); border:1px solid rgba(100,100,255,0.3); color:#aaa; padding:6px 14px; border-radius:6px; cursor:pointer; font-size:12px; font-family:inherit; backdrop-filter:blur(10px); transition:all .2s }
|
||||
.starmap-toggle button:hover { background:rgba(68,136,255,0.2); color:#fff; border-color:rgba(68,136,255,0.6) }
|
||||
.starmap-toggle button.on { background:rgba(68,136,255,0.3); color:#4488ff; border-color:#4488ff }
|
||||
#sm-container-chat { height:260px; background:var(--bg-input); border-radius:6px; border:1px solid var(--border-color); overflow:hidden; position:relative }
|
||||
#sm-container-chat canvas { display:block }
|
||||
input:focus, textarea:focus, select:focus { border-color: var(--accent) }
|
||||
textarea { resize: vertical; min-height: 80px; font-family: var(--font-mono); font-size: 12px }
|
||||
label { display: block; font-size: 11px; color: var(--text-secondary); margin-bottom: 3px; font-weight: 500 }
|
||||
pre { background: var(--pre-bg); border-radius: var(--radius-sm); padding: 12px; font-size: 12px; overflow-x: auto; color: var(--pre-color); font-family: var(--font-mono); max-height: 400px; overflow-y: auto }
|
||||
code { font-family: var(--font-mono); font-size: 12px; color: var(--pre-color) }
|
||||
.empty-state { text-align: center; padding: 40px 20px; color: var(--text-muted) }
|
||||
.empty-state p { font-size: 14px; margin-bottom: 8px }
|
||||
|
||||
.toggle-row { margin-top:8px; display:flex; align-items:center; gap:12px }
|
||||
.toggle-row .label-text { color:#8888aa; font-size:12px }
|
||||
.toggle-switch { position:relative; width:36px; height:20px; cursor:pointer; flex-shrink:0 }
|
||||
.toggle-track { position:absolute; inset:0; background:rgba(60,60,80,0.8); border-radius:10px; transition:all 0.3s; border:1px solid rgba(100,100,255,0.2) }
|
||||
.toggle-track.on { background:rgba(68,136,255,0.5); border-color:#4488ff }
|
||||
.toggle-knob { position:absolute; width:16px; height:16px; left:2px; top:2px; background:#6666aa; border-radius:50%; transition:all 0.3s }
|
||||
.toggle-knob.on { left:18px; background:#4488ff }
|
||||
.toggle-btn { display:flex; align-items:center; gap:4px; padding:2px 8px; border-radius:4px; border:1px solid rgba(100,100,255,0.15); background:transparent; color:#8888aa; font-size:12px; font-family:inherit; cursor:pointer; transition:all 0.2s }
|
||||
.toggle-btn:hover { background:rgba(68,136,255,0.15); color:#fff }
|
||||
.toggle-btn.on { background:rgba(68,136,255,0.3); color:#4488ff; border-color:#4488ff }
|
||||
.label-text { color:#8888aa; font-size:12px }
|
||||
.loading-spinner { width:32px; height:32px; border:3px solid rgba(68,136,255,0.15); border-top:3px solid #4488ff; border-radius:50%; animation:spin 0.8s linear infinite }
|
||||
@keyframes spin { to { transform:rotate(360deg) } }
|
||||
/* ===== Chat (对话主页) ===== */
|
||||
.chat-layout { display: flex; flex-direction: column; gap: 12px; height: 100%; min-height: 60vh }
|
||||
.chat-tabs { display: flex; gap: 4px; flex-wrap: wrap; border-bottom: 1px solid var(--border-color); padding-bottom: 10px }
|
||||
.chat-tabs span { padding: 6px 14px; font-size: 13px; cursor: pointer; color: var(--text-muted); border-radius: var(--radius-pill); border: 1px solid transparent; transition: all .15s }
|
||||
.chat-tabs span:hover { color: var(--text-primary); background: var(--bg-hover) }
|
||||
.chat-tabs span.active { color: var(--accent); background: var(--accent-bg); border-color: rgba(255,127,172,.35) }
|
||||
.chat-panel { display: none; flex: 1; min-height: 0; flex-direction: column }
|
||||
.chat-panel.active { display: flex }
|
||||
.chat-main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column }
|
||||
.chat-main .card { flex: 1; display: flex; flex-direction: column; margin-bottom: 0; min-height: 0; background: transparent; border: none; box-shadow: none; backdrop-filter: none; padding: 0 }
|
||||
.chat-main .card h2 { flex-shrink: 0 }
|
||||
.chat-messages { flex: 1; overflow-y: auto; padding: 18px 18px 26px; margin-bottom: 0; display: flex; flex-direction: column; gap: 6px; min-height: 0 }
|
||||
.msg { display: flex; gap: 8px; margin-bottom: 2px; align-items: flex-start; max-width: 100% }
|
||||
.msg-user { flex-direction: row-reverse; align-self: flex-end }
|
||||
.msg-assistant { align-self: flex-start }
|
||||
.msg-system { align-self: center; max-width: 90% }
|
||||
.msg-avatar { width: 28px; height: 28px; border-radius: 50%; overflow: hidden; display: flex; align-items: center; justify-content: center; font-size: 12px; flex-shrink: 0 }
|
||||
.msg-avatar img { width: 100%; height: 100%; object-fit: cover; display: block }
|
||||
.msg-assistant .msg-avatar img { transform: scale(1.5) }
|
||||
.msg-avatar svg { display: block; width: 16px; height: 16px }
|
||||
.msg-user .msg-avatar { background: var(--msg-user-bg); color: var(--msg-user-color) }
|
||||
.msg-assistant .msg-avatar { background: transparent }
|
||||
.msg-system .msg-avatar { background: var(--msg-system-bg); color: var(--msg-system-color) }
|
||||
.msg-content { min-width: 0; flex: 1 }
|
||||
.msg-content .msg-bubble + .msg-bubble { margin-top: 6px }
|
||||
.msg-channel { align-self: flex-start }
|
||||
.msg-channel .msg-avatar.chan-avatar { color: #ffffff; font-weight: 700; font-size: 13px; text-transform: uppercase }
|
||||
.msg-channel .msg-chan-name { font-size: 10px; color: var(--text-muted); opacity: .8; margin-bottom: 2px; padding-left: 4px; letter-spacing: .5px }
|
||||
.msg-channel .msg-bubble { background: var(--msg-bubble-bg); color: var(--msg-bubble-color); border-bottom-left-radius: 4px; border: 1px solid var(--msg-bubble-border); box-shadow: 0 2px 8px rgba(0,0,0,.28) }
|
||||
.msg-bubble { padding: 9px 12px; border-radius: var(--radius-md); font-size: 15px; line-height: 1.62; word-break: break-word; position: relative }
|
||||
.msg-source { font-size: 11px; opacity: .55; margin-bottom: 4px; color: inherit }
|
||||
.msg-user .msg-bubble { background: var(--msg-bubble-bg); color: var(--msg-bubble-color); border-bottom-right-radius: 4px; border: 1px solid var(--msg-bubble-border) }
|
||||
.msg-assistant .msg-bubble { background: var(--msg-bubble-bg); color: var(--msg-bubble-color); border-bottom-left-radius: 4px; border: 1px solid var(--msg-bubble-border); box-shadow: 0 2px 10px rgba(0,0,0,.3) }
|
||||
.msg-system .msg-bubble { background: var(--msg-system-bg); color: var(--msg-system-color); text-align: center; font-size: 12px; border: 1px solid var(--msg-bubble-border) }
|
||||
.msg-bubble .text { white-space: pre-wrap }
|
||||
.msg-bubble .text p { margin: 4px 0 }
|
||||
.msg-bubble .text h1, .msg-bubble .text h2, .msg-bubble .text h3 { font-size: 1em; margin: 8px 0 4px }
|
||||
.msg-bubble .text ul, .msg-bubble .text ol { padding-left: 20px; margin: 4px 0 }
|
||||
.msg-bubble .text pre { background: var(--pre-bg); color: var(--pre-color); border-radius: var(--radius-sm); padding: 8px; margin: 4px 0; overflow-x: auto; font-size: 12px; line-height: 1.5; border: 1px solid var(--border-color); max-height: 200px }
|
||||
.msg-bubble .text code { background: var(--pre-bg); color: var(--pre-color); padding: 1px 4px; border-radius: 3px; font-size: 12px }
|
||||
.msg-bubble .text pre code { background: none; padding: 0 }
|
||||
.msg-bubble .text blockquote { border-left: 3px solid var(--border-color); padding-left: 8px; margin: 4px 0; opacity: .8 }
|
||||
.msg-bubble .text table { border-collapse: collapse; margin: 4px 0; font-size: 12px; width: 100% }
|
||||
.msg-bubble .text th, .msg-bubble .text td { border: 1px solid var(--border-color); padding: 3px 6px; text-align: left }
|
||||
.msg-bubble .text img { max-width: 100%; border-radius: var(--radius-sm) }
|
||||
.msg-bubble .reasoning { border-left: 2px solid #dddddd; padding-left: 8px; margin: 6px 0; font-size: 11px; opacity: .8 }
|
||||
.msg-bubble .reasoning-title { cursor: pointer; font-size: 10px; font-weight: 600; user-select: none; margin-bottom: 2px; opacity: .6 }
|
||||
.msg-bubble .reasoning-body { line-height: 1.4 }
|
||||
.msg-bubble .tool-call { background: var(--bg-hover); color: var(--text-secondary); border-radius: var(--radius-sm); padding: 7px 10px; margin: 6px 0; font-size: 11px; border: 1px solid var(--kv-border); border-left: 3px solid var(--accent); cursor: pointer }
|
||||
.msg-bubble .tool-call .tc-line { display: flex; align-items: center; gap: 6px; font-size: 11px; user-select: none }
|
||||
.msg-bubble .tool-call .tc-ico { display: inline-flex; flex-shrink: 0 }
|
||||
.msg-bubble .tool-call .tc-name { font-weight: 600; color: var(--text-primary) }
|
||||
.msg-bubble .tool-call .tc-state { margin-left: 6px; font-size: 9.5px; font-weight: 500; padding: 1px 6px; border-radius: 999px; flex-shrink: 0; margin-left: auto }
|
||||
.msg-bubble .tool-call .tc-run { background: rgba(63,110,245,.18); color: #a3b8ff }
|
||||
.msg-bubble .tool-call .tc-done { background: rgba(23,169,100,.18); color: #6ee7a8 }
|
||||
.msg-bubble .tool-call .tc-deny { background: rgba(219,54,148,.18); color: #ff9ec6 }
|
||||
.msg-bubble .tool-call .tc-caret { font-size: 10px; color: var(--text-muted); transition: transform .15s var(--ease-out) }
|
||||
.msg-bubble .tool-call.open .tc-caret { transform: rotate(180deg) }
|
||||
.msg-bubble .tool-call .tc-detail { margin-top: 6px }
|
||||
.msg-bubble .tool-call .tc-args { font-family: var(--font-mono); font-size: 11px; opacity: .85; white-space: pre-wrap; word-break: break-all; margin-top: 5px; background: var(--pre-bg); color: var(--pre-color); border-radius: 4px; padding: 5px 7px }
|
||||
.msg-bubble .tool-call .tc-result { font-family: var(--font-mono); font-size: 11px; opacity: .72; white-space: pre-wrap; word-break: break-all; margin-top: 5px; background: var(--pre-bg); color: var(--pre-color); border-radius: 4px; padding: 5px 7px; max-height: 80px; overflow-y: auto }
|
||||
[data-theme=light] .msg-bubble .tool-call .tc-run { color: #3f6ef5 }
|
||||
[data-theme=light] .msg-bubble .tool-call .tc-done { color: #128a52 }
|
||||
[data-theme=light] .msg-bubble .tool-call .tc-deny { color: #c2185b }
|
||||
.msg-bubble .thinking-tools { display: flex; gap: 6px; align-items: center; flex-wrap: wrap }
|
||||
.msg-bubble .thinking-tool { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; color: var(--text-secondary); background: var(--pre-bg); border-radius: 999px; padding: 3px 10px }
|
||||
.msg-bubble .thinking-tool.pill-in { animation: pillIn .25s var(--ease-out) both }
|
||||
@keyframes pillIn { from { opacity: 0; transform: translateX(8px) } to { opacity: 1; transform: none } }
|
||||
.msg-bubble .live-spinner { width: 15px; height: 15px; border-radius: 50%; border: 2px solid var(--msg-user-bg); border-top-color: var(--accent); animation: spin .7s linear infinite; flex-shrink: 0; display: inline-block; vertical-align: middle }
|
||||
.msg-bubble .live-spinner + .thinking-tools,
|
||||
.msg-bubble .live-spinner + .text { margin-left: 8px }
|
||||
.msg-bubble.grow-in { animation: bubbleGrow .5s var(--ease-out) both }
|
||||
.msg-bubble.grow-in .text { animation: textFadeIn .35s ease .125s both }
|
||||
@keyframes bubbleGrow { from { max-height: 28px; opacity: .4 } to { max-height: 3000px; opacity: 1 } }
|
||||
@keyframes textFadeIn { from { opacity: 0 } to { opacity: 1 } }
|
||||
.tool-call.tool-drip-in { animation: toolDripIn .3s var(--ease-out) both }
|
||||
@keyframes toolDripIn { from { opacity: 0; transform: translateY(-14px) scale(.98) } to { opacity: 1; transform: none } }
|
||||
.chat-input-row { display: flex; gap: 8px; flex-shrink: 0; padding-top: 10px }
|
||||
.chat-input-row input { flex: 1; margin-bottom: 0 }
|
||||
.chat-input-row button { flex-shrink: 0; margin-bottom: 0 }
|
||||
#sm-container-chat { height: 480px; background: var(--bg-input); border-radius: var(--radius-md); border: 1px solid var(--glass-border); overflow: hidden; position: relative }
|
||||
#sm-container-chat canvas { display: block }
|
||||
.loading { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--loading-border); border-radius: 50%; border-top-color: var(--loading-top); animation: spin .6s linear infinite }
|
||||
.loading-spinner { width: 32px; height: 32px; border: 3px solid rgba(255,127,172,.15); border-top: 3px solid var(--accent); border-radius: 50%; animation: spin .8s linear infinite }
|
||||
@keyframes spin { to { transform: rotate(360deg) } }
|
||||
|
||||
.sidebar-subnav { display:flex; gap:0; border-bottom:1px solid var(--border-color); margin-bottom:10px }
|
||||
.sidebar-subnav span { padding:6px 12px; font-size:12px; cursor:pointer; color:var(--text-muted); border-bottom:2px solid transparent; transition:all .15s }
|
||||
.sidebar-subnav span:hover { color:var(--text-primary) }
|
||||
.sidebar-subnav span.active { color:var(--accent); border-bottom-color:var(--accent) }
|
||||
@media(max-width:900px) {
|
||||
.chat-layout { flex-direction:column; height:auto }
|
||||
.chat-sidebar { max-width:none }
|
||||
.msg { max-width:95% }
|
||||
/* ===== 未配置后端引导 ===== */
|
||||
.setup-card {
|
||||
text-align: center; padding: 48px 32px;
|
||||
display: flex; flex-direction: column; align-items: center; gap: 16px;
|
||||
}
|
||||
.setup-card .setup-icon { width: 64px; height: 64px; opacity: .8 }
|
||||
.setup-card h2 { font-size: 18px; margin-bottom: 0 }
|
||||
.setup-card p { color: var(--text-muted); font-size: 13px; max-width: 420px; line-height: 1.6 }
|
||||
.setup-card .btn { padding: 8px 20px; font-size: 13px }
|
||||
|
||||
/* ===== Settings ===== */
|
||||
.settings-tabs { display: flex; gap: 4px; flex-wrap: wrap; border-bottom: 1px solid var(--border-color); padding-bottom: 10px; margin-bottom: 16px }
|
||||
.settings-tabs span { padding: 6px 14px; font-size: 13px; cursor: pointer; color: var(--text-muted); border-radius: var(--radius-pill); border: 1px solid transparent; transition: all .15s }
|
||||
.settings-tabs span:hover { color: var(--text-primary); background: var(--bg-hover) }
|
||||
.settings-tabs span.active { color: var(--accent); background: var(--accent-bg); border-color: rgba(255,127,172,.35) }
|
||||
.settings-content { min-width: 0 }
|
||||
.settings-key { font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); margin-bottom: 2px }
|
||||
.kv-row { display: flex; padding: 6px 0; border-bottom: 1px solid var(--kv-border); font-size: 13px }
|
||||
.kv-row .key { color: var(--text-muted); width: 180px; flex-shrink: 0 }
|
||||
.kv-row .val { color: var(--text-primary); word-break: break-all }
|
||||
.tool-badge { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; background: var(--accent-bg); color: var(--accent); margin: 1px }
|
||||
.check-pass { color: #6ee7a8 }
|
||||
.check-fail { color: #ff9ec6 }
|
||||
.check-skip { color: var(--text-secondary) }
|
||||
.health-item { display: flex; align-items: center; gap: 10px; padding: 8px 12px; background: var(--bg-input); border-radius: var(--radius-sm); font-size: 13px }
|
||||
.health-item .check-name { flex: 1 }
|
||||
.health-item .check-status { font-size: 11px; font-weight: 500 }
|
||||
|
||||
/* ===== 连接管理 (设置页内嵌) ===== */
|
||||
.conn-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border: 1px solid var(--glass-border); border-radius: var(--radius-md); margin-bottom: 8px; cursor: pointer; transition: all .12s }
|
||||
.conn-item:hover { background: var(--bg-hover); border-color: var(--accent) }
|
||||
.conn-item.active { border-color: var(--accent); background: var(--accent-bg) }
|
||||
.conn-item .conn-info { flex: 1; min-width: 0 }
|
||||
.conn-item .conn-name { font-size: 14px; font-weight: 600; color: var(--text-primary) }
|
||||
.conn-item .conn-url { font-size: 11px; color: var(--text-muted); margin-top: 2px }
|
||||
.conn-item .conn-actions { display: flex; gap: 4px; flex-shrink: 0 }
|
||||
.conn-form h3 { font-size: 15px; margin-bottom: 12px }
|
||||
.conn-form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px }
|
||||
|
||||
/* ===== Toast ===== */
|
||||
.toast {
|
||||
position: fixed; bottom: 20px; right: 20px;
|
||||
background: var(--toast-bg); color: var(--toast-color);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 10px 20px; border-radius: var(--radius-md); font-size: 13px;
|
||||
display: none; z-index: 300;
|
||||
border: 1px solid rgba(23,169,100,.3);
|
||||
box-shadow: var(--shadow-md);
|
||||
animation: toastIn .15s var(--ease-out);
|
||||
}
|
||||
.toast.error { background: var(--toast-error-bg); color: var(--toast-error-color); border-color: rgba(219,54,148,.3) }
|
||||
@keyframes toastIn { from { opacity: 0; transform: translateX(30px) } to { opacity: 1; transform: translateX(0) } }
|
||||
|
||||
/* ===== Starmap ===== */
|
||||
#starmap-stats, #starmap-info {
|
||||
position: absolute; top: 16px;
|
||||
background: rgba(10,10,26,.85); padding: 12px 16px; border-radius: var(--radius-md);
|
||||
border: 1px solid rgba(100,100,255,.3); font-size: 13px; z-index: 10;
|
||||
backdrop-filter: blur(10px); color: #ccc;
|
||||
}
|
||||
#starmap-stats { left: 16px }
|
||||
#starmap-info { right: 16px; display: none; max-width: 260px }
|
||||
#starmap-stats h3, #starmap-info h3 { margin-bottom: 6px; font-size: 14px }
|
||||
#starmap-stats h3 { color: #4488ff }
|
||||
#starmap-info h3 { color: #44ff88 }
|
||||
#starmap-stats p, #starmap-info p { margin: 2px 0; color: #888; font-size: 12px }
|
||||
#starmap-stats span { color: #fff; font-weight: 700 }
|
||||
#starmap-info .label { color: #666 }
|
||||
#starmap-loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 18px; color: #4488ff; z-index: 20 }
|
||||
.starmap-toggle { position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; z-index: 10 }
|
||||
.starmap-toggle button { background: rgba(10,10,26,.85); border: 1px solid rgba(100,100,255,.3); color: #aaa; padding: 6px 14px; border-radius: var(--radius-sm); cursor: pointer; font-size: 12px; font-family: inherit; backdrop-filter: blur(10px); transition: all .2s }
|
||||
.starmap-toggle button:hover { background: rgba(68,136,255,.2); color: #fff; border-color: rgba(68,136,255,.6) }
|
||||
.starmap-toggle button.on { background: rgba(68,136,255,.3); color: #4488ff; border-color: #4488ff }
|
||||
.fade-in { animation: fadeIn .2s var(--ease-out) }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px) } to { opacity: 1; transform: translateY(0) } }
|
||||
|
||||
@media(max-width:768px) {
|
||||
nav { padding:0 6px; gap:2px; overflow-x:auto; scrollbar-width:none; -ms-overflow-style:none; flex-wrap:nowrap }
|
||||
nav::-webkit-scrollbar { display:none }
|
||||
nav h1 { display:none }
|
||||
nav a { padding:10px 8px; font-size:12px; white-space:nowrap; flex-shrink:0 }
|
||||
nav > div { flex-shrink:0 }
|
||||
#conn-name-display { display:none }
|
||||
.conn-indicator { padding:4px 6px }
|
||||
.container { padding:12px }
|
||||
.card { padding:12px }
|
||||
.grid-2,.grid-3,.grid-4 { grid-template-columns:1fr }
|
||||
.stat-value { font-size:20px }
|
||||
.settings-layout { flex-direction:column }
|
||||
.settings-sidebar { width:100%; max-height:200px; display:flex; flex-wrap:wrap; padding:4px; overflow-x:auto }
|
||||
.settings-sidebar a { display:inline-block; padding:6px 12px; border-left:none; border-bottom:2px solid transparent; white-space:nowrap }
|
||||
.settings-sidebar a.active { border-left:none; border-bottom-color:var(--accent) }
|
||||
.kv-row { flex-direction:column; gap:2px }
|
||||
.kv-row .key { width:auto }
|
||||
#starmap-stats { top:8px; left:8px; padding:8px 10px; font-size:11px }
|
||||
#starmap-info { top:8px; right:8px; padding:8px 10px; max-width:180px; font-size:11px }
|
||||
#starmap-container { height:calc(100vh - 48px) }
|
||||
.chat-messages { max-height:300px }
|
||||
.msg-avatar { width:24px; height:24px; font-size:11px }
|
||||
.health-item { flex-wrap:wrap; gap:4px }
|
||||
.health-item .check-name { flex:auto; width:100% }
|
||||
.rail { width: 48px }
|
||||
.container { padding: 12px }
|
||||
.card { padding: 12px }
|
||||
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr }
|
||||
.stat-value { font-size: 20px }
|
||||
.kv-row { flex-direction: column; gap: 2px }
|
||||
.kv-row .key { width: auto }
|
||||
.msg { max-width: 100% }
|
||||
.term-screen { background: #0f1115; border: 1px solid #2a2e35; border-radius: 8px; margin: 6px 0 4px 0; overflow: hidden; font-size: 11px; line-height: 1.45 }
|
||||
.term-screen .term-head { display: flex; align-items: center; gap: 6px; padding: 4px 8px; background: #1a1d23; color: #9aa0a8; font-size: 10px; border-bottom: 1px solid #2a2e35; font-family: var(--font-mono) }
|
||||
.term-screen .term-head .term-dot { width: 8px; height: 8px; border-radius: 50%; background: #3fb950; flex-shrink: 0 }
|
||||
.term-screen .term-head .term-dot.stopped { background: #d29922 }
|
||||
.term-screen pre.term-buf { margin: 0; padding: 6px 8px; background: transparent; color: #d8dee9; font-family: var(--font-mono); font-size: 11px; white-space: pre-wrap; word-break: break-all; max-height: 260px; overflow-y: auto; scrollbar-width: thin }
|
||||
.term-screen pre.term-buf::-webkit-scrollbar { width: 6px }
|
||||
.term-screen pre.term-buf::-webkit-scrollbar-thumb { background: #33373d; border-radius: 3px }
|
||||
.health-item { flex-wrap: wrap; gap: 4px }
|
||||
}
|
||||
@media(max-width:480px) {
|
||||
.chat-layout { gap:10px }
|
||||
.chat-sidebar { min-width:0 }
|
||||
.msg { max-width:98% }
|
||||
.settings-sidebar a { padding:6px 10px; font-size:12px }
|
||||
}
|
||||
|
||||
/* Connection Manager */
|
||||
.overlay { position:fixed; inset:0; background:rgba(0,0,0,0.6); display:none; align-items:center; justify-content:center; z-index:1000 }
|
||||
.overlay-content { background:var(--bg-card); border:1px solid var(--border-color); border-radius:12px; padding:28px; width:520px; max-height:80vh; overflow-y:auto; animation:overlayIn .15s ease both }
|
||||
.overlay-content h2 { font-size:18px; margin-bottom:16px; color:var(--text-primary) }
|
||||
@keyframes overlayIn {
|
||||
from { opacity:0; transform:translateY(12px) }
|
||||
to { opacity:1; transform:translateY(0) }
|
||||
}
|
||||
.conn-item { display:flex; align-items:center; gap:12px; padding:12px 16px; border:1px solid var(--border-color); border-radius:8px; margin-bottom:8px; cursor:pointer; transition:border-color .12s,background .12s }
|
||||
.conn-item:hover { background:var(--bg-hover); border-color:var(--accent) }
|
||||
.conn-item.active { border-color:var(--accent); background:var(--accent-bg) }
|
||||
.conn-item .conn-info { flex:1; min-width:0 }
|
||||
.conn-item .conn-name { font-size:14px; font-weight:600; color:var(--text-primary) }
|
||||
.conn-item .conn-url { font-size:11px; color:var(--text-muted); margin-top:2px }
|
||||
.conn-item .conn-actions { display:flex; gap:4px; flex-shrink:0 }
|
||||
.conn-form h3 { font-size:15px; margin-bottom:12px }
|
||||
.conn-form-actions { display:flex; gap:8px; justify-content:flex-end; margin-top:12px }
|
||||
.conn-indicator { display:flex; align-items:center; gap:4px; cursor:pointer; padding:4px 10px; border-radius:6px; font-size:12px; color:var(--text-secondary); transition:all .15s; user-select:none }
|
||||
.conn-indicator:hover { background:var(--bg-hover); color:var(--text-primary) }
|
||||
|
||||
Reference in New Issue
Block a user