algorembrant's picture
Upload 18 files
b9a3ef2 verified
// ---------------------------------------------------------------------------
// Agent Bridge -- Frontend Application (v2.1)
// ---------------------------------------------------------------------------
// Handles WebSocket, settings panel, honest CLI status, real chat relay,
// tool rail, suggestion chips, and output rendering.
// ---------------------------------------------------------------------------
(function () {
'use strict';
// -----------------------------------------------------------------------
// DOM
// -----------------------------------------------------------------------
const $ = (id) => document.getElementById(id);
const $toolRail = $('tool-rail');
const $centerCard = $('center-card');
const $greeting = $('greeting');
const $promptInput = $('prompt-input');
const $sendBtn = $('send-btn');
const $iconBar = $('icon-bar');
const $chips = $('suggestion-chips');
const $connInd = $('connection-indicator');
const $statusText = $('status-text');
const $cardClose = $('card-close');
const $settingsToggle = $('settings-toggle');
// Settings panel
const $settingsPanel = $('settings-panel');
const $settingsClose = $('settings-close');
const $cliPathInput = $('cli-path-input');
const $savePathBtn = $('save-path-btn');
const $detectIndicator = $('detect-indicator');
const $detectText = $('detect-text');
const $detectDetails = $('detect-details');
const $redetectBtn = $('redetect-btn');
const $connectBtn = $('connect-btn');
const $disconnectBtn = $('disconnect-btn');
const $sessionStatus = $('session-status');
// Conversation
const $conversation = $('conversation');
const $messages = $('messages');
const $convInput = $('conv-input');
const $convSendBtn = $('conv-send-btn');
const $outputArea = $('output-area');
const $outputContent = $('output-content');
const $closeOutput = $('close-output');
const $linkTools = $('link-tools');
const $linkBridge = $('link-bridge');
// -----------------------------------------------------------------------
// State
// -----------------------------------------------------------------------
let socket = null;
let tools = [];
let msgCount = 0;
let inConversation = false;
let cliInfo = { detected: false, version: null, path: null, method: null };
let sessionActive = false;
let savedSettings = { cliPath: '', autoConnect: false };
// -----------------------------------------------------------------------
// Connect
// -----------------------------------------------------------------------
function connect() {
socket = io({ transports: ['websocket', 'polling'] });
socket.on('connect', () => {
$connInd.classList.add('connected');
updateStatusText();
});
socket.on('disconnect', () => {
$connInd.classList.remove('connected');
$statusText.textContent = 'Disconnected';
});
socket.on('connect_error', () => {
$connInd.classList.remove('connected');
$statusText.textContent = 'Connection failed';
});
// Initial state from server
socket.on('bridge:init', (data) => {
tools = data.tools || [];
cliInfo = data.cli || {};
savedSettings = data.settings || {};
sessionActive = data.sessionActive || false;
renderToolRail(tools);
renderIconBar(tools);
renderChips(tools);
updateStatusText();
updateSettingsPanel();
});
// Chat messages
socket.on('agent:message', (data) => {
if (data.from === 'system') {
addMsg('system', '', data.message);
} else if (data.from !== 'human') {
addMsg('agent', 'Agent', data.message);
}
});
// Tool events
socket.on('tool:started', (data) => {
addMsg('tool', data.toolName, 'Running...');
showOutput();
appendOutput(`[${data.toolName}] Started\n`);
});
socket.on('tool:progress', (data) => {
appendProgressLine(data.progress);
});
socket.on('tool:completed', (data) => {
addMsg('tool', data.toolName, 'Completed.');
renderToolResult(data.result);
});
socket.on('tool:error', (data) => {
addMsg('error', data.toolName, data.error);
appendOutput(`[Error] ${data.error}\n`);
});
// CLI stdout/stderr -- these come from the real Antigravity process
socket.on('cli:stdout', (data) => {
showOutput();
appendOutput(data.data);
// Also show in chat as agent messages (strip ANSI codes)
const clean = stripAnsi(data.data).trim();
if (clean) {
addMsg('agent', 'Antigravity', clean);
}
});
socket.on('cli:stderr', (data) => {
showOutput();
appendOutput(`${data.data}`);
});
socket.on('cli:started', () => {
sessionActive = true;
addMsg('system', '', 'Antigravity CLI session started.');
updateStatusText();
updateSessionButtons();
});
socket.on('cli:starting', () => {
addMsg('system', '', 'Starting Antigravity CLI...');
});
socket.on('cli:ended', (data) => {
sessionActive = false;
addMsg('system', '', `Antigravity CLI session ended${data.exitCode !== null ? ` (exit code ${data.exitCode})` : ''}.`);
updateStatusText();
updateSessionButtons();
});
socket.on('cli:error', (data) => {
addMsg('error', 'CLI', data.message);
sessionActive = false;
updateStatusText();
updateSessionButtons();
});
// Settings changes from other clients
socket.on('settings:changed', (data) => {
savedSettings = data.settings;
cliInfo = data.cli;
updateSettingsPanel();
updateStatusText();
});
}
// -----------------------------------------------------------------------
// Status
// -----------------------------------------------------------------------
function updateStatusText() {
const parts = [];
if (socket?.connected) {
parts.push('Server: connected');
} else {
parts.push('Server: disconnected');
}
if (sessionActive) {
parts.push('CLI: active session');
} else if (cliInfo.detected) {
parts.push('CLI: detected (not running)');
} else {
parts.push('CLI: not detected');
}
$statusText.textContent = parts.join(' | ');
}
// -----------------------------------------------------------------------
// Settings Panel
// -----------------------------------------------------------------------
function openSettings() {
$settingsPanel.classList.remove('hidden');
$cliPathInput.value = savedSettings.cliPath || '';
updateSettingsPanel();
}
function closeSettings() {
$settingsPanel.classList.add('hidden');
}
function updateSettingsPanel() {
// Detection status
if (cliInfo.detected) {
$detectIndicator.className = 'detect-dot found';
$detectText.textContent = `Detected: ${cliInfo.path || 'unknown'}`;
$detectDetails.textContent = `Version: ${cliInfo.version || 'unknown'}\nMethod: ${cliInfo.method || 'unknown'}`;
} else {
$detectIndicator.className = 'detect-dot missing';
$detectText.textContent = 'Not detected';
$detectDetails.textContent = 'Configure the path above and click Save, or install Antigravity CLI and click Re-detect.';
}
// Path input
if (savedSettings.cliPath && !$cliPathInput.value) {
$cliPathInput.value = savedSettings.cliPath;
}
updateSessionButtons();
}
function updateSessionButtons() {
$connectBtn.disabled = sessionActive;
$disconnectBtn.disabled = !sessionActive;
$sessionStatus.textContent = sessionActive
? 'CLI session is active. Messages will be sent to the Antigravity agent.'
: 'No active session. Click Connect to start.';
}
// Save CLI path
$savePathBtn.addEventListener('click', () => {
const path = $cliPathInput.value.trim();
socket.emit('settings:update', { cliPath: path }, (response) => {
savedSettings = response.settings;
cliInfo = response.cli;
updateSettingsPanel();
updateStatusText();
addMsg('system', '', cliInfo.detected
? `CLI detected at: ${cliInfo.path}`
: `CLI not found at the specified path. Check the path and try again.`);
});
});
// Re-detect
$redetectBtn.addEventListener('click', () => {
$detectText.textContent = 'Detecting...';
socket.emit('settings:update', { cliPath: $cliPathInput.value.trim() }, (response) => {
savedSettings = response.settings;
cliInfo = response.cli;
updateSettingsPanel();
updateStatusText();
});
});
// Connect CLI session
$connectBtn.addEventListener('click', () => {
$connectBtn.disabled = true;
$sessionStatus.textContent = 'Starting...';
socket.emit('cli:start', { cliPath: savedSettings.cliPath || cliInfo.path });
});
// Disconnect CLI session
$disconnectBtn.addEventListener('click', () => {
socket.emit('cli:stop');
});
$settingsToggle.addEventListener('click', openSettings);
$settingsClose.addEventListener('click', closeSettings);
// Close settings on backdrop click
$settingsPanel.addEventListener('click', (e) => {
if (e.target === $settingsPanel) closeSettings();
});
// -----------------------------------------------------------------------
// Tool Rail
// -----------------------------------------------------------------------
function renderToolRail(toolList) {
$toolRail.innerHTML = '';
toolList.forEach((tool, i) => {
const btn = document.createElement('button');
btn.className = 'rail-btn' + (tool.mock ? '' : ' active');
btn.textContent = String(i + 1);
btn.title = tool.name.replace(/_/g, ' ');
btn.addEventListener('click', () => insertSyntax(tool.syntax || `use <${tool.name}> `));
$toolRail.appendChild(btn);
});
}
// -----------------------------------------------------------------------
// Icon Bar
// -----------------------------------------------------------------------
function renderIconBar(toolList) {
$iconBar.innerHTML = '';
addIconButton($iconBar, iconSvg('globe'), 'Browse tools', () => openSettings());
toolList.slice(0, 6).forEach((tool) => {
addIconButton($iconBar, tool.mock ? iconSvg('box') : iconSvg('zap'),
tool.name.replace(/_/g, ' '),
() => insertSyntax(tool.syntax || `use <${tool.name}> `)
);
});
}
function addIconButton(parent, svgHtml, title, onClick) {
const btn = document.createElement('button');
btn.className = 'icon-btn';
btn.innerHTML = svgHtml;
btn.title = title;
btn.addEventListener('click', onClick);
parent.appendChild(btn);
}
// -----------------------------------------------------------------------
// Chips
// -----------------------------------------------------------------------
function renderChips(toolList) {
$chips.innerHTML = '';
const suggestions = [
{ label: 'Transcribe YouTube', syntax: 'use <transcript_tool> ' },
...toolList.filter(t => t.mock).slice(0, 3).map(t => ({
label: t.name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
syntax: t.syntax,
})),
];
suggestions.forEach((s) => {
const chip = document.createElement('button');
chip.className = 'chip';
chip.textContent = s.label;
chip.addEventListener('click', () => insertSyntax(s.syntax));
$chips.appendChild(chip);
});
}
// -----------------------------------------------------------------------
// Input helpers
// -----------------------------------------------------------------------
function insertSyntax(syntax) {
const input = inConversation ? $convInput : $promptInput;
input.value = syntax;
input.focus();
input.setSelectionRange(input.value.length, input.value.length);
}
function send(inputEl) {
const text = inputEl.value.trim();
if (!text) return;
const id = `msg-${++msgCount}-${Date.now()}`;
if (!inConversation) enterConversation();
addMsg('user', 'You', text);
socket.emit('prompt:send', { message: text, id });
inputEl.value = '';
inputEl.focus();
}
// -----------------------------------------------------------------------
// View Switching
// -----------------------------------------------------------------------
function enterConversation() {
inConversation = true;
$centerCard.style.display = 'none';
$conversation.classList.remove('hidden');
$convInput.focus();
}
// -----------------------------------------------------------------------
// Messages
// -----------------------------------------------------------------------
function addMsg(type, label, body) {
if (!inConversation) enterConversation();
const div = document.createElement('div');
if (type === 'system') {
div.className = 'msg msg-system';
div.textContent = body;
} else {
div.className = 'msg msg-' + type;
const labelEl = document.createElement('div');
labelEl.className = 'msg-label';
labelEl.textContent = label;
div.appendChild(labelEl);
const bodyEl = document.createElement('div');
bodyEl.className = 'msg-body';
bodyEl.textContent = body;
div.appendChild(bodyEl);
}
$messages.appendChild(div);
$messages.scrollTop = $messages.scrollHeight;
}
// -----------------------------------------------------------------------
// Output
// -----------------------------------------------------------------------
function showOutput() { $outputArea.classList.remove('hidden'); }
function appendOutput(text) {
const span = document.createElement('span');
span.textContent = text;
$outputContent.appendChild(span);
$outputContent.scrollTop = $outputContent.scrollHeight;
}
function appendProgressLine(text) {
showOutput();
const line = document.createElement('div');
line.className = 'progress-line';
line.textContent = text;
$outputContent.appendChild(line);
$outputContent.scrollTop = $outputContent.scrollHeight;
}
function renderToolResult(result) {
if (!result) return;
if (result.transcript) {
const preview = document.createElement('div');
preview.className = 'transcript-preview';
preview.textContent = result.transcript.length > 1500
? result.transcript.substring(0, 1500) + '\n\n[truncated]'
: result.transcript;
$outputContent.appendChild(preview);
if (result.downloadUrl) {
const link = document.createElement('a');
link.className = 'download-link';
link.href = result.downloadUrl;
link.download = result.filename || 'transcript.txt';
link.textContent = 'Download Transcript';
$outputContent.appendChild(link);
}
} else if (result.message) {
appendOutput('\n' + result.message + '\n');
}
$outputContent.scrollTop = $outputContent.scrollHeight;
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
function stripAnsi(str) {
return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1B\][^\x07]*\x07/g, '');
}
function iconSvg(name) {
const icons = {
globe: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>',
zap: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>',
box: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>',
};
return icons[name] || '';
}
// -----------------------------------------------------------------------
// Events
// -----------------------------------------------------------------------
$promptInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); send($promptInput); } });
$sendBtn.addEventListener('click', () => send($promptInput));
$convInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); send($convInput); } });
$convSendBtn.addEventListener('click', () => send($convInput));
$cardClose.addEventListener('click', () => { $centerCard.style.display = 'none'; });
$closeOutput.addEventListener('click', () => { $outputArea.classList.add('hidden'); });
$linkTools.addEventListener('click', (e) => {
e.preventDefault();
showOutput();
$outputContent.innerHTML = '';
appendOutput('Registered Tools:\n');
tools.forEach((t, i) => {
appendOutput(` ${i + 1}. ${t.name} ${t.mock ? '[mock]' : '[live]'}\n`);
appendOutput(` ${t.description}\n`);
appendOutput(` Syntax: ${t.syntax}\n\n`);
});
});
$linkBridge.addEventListener('click', (e) => {
e.preventDefault();
openSettings();
});
// -----------------------------------------------------------------------
// Init
// -----------------------------------------------------------------------
connect();
})();