mirror of
https://github.com/myronblair/jarvis
synced 2026-07-28 08:43:00 -05:00
feat: Phase 2 — Intel Protocol + Iron Protocol
Arc Reactor v2.0: - research handler: DDG search → async page fetch → trafilatura extraction → Claude synthesis - tool_loop handler: multi-step agent loop (up to 200 iter) with web_search, fetch_url, jarvis_agents, jarvis_alerts, current_time tools - llm handler: multi-provider router (Claude/Groq/Ollama) - /jobs/recent endpoint for HUD polling - Phase 1 handlers preserved (ping/echo/shell) chat.php — Tier 0.9 Intel Protocol (before KB intent engine): - Detects: research/investigate/deep-dive/look up/find out about → research job - Detects: step-by-step/figure out/analyze and report → tool_loop job - Returns arc_job ID in response for UI polling - Depth modifiers: quick/standard/deep index.html: - INTEL tab in right panel tab bar - Research result cards with expand/collapse, synthesis, sources, status - Live polling (4s) when INTEL tab is active + active jobs present - Auto-switches to INTEL tab when research is triggered from chat - intelPrompt() pre-fills chat input for new research Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -875,6 +875,27 @@ body::after{
|
||||
transition:filter 1.2s ease;
|
||||
}
|
||||
#app.sleeping #sleepOverlay{display:flex}
|
||||
/* ── INTEL PROTOCOL — research result cards ──────────────────────── */
|
||||
.intel-card{background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:8px;overflow:hidden}
|
||||
.intel-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none}
|
||||
.intel-card-head:hover{background:rgba(0,212,255,0.06)}
|
||||
.intel-card-query{font-family:var(--font-display);font-size:0.6rem;letter-spacing:1px;color:var(--cyan);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.intel-card-status{font-family:var(--font-mono);font-size:0.55rem;padding:2px 6px;border-radius:2px;flex-shrink:0}
|
||||
.intel-card-status.running{color:#ffd700;border:1px solid rgba(255,215,0,0.4);animation:pulse 1.5s ease-in-out infinite}
|
||||
.intel-card-status.done{color:var(--green);border:1px solid rgba(0,255,136,0.3)}
|
||||
.intel-card-status.failed{color:var(--red);border:1px solid rgba(255,34,68,0.3)}
|
||||
.intel-card-body{display:none;padding:0 10px 10px;border-top:1px solid var(--panel-border)}
|
||||
.intel-card.open .intel-card-body{display:block}
|
||||
.intel-card-body .synthesis{font-size:0.65rem;line-height:1.6;color:var(--text);margin:8px 0;white-space:pre-wrap}
|
||||
.intel-sources{margin-top:8px}
|
||||
.intel-source{font-size:0.58rem;color:var(--text-dim);padding:2px 0;border-bottom:1px solid rgba(0,212,255,0.06)}
|
||||
.intel-source a{color:var(--cyan2);text-decoration:none}
|
||||
.intel-source a:hover{text-decoration:underline}
|
||||
.intel-empty{text-align:center;padding:24px 10px;font-family:var(--font-mono);font-size:0.6rem;color:var(--text-dim);letter-spacing:1px}
|
||||
.intel-new-btn{width:100%;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:4px;padding:5px;color:var(--cyan);font-family:var(--font-display);font-size:0.55rem;letter-spacing:2px;cursor:pointer;margin-bottom:8px}
|
||||
.intel-new-btn:hover{background:rgba(0,212,255,0.12)}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1077,6 +1098,7 @@ body::after{
|
||||
<div class="tab" onclick="switchTab('news')">NEWS</div>
|
||||
<div class="tab" onclick="switchTab('agents')">AGENTS</div>
|
||||
<div class="tab" onclick="switchTab('sites')">SITES</div>
|
||||
<div class="tab" id="tab-btn-intel" onclick="switchTab('intel')">INTEL</div>
|
||||
</div>
|
||||
<div id="tab-vms" class="tab-pane" style="overflow-y:auto;flex:1">
|
||||
<div id="vm-list"><div class="loading-shimmer"></div></div>
|
||||
@@ -1093,6 +1115,9 @@ body::after{
|
||||
<div id="tab-agents" class="tab-pane" style="overflow-y:auto;flex:1">
|
||||
<div id="agents-list"><div class="loading-shimmer"></div></div>
|
||||
</div>
|
||||
<div id="tab-intel" class="tab-pane" style="overflow-y:auto;flex:1;padding:4px 0">
|
||||
<div id="intel-list"><div class="loading-shimmer"></div></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -2997,6 +3022,7 @@ function switchTab(name) {
|
||||
if (pane) pane.classList.add('active');
|
||||
if (name === 'news') loadNews();
|
||||
if (name === 'agents') loadAgents();
|
||||
if (name === 'intel') loadIntel();
|
||||
if (name === 'alerts') loadAlerts();
|
||||
}
|
||||
|
||||
@@ -3140,6 +3166,7 @@ async function sendMessage() {
|
||||
addMessage('jarvis', data.reply);
|
||||
speak(data.reply);
|
||||
}
|
||||
if (data.arc_job) { onArcJobStarted(data.arc_job, data.source || ''); }
|
||||
} catch(e) {
|
||||
const bubble = document.getElementById('thinking-bubble');
|
||||
if (bubble) bubble.remove();
|
||||
@@ -3497,6 +3524,135 @@ async function arcWaitJob(jobId, onProgress) {
|
||||
}
|
||||
|
||||
|
||||
// ── INTEL PROTOCOL — HUD panel ────────────────────────────────────────
|
||||
let _intelPollTimer = null;
|
||||
let _intelActiveJobs = new Set();
|
||||
let _intelLastLoad = 0;
|
||||
|
||||
async function loadIntel() {
|
||||
const el = document.getElementById('intel-list');
|
||||
if (!el) return;
|
||||
_intelLastLoad = Date.now();
|
||||
|
||||
try {
|
||||
// Fetch recent research + tool_loop jobs
|
||||
const [resJobs, toolJobs] = await Promise.all([
|
||||
api('arc?action=jobs&status=&limit=20').catch(() => []),
|
||||
Promise.resolve([]),
|
||||
]);
|
||||
const jobs = Array.isArray(resJobs) ? resJobs.filter(j => ['research','tool_loop','llm'].includes(j.job_type)) : [];
|
||||
|
||||
if (!jobs.length) {
|
||||
el.innerHTML = '<div class="intel-empty">◈ NO INTEL JOBS<br><span style="opacity:0.5">Say "research [topic]" to activate</span></div>';
|
||||
stopIntelPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for active jobs
|
||||
const hasActive = jobs.some(j => j.status === 'queued' || j.status === 'running');
|
||||
if (hasActive) startIntelPolling(); else stopIntelPolling();
|
||||
|
||||
let html = '<button class="intel-new-btn" onclick="intelPrompt()">⚡ NEW RESEARCH</button>';
|
||||
for (const job of jobs) {
|
||||
const isOpen = _intelActiveJobs.has(job.id) || job.status === 'running';
|
||||
const statusClass = job.status === 'done' ? 'done' : job.status === 'failed' ? 'failed' : 'running';
|
||||
const statusLabel = job.status === 'queued' ? 'QUEUED' : job.status === 'running' ? '● ACTIVE' : job.status.toUpperCase();
|
||||
const typeLabel = job.job_type === 'research' ? '◈ INTEL' : job.job_type === 'tool_loop' ? '⚡ IRON' : '◈ LLM';
|
||||
|
||||
// Get result details if done
|
||||
let bodyHtml = '';
|
||||
if (job.status === 'done' && job.result) {
|
||||
let r = job.result;
|
||||
if (typeof r === 'string') { try { r = JSON.parse(r); } catch(e) {} }
|
||||
if (typeof r === 'object') {
|
||||
const synthesis = (r.synthesis || r.result || r.response || '').trim();
|
||||
const sources = r.sources || [];
|
||||
const query = r.query || r.task || '';
|
||||
const provider = r.provider || '';
|
||||
|
||||
bodyHtml = `<div class="intel-card-body">`;
|
||||
if (provider) bodyHtml += `<div style="font-size:0.55rem;color:var(--text-dim);margin:6px 0 2px;font-family:var(--font-mono)">PROVIDER: ${provider.toUpperCase()} · SOURCES: ${r.source_count||sources.length||'—'}</div>`;
|
||||
if (synthesis) bodyHtml += `<div class="synthesis">${escHtml(synthesis.substring(0, 1500))}${synthesis.length>1500?'
|
||||
|
||||
[...truncated — view in admin]':''}</div>`;
|
||||
if (sources.length) {
|
||||
bodyHtml += '<div class="intel-sources"><div style="font-size:0.55rem;letter-spacing:2px;color:var(--text-dim);margin-bottom:4px;font-family:var(--font-display)">SOURCES</div>';
|
||||
sources.slice(0,5).forEach((s,i) => {
|
||||
const title = escHtml((s.title||s.url||'').substring(0,60));
|
||||
const url = escHtml(s.url||'');
|
||||
bodyHtml += `<div class="intel-source">${i+1}. <a href="${url}" target="_blank" rel="noopener">${title||url}</a></div>`;
|
||||
});
|
||||
bodyHtml += '</div>';
|
||||
}
|
||||
bodyHtml += '</div>';
|
||||
}
|
||||
} else if (job.status === 'running' || job.status === 'queued') {
|
||||
const typeMsg = job.job_type === 'research' ? 'Searching sources and extracting content...' : 'Executing tool loop...';
|
||||
bodyHtml = `<div class="intel-card-body"><div style="font-size:0.62rem;color:var(--text-dim);padding:8px 0;font-family:var(--font-mono)">${typeMsg}</div></div>`;
|
||||
} else if (job.status === 'failed' && job.error) {
|
||||
bodyHtml = `<div class="intel-card-body"><div style="font-size:0.62rem;color:var(--red);padding:8px 0;font-family:var(--font-mono)">${escHtml(job.error.substring(0,200))}</div></div>`;
|
||||
}
|
||||
|
||||
const queryText = job.created_by ? job.created_by.replace('chat:', '').replace(/session.*/, '') : '';
|
||||
const ts = job.created_at ? new Date(job.created_at).toLocaleTimeString() : '';
|
||||
|
||||
html += `<div class="intel-card${(isOpen && bodyHtml) ? ' open':''}" id="intel-card-${job.id}">
|
||||
<div class="intel-card-head" onclick="toggleIntelCard(${job.id})">
|
||||
<span style="font-size:0.55rem;color:var(--text-dim);font-family:var(--font-mono);flex-shrink:0">${typeLabel}</span>
|
||||
<span class="intel-card-query">#${job.id} ${escHtml((job.created_by||'').replace('chat:','').substring(0,40))}</span>
|
||||
<span style="font-size:0.55rem;color:var(--text-dim);flex-shrink:0;font-family:var(--font-mono)">${ts}</span>
|
||||
<span class="intel-card-status ${statusClass}">${statusLabel}</span>
|
||||
</div>
|
||||
${bodyHtml}
|
||||
</div>`;
|
||||
}
|
||||
el.innerHTML = html;
|
||||
|
||||
} catch(e) {
|
||||
if (el) el.innerHTML = '<div class="intel-empty">INTEL OFFLINE</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleIntelCard(id) {
|
||||
const card = document.getElementById('intel-card-' + id);
|
||||
if (!card) return;
|
||||
if (_intelActiveJobs.has(id)) _intelActiveJobs.delete(id);
|
||||
else _intelActiveJobs.add(id);
|
||||
card.classList.toggle('open');
|
||||
}
|
||||
|
||||
function startIntelPolling() {
|
||||
if (_intelPollTimer) return;
|
||||
_intelPollTimer = setInterval(() => {
|
||||
if (document.getElementById('tab-intel')?.classList.contains('active')) {
|
||||
loadIntel();
|
||||
}
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
function stopIntelPolling() {
|
||||
if (_intelPollTimer) { clearInterval(_intelPollTimer); _intelPollTimer = null; }
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function intelPrompt() {
|
||||
const input = document.getElementById('textInput');
|
||||
if (input) { input.value = 'research '; input.focus(); }
|
||||
}
|
||||
|
||||
// Called from chat.js when arc_job is returned
|
||||
function onArcJobStarted(jobId, jobType) {
|
||||
_intelActiveJobs.add(jobId);
|
||||
// Auto-switch to INTEL tab
|
||||
const intelTab = document.querySelector('[onclick*="switchTab(\'intel\')"]');
|
||||
if (intelTab) intelTab.click();
|
||||
startIntelPolling();
|
||||
}
|
||||
|
||||
|
||||
async function loadAgents() {
|
||||
const [listData, metricsData] = await Promise.all([
|
||||
api('agent/list'),
|
||||
|
||||
Reference in New Issue
Block a user