|
|
|
|
@ -1,909 +1,14 @@
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>AgentKit</title>
|
|
|
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🤖</text></svg>">
|
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
|
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
|
|
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet">
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
|
|
|
|
|
<style>
|
|
|
|
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
|
|
|
:root{
|
|
|
|
|
--bg:#f8f7f4;
|
|
|
|
|
--surface:#ffffff;
|
|
|
|
|
--surface2:#f1f0ec;
|
|
|
|
|
--surface3:#e8e7e3;
|
|
|
|
|
--border:#e2e0db;
|
|
|
|
|
--border-light:#eceae6;
|
|
|
|
|
--text:#1a1a1a;
|
|
|
|
|
--text2:#737068;
|
|
|
|
|
--text3:#a09d95;
|
|
|
|
|
--primary:#3b5bdb;
|
|
|
|
|
--primary-hover:#2c4ac6;
|
|
|
|
|
--primary-light:#eef1fd;
|
|
|
|
|
--primary-subtle:#d4daf9;
|
|
|
|
|
--user-bg:#3b5bdb;
|
|
|
|
|
--user-text:#ffffff;
|
|
|
|
|
--agent-bg:#f1f0ec;
|
|
|
|
|
--agent-text:#1a1a1a;
|
|
|
|
|
--danger:#dc2626;
|
|
|
|
|
--danger-light:#fef2f2;
|
|
|
|
|
--success:#16a34a;
|
|
|
|
|
--success-light:#f0fdf4;
|
|
|
|
|
--warning:#d97706;
|
|
|
|
|
--radius-sm:8px;
|
|
|
|
|
--radius:12px;
|
|
|
|
|
--radius-lg:16px;
|
|
|
|
|
--radius-xl:20px;
|
|
|
|
|
--shadow-xs:0 1px 2px rgba(0,0,0,.04);
|
|
|
|
|
--shadow-sm:0 1px 3px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);
|
|
|
|
|
--shadow-md:0 4px 12px rgba(0,0,0,.07),0 1px 3px rgba(0,0,0,.05);
|
|
|
|
|
--shadow-lg:0 8px 24px rgba(0,0,0,.09),0 2px 6px rgba(0,0,0,.05);
|
|
|
|
|
--sidebar-w:280px;
|
|
|
|
|
--right-w:340px;
|
|
|
|
|
--font:'Plus Jakarta Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
|
|
|
|
}
|
|
|
|
|
html,body{height:100%;font-family:var(--font);background:var(--bg);color:var(--text);overflow:hidden;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
|
|
|
|
|
.app{display:flex;height:100vh}
|
|
|
|
|
|
|
|
|
|
/* ── Left Sidebar ────────────────────────────────────────────── */
|
|
|
|
|
.sidebar{width:var(--sidebar-w);background:var(--surface);border-right:1px solid var(--border-light);display:flex;flex-direction:column;flex-shrink:0}
|
|
|
|
|
.sidebar-header{padding:20px 16px 16px;display:flex;align-items:center;justify-content:space-between}
|
|
|
|
|
.sidebar-brand{display:flex;align-items:center;gap:10px}
|
|
|
|
|
.sidebar-logo{width:32px;height:32px;background:var(--primary);border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;color:#fff;font-size:16px;font-weight:700}
|
|
|
|
|
.sidebar-header h1{font-size:17px;font-weight:700;letter-spacing:-0.4px;color:var(--text)}
|
|
|
|
|
.btn-new{background:var(--primary-light);color:var(--primary);border:none;border-radius:var(--radius-sm);padding:7px 14px;font-size:13px;font-weight:600;cursor:pointer;transition:all .2s;font-family:var(--font)}
|
|
|
|
|
.btn-new:hover{background:var(--primary-subtle);transform:translateY(-1px)}
|
|
|
|
|
|
|
|
|
|
.session-list{flex:1;overflow-y:auto;padding:8px 8px 16px}
|
|
|
|
|
.session-item{padding:10px 12px;border-radius:var(--radius-sm);cursor:pointer;transition:all .15s;margin-bottom:2px;display:flex;align-items:center;justify-content:space-between;border:1px solid transparent}
|
|
|
|
|
.session-item:hover{background:var(--surface2);border-color:var(--border-light)}
|
|
|
|
|
.session-item.active{background:var(--primary-light);border-color:var(--primary-subtle);color:var(--primary)}
|
|
|
|
|
.session-item.active .title{font-weight:600}
|
|
|
|
|
.session-item .title{font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;font-weight:450}
|
|
|
|
|
.session-item .time{font-size:11px;color:var(--text3);margin-left:8px;flex-shrink:0}
|
|
|
|
|
.session-item .del{opacity:0;color:var(--danger);cursor:pointer;margin-left:6px;font-size:16px;flex-shrink:0;transition:opacity .15s;width:20px;height:20px;display:flex;align-items:center;justify-content:center;border-radius:4px}
|
|
|
|
|
.session-item:hover .del{opacity:.6}
|
|
|
|
|
.session-item .del:hover{opacity:1;background:var(--danger-light)}
|
|
|
|
|
.empty-state{color:var(--text3);font-size:13px;text-align:center;padding:40px 16px;line-height:1.7}
|
|
|
|
|
|
|
|
|
|
/* ── Main Chat Area ──────────────────────────────────────────── */
|
|
|
|
|
.chat-area{flex:1;display:flex;flex-direction:column;min-width:0;background:var(--bg)}
|
|
|
|
|
.chat-header{padding:12px 24px;border-bottom:1px solid var(--border-light);display:flex;align-items:center;gap:12px;background:var(--surface);box-shadow:var(--shadow-xs)}
|
|
|
|
|
.chat-header .agent-name{font-size:15px;font-weight:600;flex:1;letter-spacing:-0.2px}
|
|
|
|
|
.chat-header .status{font-size:12px;color:var(--text3);display:flex;align-items:center;gap:5px}
|
|
|
|
|
.chat-header .status::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--text3);flex-shrink:0}
|
|
|
|
|
.chat-header .status.connected{color:var(--success)}
|
|
|
|
|
.chat-header .status.connected::before{background:var(--success)}
|
|
|
|
|
.btn-icon{background:var(--surface);border:1px solid var(--border);color:var(--text2);border-radius:var(--radius-sm);width:36px;height:36px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .2s;font-size:16px}
|
|
|
|
|
.btn-icon:hover{background:var(--surface2);color:var(--text);border-color:var(--primary);box-shadow:var(--shadow-xs)}
|
|
|
|
|
|
|
|
|
|
/* ── Messages ────────────────────────────────────────────────── */
|
|
|
|
|
.messages{flex:1;overflow-y:auto;padding:24px 24px 16px;display:flex;flex-direction:column;gap:20px;scroll-behavior:smooth}
|
|
|
|
|
.msg{display:flex;flex-direction:column;max-width:72%;animation:msgIn .35s cubic-bezier(.16,1,.3,1)}
|
|
|
|
|
.msg.user{align-self:flex-end}
|
|
|
|
|
.msg.agent{align-self:flex-start}
|
|
|
|
|
.msg .bubble{padding:12px 18px;font-size:14px;line-height:1.7;white-space:pre-wrap;word-break:break-word;position:relative}
|
|
|
|
|
.msg.user .bubble{background:var(--user-bg);color:var(--user-text);border-radius:var(--radius-lg) var(--radius-lg) var(--radius-sm) var(--radius-lg);box-shadow:0 2px 8px rgba(59,91,219,.2)}
|
|
|
|
|
.msg.agent .bubble{background:var(--surface);color:var(--agent-text);border-radius:var(--radius-lg) var(--radius-lg) var(--radius-lg) var(--radius-sm);border:1px solid var(--border-light);box-shadow:var(--shadow-xs)}
|
|
|
|
|
|
|
|
|
|
/* ── Markdown in Agent Bubbles ──────────────────────────────── */
|
|
|
|
|
.msg.agent .bubble.md{white-space:normal}
|
|
|
|
|
.msg.agent .bubble.md p{margin:0 0 .6em}
|
|
|
|
|
.msg.agent .bubble.md p:last-child{margin-bottom:0}
|
|
|
|
|
.msg.agent .bubble.md h1,.msg.agent .bubble.md h2,.msg.agent .bubble.md h3,.msg.agent .bubble.md h4{margin:.8em 0 .4em;font-weight:700;letter-spacing:-.3px;line-height:1.3}
|
|
|
|
|
.msg.agent .bubble.md h1{font-size:1.4em}
|
|
|
|
|
.msg.agent .bubble.md h2{font-size:1.2em}
|
|
|
|
|
.msg.agent .bubble.md h3{font-size:1.05em}
|
|
|
|
|
.msg.agent .bubble.md h4{font-size:1em}
|
|
|
|
|
.msg.agent .bubble.md ul,.msg.agent .bubble.md ol{margin:.4em 0 .6em 1.5em;padding:0}
|
|
|
|
|
.msg.agent .bubble.md li{margin:.2em 0}
|
|
|
|
|
.msg.agent .bubble.md li>p{margin:.2em 0}
|
|
|
|
|
.msg.agent .bubble.md blockquote{margin:.5em 0;padding:.4em 1em;border-left:3px solid var(--primary);background:var(--primary-light);border-radius:0 var(--radius-sm) var(--radius-sm) 0;color:var(--text2)}
|
|
|
|
|
.msg.agent .bubble.md blockquote p{margin:.2em 0}
|
|
|
|
|
.msg.agent .bubble.md code{font-family:'SF Mono',SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace;font-size:.88em;background:var(--surface2);padding:.15em .4em;border-radius:4px;color:var(--text)}
|
|
|
|
|
.msg.agent .bubble.md pre{margin:.5em 0;padding:12px 14px;background:var(--surface2);border:1px solid var(--border-light);border-radius:var(--radius-sm);overflow-x:auto;line-height:1.5}
|
|
|
|
|
.msg.agent .bubble.md pre code{background:none;padding:0;font-size:.85em;color:var(--text)}
|
|
|
|
|
.msg.agent .bubble.md table{border-collapse:collapse;margin:.5em 0;width:100%;font-size:.9em}
|
|
|
|
|
.msg.agent .bubble.md th,.msg.agent .bubble.md td{border:1px solid var(--border);padding:6px 10px;text-align:left}
|
|
|
|
|
.msg.agent .bubble.md th{background:var(--surface2);font-weight:600}
|
|
|
|
|
.msg.agent .bubble.md hr{border:none;border-top:1px solid var(--border);margin:.8em 0}
|
|
|
|
|
.msg.agent .bubble.md a{color:var(--primary);text-decoration:none}
|
|
|
|
|
.msg.agent .bubble.md a:hover{text-decoration:underline}
|
|
|
|
|
.msg.agent .bubble.md img{max-width:100%;border-radius:var(--radius-sm)}
|
|
|
|
|
.msg.agent .bubble.md strong{font-weight:700}
|
|
|
|
|
.msg.agent .bubble.md em{font-style:italic}
|
|
|
|
|
.msg .meta{font-size:11px;color:var(--text3);margin-top:5px;padding:0 4px;font-weight:500}
|
|
|
|
|
.msg.user .meta{text-align:right}
|
|
|
|
|
.typing-indicator{display:inline-flex;gap:5px;padding:6px 0}
|
|
|
|
|
.typing-indicator span{width:7px;height:7px;background:var(--text3);border-radius:50%;animation:bounce 1.4s ease-in-out infinite}
|
|
|
|
|
.typing-indicator span:nth-child(2){animation-delay:.15s}
|
|
|
|
|
.typing-indicator span:nth-child(3){animation-delay:.3s}
|
|
|
|
|
|
|
|
|
|
/* ── Input Area ──────────────────────────────────────────────── */
|
|
|
|
|
.input-area{padding:16px 24px 20px;background:transparent}
|
|
|
|
|
.input-wrap{display:flex;gap:10px;align-items:flex-end;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:6px 6px 6px 16px;box-shadow:var(--shadow-sm);transition:all .2s}
|
|
|
|
|
.input-wrap:focus-within{border-color:var(--primary);box-shadow:0 0 0 3px rgba(59,91,219,.1),var(--shadow-md)}
|
|
|
|
|
.input-wrap textarea{flex:1;background:transparent;border:none;padding:8px 0;font-size:14px;color:var(--text);resize:none;outline:none;min-height:40px;max-height:160px;font-family:var(--font);line-height:1.5}
|
|
|
|
|
.input-wrap textarea::placeholder{color:var(--text3)}
|
|
|
|
|
.btn-send{background:var(--primary);color:#fff;border:none;border-radius:var(--radius);padding:10px 20px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s;flex-shrink:0;font-family:var(--font)}
|
|
|
|
|
.btn-send:hover{background:var(--primary-hover);transform:translateY(-1px);box-shadow:0 2px 8px rgba(59,91,219,.3)}
|
|
|
|
|
.btn-send:active{transform:translateY(0)}
|
|
|
|
|
.btn-send:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none}
|
|
|
|
|
|
|
|
|
|
/* ── Welcome ─────────────────────────────────────────────────── */
|
|
|
|
|
.welcome{flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:16px;color:var(--text2);padding:40px}
|
|
|
|
|
.welcome-icon{width:64px;height:64px;background:var(--primary-light);border-radius:var(--radius-xl);display:flex;align-items:center;justify-content:center;font-size:28px;margin-bottom:4px}
|
|
|
|
|
.welcome h2{color:var(--text);font-size:24px;font-weight:700;letter-spacing:-0.5px}
|
|
|
|
|
.welcome p{font-size:14px;max-width:380px;text-align:center;line-height:1.7;color:var(--text2)}
|
|
|
|
|
|
|
|
|
|
/* ── Right Sidebar ───────────────────────────────────────────── */
|
|
|
|
|
.right-sidebar{width:0;overflow:hidden;background:var(--surface);border-left:1px solid var(--border-light);display:flex;flex-direction:column;transition:width .3s cubic-bezier(.16,1,.3,1);flex-shrink:0}
|
|
|
|
|
.right-sidebar.open{width:var(--right-w)}
|
|
|
|
|
.right-sidebar-header{padding:16px 16px 12px;display:flex;align-items:center;justify-content:space-between}
|
|
|
|
|
.right-sidebar-header h2{font-size:15px;font-weight:700;letter-spacing:-0.2px}
|
|
|
|
|
.right-sidebar-content{flex:1;overflow-y:auto;padding:0}
|
|
|
|
|
|
|
|
|
|
/* ── Tabs ────────────────────────────────────────────────────── */
|
|
|
|
|
.tab-bar{display:flex;gap:2px;padding:0 12px;border-bottom:1px solid var(--border-light);background:var(--surface)}
|
|
|
|
|
.tab-btn{padding:10px 14px;font-size:12px;font-weight:600;color:var(--text3);background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .2s;text-align:center;letter-spacing:.2px;text-transform:uppercase}
|
|
|
|
|
.tab-btn:hover{color:var(--text2)}
|
|
|
|
|
.tab-btn.active{color:var(--primary);border-bottom-color:var(--primary)}
|
|
|
|
|
.tab-panel{display:none;padding:16px}
|
|
|
|
|
.tab-panel.active{display:block}
|
|
|
|
|
|
|
|
|
|
/* ── Skill Grid ──────────────────────────────────────────────── */
|
|
|
|
|
.skill-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
|
|
|
|
.skill-card{background:var(--surface2);border:1px solid var(--border-light);border-radius:var(--radius);padding:14px;cursor:pointer;transition:all .2s cubic-bezier(.16,1,.3,1);position:relative}
|
|
|
|
|
.skill-card:hover{border-color:var(--primary-subtle);transform:translateY(-2px);box-shadow:var(--shadow-md);background:var(--surface)}
|
|
|
|
|
.skill-card .skill-name{font-size:13px;font-weight:600;margin-bottom:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:-0.1px}
|
|
|
|
|
.skill-card .skill-desc{font-size:11px;color:var(--text2);line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
|
|
|
.skill-card .skill-tools{font-size:10px;color:var(--primary);margin-top:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500;letter-spacing:.2px}
|
|
|
|
|
.skill-card .skill-remove{position:absolute;top:8px;right:8px;width:22px;height:22px;border-radius:6px;background:var(--surface);border:1px solid var(--border);color:var(--text3);font-size:13px;cursor:pointer;display:none;align-items:center;justify-content:center;line-height:1;transition:all .15s}
|
|
|
|
|
.skill-card:hover .skill-remove{display:flex}
|
|
|
|
|
.skill-card .skill-remove:hover{background:var(--danger);color:#fff;border-color:var(--danger)}
|
|
|
|
|
|
|
|
|
|
/* ── Add Skill ───────────────────────────────────────────────── */
|
|
|
|
|
.add-skill-area{margin-top:16px;padding-top:16px;border-top:1px solid var(--border-light)}
|
|
|
|
|
.add-skill-label{font-size:12px;color:var(--text2);margin-bottom:8px;font-weight:500}
|
|
|
|
|
.add-skill-input{display:flex;gap:8px}
|
|
|
|
|
.add-skill-input input{flex:1;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:9px 12px;font-size:13px;color:var(--text);outline:none;transition:all .2s;font-family:var(--font)}
|
|
|
|
|
.add-skill-input input:focus{border-color:var(--primary);box-shadow:0 0 0 3px rgba(59,91,219,.08)}
|
|
|
|
|
.add-skill-input input::placeholder{color:var(--text3)}
|
|
|
|
|
.btn-add-skill{background:var(--primary);color:#fff;border:none;border-radius:var(--radius-sm);padding:9px 16px;font-size:13px;font-weight:600;cursor:pointer;transition:all .2s;white-space:nowrap;font-family:var(--font)}
|
|
|
|
|
.btn-add-skill:hover{background:var(--primary-hover)}
|
|
|
|
|
.btn-add-skill:disabled{opacity:.4;cursor:not-allowed}
|
|
|
|
|
.install-status{font-size:11px;margin-top:8px;color:var(--text3);font-weight:500}
|
|
|
|
|
.install-status.success{color:var(--success)}
|
|
|
|
|
.install-status.error{color:var(--danger)}
|
|
|
|
|
|
|
|
|
|
/* ── Scrollbar ───────────────────────────────────────────────── */
|
|
|
|
|
::-webkit-scrollbar{width:5px}
|
|
|
|
|
::-webkit-scrollbar-track{background:transparent}
|
|
|
|
|
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
|
|
|
|
::-webkit-scrollbar-thumb:hover{background:var(--text3)}
|
|
|
|
|
|
|
|
|
|
/* ── Animations ──────────────────────────────────────────────── */
|
|
|
|
|
@keyframes msgIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
|
|
|
|
@keyframes bounce{0%,80%,100%{transform:translateY(0)}40%{transform:translateY(-8px)}}
|
|
|
|
|
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
|
|
|
|
@keyframes slideInRight{from{opacity:0;transform:translateX(12px)}to{opacity:1;transform:translateX(0)}}
|
|
|
|
|
|
|
|
|
|
/* ── Thinking ──────────────────────────────────────────────────── */
|
|
|
|
|
.thinking-msg{display:flex;flex-direction:column;max-width:72%;animation:msgIn .35s cubic-bezier(.16,1,.3,1);align-self:flex-start}
|
|
|
|
|
.thinking-msg .bubble{padding:10px 16px;font-size:13px;line-height:1.6;color:var(--text3);font-style:italic;background:var(--surface2);border-radius:var(--radius-lg) var(--radius-lg) var(--radius-lg) var(--radius-sm);border:1px dashed var(--border)}
|
|
|
|
|
|
|
|
|
|
/* ── Loading indicator ───────────────────────────────────────── */
|
|
|
|
|
.loading-msg{display:flex;flex-direction:column;max-width:72%;align-self:flex-start;animation:msgIn .35s cubic-bezier(.16,1,.3,1)}
|
|
|
|
|
.loading-msg .loading-bubble{padding:8px 20px;background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius-lg) var(--radius-lg) var(--radius-lg) var(--radius-sm);display:inline-flex;align-items:center;gap:6px;box-shadow:var(--shadow-xs)}
|
|
|
|
|
.loading-msg .loading-dot{width:8px;height:8px;border-radius:50%;background:var(--text3);animation:loadingDot 1.4s infinite ease-in-out}
|
|
|
|
|
.loading-msg .loading-dot:nth-child(2){animation-delay:.2s}
|
|
|
|
|
.loading-msg .loading-dot:nth-child(3){animation-delay:.4s}
|
|
|
|
|
@keyframes loadingDot{0%,80%,100%{transform:scale(.4);opacity:.3}40%{transform:scale(1);opacity:1}}
|
|
|
|
|
|
|
|
|
|
/* ── Confirmation Card ──────────────────────────────────────── */
|
|
|
|
|
.confirm-card{display:flex;flex-direction:column;max-width:72%;align-self:flex-start;animation:msgIn .35s cubic-bezier(.16,1,.3,1)}
|
|
|
|
|
.confirm-card .card-inner{background:var(--surface);border:1px solid var(--warning);border-radius:var(--radius-lg);box-shadow:var(--shadow-md);overflow:hidden}
|
|
|
|
|
.confirm-card .card-header{display:flex;align-items:center;gap:10px;padding:14px 18px 10px;border-bottom:1px solid var(--border-light)}
|
|
|
|
|
.confirm-card .card-icon{width:32px;height:32px;border-radius:var(--radius-sm);background:#fef3c7;display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}
|
|
|
|
|
.confirm-card .card-title{font-size:14px;font-weight:600;color:var(--text);letter-spacing:-0.2px}
|
|
|
|
|
.confirm-card .card-body{padding:12px 18px 14px}
|
|
|
|
|
.confirm-card .card-command{background:var(--surface2);border:1px solid var(--border-light);border-radius:var(--radius-sm);padding:10px 14px;font-family:'SF Mono',SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace;font-size:13px;color:var(--text);word-break:break-all;line-height:1.6;margin-bottom:10px}
|
|
|
|
|
.confirm-card .card-reason{font-size:13px;color:var(--text2);line-height:1.5;margin-bottom:4px}
|
|
|
|
|
.confirm-card .card-actions{display:flex;gap:8px;padding:0 18px 14px}
|
|
|
|
|
.confirm-card .btn-confirm{flex:1;padding:9px 16px;border-radius:var(--radius-sm);font-size:13px;font-weight:600;cursor:pointer;transition:all .2s;border:1px solid;font-family:var(--font)}
|
|
|
|
|
.confirm-card .btn-confirm.approve{background:var(--primary-light);color:var(--primary);border-color:var(--primary-subtle)}
|
|
|
|
|
.confirm-card .btn-confirm.approve:hover{background:var(--primary);color:#fff;border-color:var(--primary)}
|
|
|
|
|
.confirm-card .btn-confirm.deny{background:var(--danger-light);color:var(--danger);border-color:#fecaca}
|
|
|
|
|
.confirm-card .btn-confirm.deny:hover{background:var(--danger);color:#fff;border-color:var(--danger)}
|
|
|
|
|
.confirm-card .btn-confirm:disabled{opacity:.5;cursor:not-allowed;transform:none}
|
|
|
|
|
.confirm-card .card-status{padding:10px 18px 14px;font-size:13px;font-weight:500;display:flex;align-items:center;gap:6px}
|
|
|
|
|
.confirm-card .card-status.approved{color:var(--success);background:var(--success-light)}
|
|
|
|
|
.confirm-card .card-status.denied{color:var(--danger);background:var(--danger-light)}
|
|
|
|
|
|
|
|
|
|
/* ── Mobile ──────────────────────────────────────────────────── */
|
|
|
|
|
@media(max-width:768px){
|
|
|
|
|
.sidebar{position:fixed;left:-100%;z-index:10;transition:left .3s cubic-bezier(.16,1,.3,1);width:85vw;max-width:320px;box-shadow:var(--shadow-lg)}
|
|
|
|
|
.sidebar.open{left:0}
|
|
|
|
|
.sidebar-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.3);z-index:9;backdrop-filter:blur(2px)}
|
|
|
|
|
.sidebar-overlay.show{display:block}
|
|
|
|
|
.mobile-toggle{display:flex!important}
|
|
|
|
|
.right-sidebar.open{position:fixed;right:0;z-index:10;width:85vw;max-width:360px;box-shadow:var(--shadow-lg)}
|
|
|
|
|
.messages{padding:16px}
|
|
|
|
|
.input-area{padding:12px 16px 16px}
|
|
|
|
|
.msg{max-width:88%}
|
|
|
|
|
}
|
|
|
|
|
.mobile-toggle{display:none;align-items:center;justify-content:center;background:none;border:none;color:var(--text);font-size:20px;cursor:pointer;padding:4px}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div class="app">
|
|
|
|
|
<!-- Left Sidebar -->
|
|
|
|
|
<div class="sidebar-overlay" id="overlay" onclick="toggleSidebar()"></div>
|
|
|
|
|
<aside class="sidebar" id="sidebar">
|
|
|
|
|
<div class="sidebar-header">
|
|
|
|
|
<div class="sidebar-brand">
|
|
|
|
|
<div class="sidebar-logo">A</div>
|
|
|
|
|
<h1>AgentKit</h1>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn-new" onclick="createSession()" title="新建对话">+ 新对话</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="session-list" id="sessionList"></div>
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
<!-- Chat -->
|
|
|
|
|
<main class="chat-area" id="chatArea">
|
|
|
|
|
<div class="chat-header">
|
|
|
|
|
<button class="mobile-toggle" onclick="toggleSidebar()">☰</button>
|
|
|
|
|
<span class="agent-name" id="agentName">AgentKit</span>
|
|
|
|
|
<span class="status" id="connStatus">未连接</span>
|
|
|
|
|
<button class="btn-icon" onclick="toggleRightSidebar()" title="技能与工具" id="rightSidebarBtn">⚙</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="messages" id="messages">
|
|
|
|
|
<div class="welcome" id="welcome">
|
|
|
|
|
<div class="welcome-icon">🤖</div>
|
|
|
|
|
<h2>欢迎使用 AgentKit</h2>
|
|
|
|
|
<p>开始一段新对话,或从侧边栏选择已有会话。</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="input-area">
|
|
|
|
|
<div class="input-wrap">
|
|
|
|
|
<textarea id="input" rows="1" placeholder="输入消息..." onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
|
|
|
|
|
<button class="btn-send" id="sendBtn" onclick="sendMessage()">发送</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<!-- Right Sidebar -->
|
|
|
|
|
<aside class="right-sidebar" id="rightSidebar">
|
|
|
|
|
<div class="right-sidebar-header">
|
|
|
|
|
<h2>工具</h2>
|
|
|
|
|
<button class="btn-icon" onclick="toggleRightSidebar()" title="关闭" style="width:28px;height:28px;font-size:14px">×</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tab-bar">
|
|
|
|
|
<button class="tab-btn" onclick="switchTab('sources')" data-tab="sources">来源</button>
|
|
|
|
|
<button class="tab-btn active" onclick="switchTab('skills')" data-tab="skills">技能</button>
|
|
|
|
|
<button class="tab-btn" onclick="switchTab('templates')" data-tab="templates">模板</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="right-sidebar-content">
|
|
|
|
|
<!-- Sources Tab -->
|
|
|
|
|
<div class="tab-panel" id="tab-sources">
|
|
|
|
|
<div class="empty-state" style="padding:20px">信息来源配置即将上线。</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Skills Tab -->
|
|
|
|
|
<div class="tab-panel active" id="tab-skills">
|
|
|
|
|
<div class="skill-grid" id="skillGrid"></div>
|
|
|
|
|
<div class="add-skill-area">
|
|
|
|
|
<div class="add-skill-label">安装新技能</div>
|
|
|
|
|
<div class="add-skill-input">
|
|
|
|
|
<input type="text" id="installSkillName" placeholder="技能名称..." onkeydown="if(event.key==='Enter')installSkill()" oninput="updateInstallBtn()">
|
|
|
|
|
<button class="btn-add-skill" id="installBtn" onclick="installSkill()" disabled>搜索</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="install-status" id="installStatus"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Templates Tab -->
|
|
|
|
|
<div class="tab-panel" id="tab-templates">
|
|
|
|
|
<div class="empty-state" style="padding:20px">输出模板配置即将上线。</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
// ── State ──────────────────────────────────────────────────────────────
|
|
|
|
|
let sessions = [];
|
|
|
|
|
let activeSessionId = null;
|
|
|
|
|
let ws = null;
|
|
|
|
|
let isStreaming = false;
|
|
|
|
|
let currentAgentBubble = null;
|
|
|
|
|
let currentAgentRawText = ''; // accumulate raw text during streaming
|
|
|
|
|
let currentThinkingBubble = null;
|
|
|
|
|
let loadingEl = null;
|
|
|
|
|
let skills = [];
|
|
|
|
|
const API = '/api/v1/chat';
|
|
|
|
|
const SKILLS_API = '/api/v1/skills';
|
|
|
|
|
|
|
|
|
|
// ── Markdown helpers ──────────────────────────────────────────
|
|
|
|
|
const _MD_RE = /(?:^#{1,4}\s|^\s*[-*+]\s|^\s*\d+\.\s|```|\*\*|__|\[.*?\]\(|^\s*>)/m;
|
|
|
|
|
|
|
|
|
|
function isMarkdown(text) {
|
|
|
|
|
if (!text) return false;
|
|
|
|
|
return _MD_RE.test(text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderMarkdown(text) {
|
|
|
|
|
if (!text || !text.trim()) return '';
|
|
|
|
|
try {
|
|
|
|
|
return marked.parse(text, { breaks: true, gfm: true });
|
|
|
|
|
} catch {
|
|
|
|
|
return esc(text);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderBubbleContent(bubble, text, forceMd) {
|
|
|
|
|
const md = forceMd || isMarkdown(text);
|
|
|
|
|
if (md) {
|
|
|
|
|
bubble.classList.add('md');
|
|
|
|
|
bubble.innerHTML = renderMarkdown(text);
|
|
|
|
|
} else {
|
|
|
|
|
bubble.classList.remove('md');
|
|
|
|
|
bubble.textContent = text;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── API helpers ────────────────────────────────────────────────
|
|
|
|
|
async function api(base, path, opts = {}) {
|
|
|
|
|
const res = await fetch(base + path, {
|
|
|
|
|
...opts,
|
|
|
|
|
headers: { 'Content-Type': 'application/json', ...opts.headers },
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Sessions ───────────────────────────────────────────────────
|
|
|
|
|
async function loadSessions() {
|
|
|
|
|
try {
|
|
|
|
|
sessions = await api(API, '/sessions');
|
|
|
|
|
} catch {
|
|
|
|
|
sessions = [];
|
|
|
|
|
}
|
|
|
|
|
renderSessions();
|
|
|
|
|
|
|
|
|
|
const savedId = localStorage.getItem('agentkit_active_session');
|
|
|
|
|
if (savedId && sessions.some(s => s.session_id === savedId)) {
|
|
|
|
|
await selectSession(savedId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderSessions() {
|
|
|
|
|
const el = document.getElementById('sessionList');
|
|
|
|
|
if (!sessions.length) {
|
|
|
|
|
el.innerHTML = '<div class="empty-state">暂无对话<br>点击 <b>+ 新对话</b> 开始</div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
el.innerHTML = sessions.map(s => {
|
|
|
|
|
const t = new Date(s.created_at);
|
|
|
|
|
const time = t.toLocaleDateString() === new Date().toLocaleDateString()
|
|
|
|
|
? t.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})
|
|
|
|
|
: t.toLocaleDateString([], {month:'short',day:'numeric'});
|
|
|
|
|
const title = s.metadata?.title || `对话 ${s.session_id.slice(0,6)}`;
|
|
|
|
|
const active = s.session_id === activeSessionId ? 'active' : '';
|
|
|
|
|
return `<div class="session-item ${active}" onclick="selectSession('${s.session_id}')">
|
|
|
|
|
<span class="title">${esc(title)}</span>
|
|
|
|
|
<span class="time">${time}</span>
|
|
|
|
|
<span class="del" onclick="event.stopPropagation();deleteSession('${s.session_id}')" title="删除">×</span>
|
|
|
|
|
</div>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createSession() {
|
|
|
|
|
try {
|
|
|
|
|
const s = await api(API, '/sessions', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ agent_name: 'default', metadata: { title: '新对话' } }),
|
|
|
|
|
});
|
|
|
|
|
sessions.unshift(s);
|
|
|
|
|
selectSession(s.session_id);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Create session failed:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteSession(id) {
|
|
|
|
|
try {
|
|
|
|
|
await api(API, `/sessions/${id}`, { method: 'DELETE' });
|
|
|
|
|
sessions = sessions.filter(s => s.session_id !== id);
|
|
|
|
|
if (activeSessionId === id) {
|
|
|
|
|
activeSessionId = null;
|
|
|
|
|
localStorage.removeItem('agentkit_active_session');
|
|
|
|
|
disconnectWs();
|
|
|
|
|
showWelcome();
|
|
|
|
|
}
|
|
|
|
|
renderSessions();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Delete session failed:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function selectSession(id) {
|
|
|
|
|
activeSessionId = id;
|
|
|
|
|
localStorage.setItem('agentkit_active_session', id);
|
|
|
|
|
renderSessions();
|
|
|
|
|
showChat();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const msgs = await api(API, `/sessions/${id}/messages`);
|
|
|
|
|
renderHistory(msgs);
|
|
|
|
|
} catch {
|
|
|
|
|
renderHistory([]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectWs(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── WebSocket ──────────────────────────────────────────────────
|
|
|
|
|
function connectWs(sessionId) {
|
|
|
|
|
disconnectWs();
|
|
|
|
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
|
|
|
const url = `${proto}//${location.host}${API}/ws/${sessionId}`;
|
|
|
|
|
ws = new WebSocket(url);
|
|
|
|
|
|
|
|
|
|
ws.onopen = () => { setConnStatus('已连接', true); };
|
|
|
|
|
ws.onmessage = (e) => { handleWsMessage(JSON.parse(e.data)); };
|
|
|
|
|
ws.onclose = () => { setConnStatus('未连接', false); ws = null; };
|
|
|
|
|
ws.onerror = () => { setConnStatus('连接错误', false); };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function disconnectWs() {
|
|
|
|
|
if (ws) { ws.close(); ws = null; }
|
|
|
|
|
setConnStatus('未连接', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleWsMessage(msg) {
|
|
|
|
|
switch (msg.type) {
|
|
|
|
|
case 'connected':
|
|
|
|
|
setConnStatus('已连接', true);
|
|
|
|
|
break;
|
|
|
|
|
case 'routing':
|
|
|
|
|
removeLoading();
|
|
|
|
|
appendStep(`路由: ${msg.skill || 'default'} (${msg.method}, ${Math.round((msg.confidence || 0) * 100)}%)`);
|
|
|
|
|
break;
|
|
|
|
|
case 'thinking':
|
|
|
|
|
removeLoading();
|
|
|
|
|
if (!currentThinkingBubble) {
|
|
|
|
|
currentThinkingBubble = appendThinking();
|
|
|
|
|
}
|
|
|
|
|
const thinkingContent = msg.content || '';
|
|
|
|
|
if (currentThinkingBubble) {
|
|
|
|
|
currentThinkingBubble.textContent += thinkingContent;
|
|
|
|
|
}
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
break;
|
|
|
|
|
case 'token':
|
|
|
|
|
removeLoading();
|
|
|
|
|
// Clear any thinking bubble when real content starts
|
|
|
|
|
if (currentThinkingBubble) {
|
|
|
|
|
currentThinkingBubble.remove();
|
|
|
|
|
currentThinkingBubble = null;
|
|
|
|
|
}
|
|
|
|
|
if (!currentAgentBubble) {
|
|
|
|
|
currentAgentBubble = appendMessage('agent', '');
|
|
|
|
|
currentAgentRawText = '';
|
|
|
|
|
isStreaming = true;
|
|
|
|
|
updateSendBtn();
|
|
|
|
|
}
|
|
|
|
|
currentAgentRawText += msg.content || '';
|
|
|
|
|
// During streaming, show raw text for performance
|
|
|
|
|
currentAgentBubble.textContent = currentAgentRawText;
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
break;
|
|
|
|
|
case 'final_answer':
|
|
|
|
|
removeLoading();
|
|
|
|
|
// Clear thinking bubble
|
|
|
|
|
if (currentThinkingBubble) {
|
|
|
|
|
currentThinkingBubble.remove();
|
|
|
|
|
currentThinkingBubble = null;
|
|
|
|
|
}
|
|
|
|
|
if (currentAgentBubble) {
|
|
|
|
|
const final = msg.content || '';
|
|
|
|
|
// Use the longer/more complete text
|
|
|
|
|
const bestText = (!currentAgentRawText.trim() || final.length > currentAgentRawText.length) ? final : currentAgentRawText;
|
|
|
|
|
renderBubbleContent(currentAgentBubble, bestText);
|
|
|
|
|
currentAgentBubble = null;
|
|
|
|
|
currentAgentRawText = '';
|
|
|
|
|
} else {
|
|
|
|
|
appendMessage('agent', msg.content || '');
|
|
|
|
|
}
|
|
|
|
|
isStreaming = false;
|
|
|
|
|
updateSendBtn();
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
// Auto-refresh skill list after agent finishes (may have installed new skills)
|
|
|
|
|
loadSkills();
|
|
|
|
|
break;
|
|
|
|
|
case 'step':
|
|
|
|
|
removeLoading();
|
|
|
|
|
if (msg.data?.event_type === 'tool_call') {
|
|
|
|
|
// Replace thinking with tool step
|
|
|
|
|
if (currentThinkingBubble) {
|
|
|
|
|
currentThinkingBubble.remove();
|
|
|
|
|
currentThinkingBubble = null;
|
|
|
|
|
}
|
|
|
|
|
appendStep(`使用工具: ${msg.data?.data?.tool_name || 'tool'}`);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'skill_match':
|
|
|
|
|
removeLoading();
|
|
|
|
|
if (msg.data?.skill) {
|
|
|
|
|
appendStep(`技能: ${msg.data.skill} (${msg.data.method}, ${Math.round((msg.data.confidence || 0) * 100)}%)`);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'confirmation_request':
|
|
|
|
|
removeLoading();
|
|
|
|
|
showConfirmationCard(msg.data);
|
|
|
|
|
break;
|
|
|
|
|
case 'confirmation_result':
|
|
|
|
|
updateConfirmationCard(msg.data);
|
|
|
|
|
break;
|
|
|
|
|
case 'error':
|
|
|
|
|
removeLoading();
|
|
|
|
|
// Clear thinking bubble on error
|
|
|
|
|
if (currentThinkingBubble) {
|
|
|
|
|
currentThinkingBubble.remove();
|
|
|
|
|
currentThinkingBubble = null;
|
|
|
|
|
}
|
|
|
|
|
appendMessage('agent', `[错误] ${msg.data?.message || '未知错误'}`);
|
|
|
|
|
currentAgentBubble = null;
|
|
|
|
|
isStreaming = false;
|
|
|
|
|
updateSendBtn();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Loading indicator ──────────────────────────────────────────
|
|
|
|
|
function showLoading() {
|
|
|
|
|
if (loadingEl) return;
|
|
|
|
|
hideWelcome();
|
|
|
|
|
const container = document.getElementById('messages');
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.className = 'loading-msg';
|
|
|
|
|
div.id = 'loadingIndicator';
|
|
|
|
|
div.innerHTML = '<div class="loading-bubble"><div class="loading-dot"></div><div class="loading-dot"></div><div class="loading-dot"></div></div>';
|
|
|
|
|
container.appendChild(div);
|
|
|
|
|
loadingEl = div;
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeLoading() {
|
|
|
|
|
if (loadingEl) {
|
|
|
|
|
loadingEl.remove();
|
|
|
|
|
loadingEl = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Confirmation Card ──────────────────────────────────────────
|
|
|
|
|
function showConfirmationCard(data) {
|
|
|
|
|
hideWelcome();
|
|
|
|
|
const container = document.getElementById('messages');
|
|
|
|
|
const confirmationId = data.confirmation_id;
|
|
|
|
|
const command = data.command || '';
|
|
|
|
|
const reason = data.reason || '';
|
|
|
|
|
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.className = 'confirm-card';
|
|
|
|
|
div.id = `confirm-${confirmationId}`;
|
|
|
|
|
div.innerHTML = `
|
|
|
|
|
<div class="card-inner">
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<div class="card-icon">⚠</div>
|
|
|
|
|
<div class="card-title">操作确认</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="card-command">${esc(command)}</div>
|
|
|
|
|
${reason ? `<div class="card-reason">${esc(reason)}</div>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card-actions" id="confirm-actions-${confirmationId}">
|
|
|
|
|
<button class="btn-confirm approve" onclick="replyConfirmation('${confirmationId}', true)">确认执行</button>
|
|
|
|
|
<button class="btn-confirm deny" onclick="replyConfirmation('${confirmationId}', false)">拒绝</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
container.appendChild(div);
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function replyConfirmation(confirmationId, approved) {
|
|
|
|
|
// Disable buttons immediately
|
|
|
|
|
const actionsEl = document.getElementById(`confirm-actions-${confirmationId}`);
|
|
|
|
|
if (actionsEl) {
|
|
|
|
|
const buttons = actionsEl.querySelectorAll('.btn-confirm');
|
|
|
|
|
buttons.forEach(btn => { btn.disabled = true; });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send reply to server
|
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
|
|
|
ws.send(JSON.stringify({
|
|
|
|
|
type: 'confirmation_reply',
|
|
|
|
|
confirmation_id: confirmationId,
|
|
|
|
|
approved: approved,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateConfirmationCard(data) {
|
|
|
|
|
const confirmationId = data.confirmation_id;
|
|
|
|
|
const approved = data.approved;
|
|
|
|
|
const cardEl = document.getElementById(`confirm-${confirmationId}`);
|
|
|
|
|
if (!cardEl) return;
|
|
|
|
|
|
|
|
|
|
// Replace actions with status
|
|
|
|
|
const actionsEl = document.getElementById(`confirm-actions-${confirmationId}`);
|
|
|
|
|
if (actionsEl) {
|
|
|
|
|
const statusClass = approved ? 'approved' : 'denied';
|
|
|
|
|
const statusText = approved ? '✓ 已确认执行' : '✗ 已拒绝';
|
|
|
|
|
actionsEl.outerHTML = `<div class="card-status ${statusClass}">${statusText}</div>`;
|
|
|
|
|
}
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Send message ───────────────────────────────────────────────
|
|
|
|
|
async function sendMessage() {
|
|
|
|
|
const input = document.getElementById('input');
|
|
|
|
|
const text = input.value.trim();
|
|
|
|
|
if (!text) return;
|
|
|
|
|
|
|
|
|
|
// Auto-create session if none is active
|
|
|
|
|
if (!activeSessionId) {
|
|
|
|
|
try {
|
|
|
|
|
const s = await api(API, '/sessions', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ agent_name: 'default', metadata: { title: text.slice(0, 30) } }),
|
|
|
|
|
});
|
|
|
|
|
sessions.unshift(s);
|
|
|
|
|
activeSessionId = s.session_id;
|
|
|
|
|
localStorage.setItem('agentkit_active_session', s.session_id);
|
|
|
|
|
renderSessions();
|
|
|
|
|
showChat();
|
|
|
|
|
renderHistory([]);
|
|
|
|
|
connectWs(s.session_id);
|
|
|
|
|
// Wait for WebSocket to open before sending
|
|
|
|
|
await new Promise(resolve => {
|
|
|
|
|
const check = () => ws && ws.readyState === WebSocket.OPEN ? resolve() : setTimeout(check, 50);
|
|
|
|
|
check();
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Auto-create session failed:', e);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
|
|
|
|
|
|
|
|
appendMessage('user', text);
|
|
|
|
|
input.value = '';
|
|
|
|
|
autoResize(input);
|
|
|
|
|
|
|
|
|
|
ws.send(JSON.stringify({ type: 'message', content: text }));
|
|
|
|
|
currentAgentBubble = null;
|
|
|
|
|
isStreaming = true;
|
|
|
|
|
updateSendBtn();
|
|
|
|
|
showLoading();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleKey(e) {
|
|
|
|
|
// e.isComposing is true during IME composition — don't submit then
|
|
|
|
|
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
sendMessage();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── UI helpers ─────────────────────────────────────────────────
|
|
|
|
|
function appendMessage(role, content) {
|
|
|
|
|
hideWelcome();
|
|
|
|
|
const container = document.getElementById('messages');
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
const cssRole = role === 'assistant' ? 'agent' : role;
|
|
|
|
|
div.className = `msg ${cssRole}`;
|
|
|
|
|
const bubble = document.createElement('div');
|
|
|
|
|
bubble.className = 'bubble';
|
|
|
|
|
// Render markdown for agent messages
|
|
|
|
|
if (cssRole === 'agent' && content) {
|
|
|
|
|
renderBubbleContent(bubble, content);
|
|
|
|
|
} else {
|
|
|
|
|
bubble.textContent = content;
|
|
|
|
|
}
|
|
|
|
|
div.appendChild(bubble);
|
|
|
|
|
const meta = document.createElement('div');
|
|
|
|
|
meta.className = 'meta';
|
|
|
|
|
meta.textContent = cssRole === 'user' ? '你' : '智能体';
|
|
|
|
|
div.appendChild(meta);
|
|
|
|
|
container.appendChild(div);
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
return bubble;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendStep(text) {
|
|
|
|
|
hideWelcome();
|
|
|
|
|
const container = document.getElementById('messages');
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.className = 'msg agent';
|
|
|
|
|
const bubble = document.createElement('div');
|
|
|
|
|
bubble.className = 'bubble';
|
|
|
|
|
bubble.style.cssText = 'opacity:.5;font-size:12px;font-style:italic;border:none;background:transparent;padding:4px 8px;box-shadow:none';
|
|
|
|
|
bubble.textContent = text;
|
|
|
|
|
div.appendChild(bubble);
|
|
|
|
|
container.appendChild(div);
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendThinking() {
|
|
|
|
|
hideWelcome();
|
|
|
|
|
const container = document.getElementById('messages');
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.className = 'thinking-msg';
|
|
|
|
|
const bubble = document.createElement('div');
|
|
|
|
|
bubble.className = 'bubble';
|
|
|
|
|
div.appendChild(bubble);
|
|
|
|
|
container.appendChild(div);
|
|
|
|
|
return bubble;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderHistory(msgs) {
|
|
|
|
|
const container = document.getElementById('messages');
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
if (!msgs.length) {
|
|
|
|
|
container.innerHTML = '<div class="welcome" id="welcome"><div class="welcome-icon">🤖</div><h2>欢迎使用 AgentKit</h2><p>开始一段新对话,或从侧边栏选择已有会话。</p></div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for (const m of msgs) {
|
|
|
|
|
if (m.role === 'user' || m.role === 'assistant') {
|
|
|
|
|
appendMessage(m.role, m.content);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showWelcome() { const el = document.getElementById('welcome'); if (el) el.style.display = 'flex'; }
|
|
|
|
|
function hideWelcome() { const el = document.getElementById('welcome'); if (el) el.style.display = 'none'; }
|
|
|
|
|
function showChat() { hideWelcome(); }
|
|
|
|
|
function setConnStatus(text, connected) {
|
|
|
|
|
const el = document.getElementById('connStatus');
|
|
|
|
|
el.textContent = text;
|
|
|
|
|
el.className = 'status' + (connected ? ' connected' : '');
|
|
|
|
|
}
|
|
|
|
|
function updateSendBtn() { document.getElementById('sendBtn').disabled = isStreaming; }
|
|
|
|
|
function scrollToBottom() { const el = document.getElementById('messages'); el.scrollTop = el.scrollHeight; }
|
|
|
|
|
function autoResize(el) { el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 160) + 'px'; }
|
|
|
|
|
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
|
|
|
|
|
|
|
|
function toggleSidebar() {
|
|
|
|
|
document.getElementById('sidebar').classList.toggle('open');
|
|
|
|
|
document.getElementById('overlay').classList.toggle('show');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Right Sidebar ──────────────────────────────────────────────
|
|
|
|
|
function toggleRightSidebar() {
|
|
|
|
|
document.getElementById('rightSidebar').classList.toggle('open');
|
|
|
|
|
if (document.getElementById('rightSidebar').classList.contains('open')) {
|
|
|
|
|
loadSkills();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function switchTab(tabId) {
|
|
|
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === tabId));
|
|
|
|
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.toggle('active', p.id === `tab-${tabId}`));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Skills ─────────────────────────────────────────────────────
|
|
|
|
|
async function loadSkills() {
|
|
|
|
|
try {
|
|
|
|
|
skills = await api(SKILLS_API, '');
|
|
|
|
|
renderSkillGrid();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Load skills failed:', e);
|
|
|
|
|
skills = [];
|
|
|
|
|
renderSkillGrid();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderSkillGrid() {
|
|
|
|
|
const grid = document.getElementById('skillGrid');
|
|
|
|
|
if (!skills.length) {
|
|
|
|
|
grid.innerHTML = '<div class="empty-state" style="padding:20px;grid-column:1/-1">暂无已安装的技能。</div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
grid.innerHTML = skills.map(s => {
|
|
|
|
|
const desc = s.intent_description || s.description || '暂无描述';
|
|
|
|
|
const tools = s.bound_tools && s.bound_tools.length ? s.bound_tools.join(', ') : (s.tools && s.tools.length ? s.tools.join(', ') : '');
|
|
|
|
|
return `<div class="skill-card" onclick="useSkill('${esc(s.name)}')" title="点击使用此技能">
|
|
|
|
|
<button class="skill-remove" onclick="event.stopPropagation();removeSkill('${esc(s.name)}')" title="移除">×</button>
|
|
|
|
|
<div class="skill-name">${esc(s.name)}</div>
|
|
|
|
|
<div class="skill-desc">${esc(desc)}</div>
|
|
|
|
|
${tools ? `<div class="skill-tools">${esc(tools)}</div>` : ''}
|
|
|
|
|
</div>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function useSkill(name) {
|
|
|
|
|
const skill = skills.find(s => s.name === name);
|
|
|
|
|
if (!skill) return;
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById('input');
|
|
|
|
|
const skillRef = `@skill:${name} `;
|
|
|
|
|
if (!input.value.includes(skillRef)) {
|
|
|
|
|
input.value = skillRef + input.value;
|
|
|
|
|
input.focus();
|
|
|
|
|
autoResize(input);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateInstallBtn() {
|
|
|
|
|
const nameInput = document.getElementById('installSkillName');
|
|
|
|
|
const btn = document.getElementById('installBtn');
|
|
|
|
|
btn.disabled = !nameInput.value.trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function installSkill() {
|
|
|
|
|
const nameInput = document.getElementById('installSkillName');
|
|
|
|
|
const name = nameInput.value.trim();
|
|
|
|
|
if (!name) return;
|
|
|
|
|
|
|
|
|
|
// Clear input immediately to prevent re-triggering
|
|
|
|
|
nameInput.value = '';
|
|
|
|
|
updateInstallBtn();
|
|
|
|
|
|
|
|
|
|
const btn = document.getElementById('installBtn');
|
|
|
|
|
const status = document.getElementById('installStatus');
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
btn.textContent = '搜索中...';
|
|
|
|
|
status.className = 'install-status';
|
|
|
|
|
status.textContent = '正在搜索并安装...';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await api(SKILLS_API, '/install', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ name }),
|
|
|
|
|
});
|
|
|
|
|
status.className = 'install-status success';
|
|
|
|
|
status.textContent = `技能 "${result.name}" 安装成功!`;
|
|
|
|
|
await loadSkills();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
status.className = 'install-status error';
|
|
|
|
|
status.textContent = `自动安装失败,正在请求智能体协助...`;
|
|
|
|
|
|
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
|
|
|
const installMsg = `请帮我安装一个名为"${name}"的技能。请使用 skill_install 工具安装,source 参数格式为 owner/repo@skill。如果不知道完整 source,先用 shell 执行 npx skills search ${name} 搜索。`;
|
|
|
|
|
appendMessage('user', installMsg);
|
|
|
|
|
ws.send(JSON.stringify({ type: 'message', content: installMsg }));
|
|
|
|
|
currentAgentBubble = null;
|
|
|
|
|
isStreaming = true;
|
|
|
|
|
updateSendBtn();
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
btn.textContent = '搜索';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function removeSkill(name) {
|
|
|
|
|
if (!confirm(`确定移除技能 "${name}" 吗?`)) return;
|
|
|
|
|
try {
|
|
|
|
|
await api(SKILLS_API, `/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
|
|
|
await loadSkills();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Remove skill failed:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Init ───────────────────────────────────────────────────────
|
|
|
|
|
loadSessions();
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8" />
|
|
|
|
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
|
|
<title>Fischer AgentKit</title>
|
|
|
|
|
<script type="module" crossorigin src="/assets/index-C33ddnd0.js"></script>
|
|
|
|
|
<link rel="stylesheet" crossorigin href="/assets/index-vR_mL1Yy.css">
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div id="app"></div>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
|