| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | (function () {
|
| | 'use strict';
|
| |
|
| |
|
| |
|
| |
|
| | 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');
|
| |
|
| |
|
| | 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');
|
| |
|
| |
|
| | 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');
|
| |
|
| |
|
| |
|
| |
|
| | 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 };
|
| |
|
| |
|
| |
|
| |
|
| | 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';
|
| | });
|
| |
|
| |
|
| | 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();
|
| | });
|
| |
|
| |
|
| | socket.on('agent:message', (data) => {
|
| | if (data.from === 'system') {
|
| | addMsg('system', '', data.message);
|
| | } else if (data.from !== 'human') {
|
| | addMsg('agent', 'Agent', data.message);
|
| | }
|
| | });
|
| |
|
| |
|
| | 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`);
|
| | });
|
| |
|
| |
|
| | socket.on('cli:stdout', (data) => {
|
| | showOutput();
|
| | appendOutput(data.data);
|
| |
|
| | 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();
|
| | });
|
| |
|
| |
|
| | socket.on('settings:changed', (data) => {
|
| | savedSettings = data.settings;
|
| | cliInfo = data.cli;
|
| | updateSettingsPanel();
|
| | updateStatusText();
|
| | });
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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(' | ');
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | function openSettings() {
|
| | $settingsPanel.classList.remove('hidden');
|
| | $cliPathInput.value = savedSettings.cliPath || '';
|
| | updateSettingsPanel();
|
| | }
|
| |
|
| | function closeSettings() {
|
| | $settingsPanel.classList.add('hidden');
|
| | }
|
| |
|
| | function updateSettingsPanel() {
|
| |
|
| | 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.';
|
| | }
|
| |
|
| |
|
| | 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.';
|
| | }
|
| |
|
| |
|
| | $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.`);
|
| | });
|
| | });
|
| |
|
| |
|
| | $redetectBtn.addEventListener('click', () => {
|
| | $detectText.textContent = 'Detecting...';
|
| | socket.emit('settings:update', { cliPath: $cliPathInput.value.trim() }, (response) => {
|
| | savedSettings = response.settings;
|
| | cliInfo = response.cli;
|
| | updateSettingsPanel();
|
| | updateStatusText();
|
| | });
|
| | });
|
| |
|
| |
|
| | $connectBtn.addEventListener('click', () => {
|
| | $connectBtn.disabled = true;
|
| | $sessionStatus.textContent = 'Starting...';
|
| | socket.emit('cli:start', { cliPath: savedSettings.cliPath || cliInfo.path });
|
| | });
|
| |
|
| |
|
| | $disconnectBtn.addEventListener('click', () => {
|
| | socket.emit('cli:stop');
|
| | });
|
| |
|
| | $settingsToggle.addEventListener('click', openSettings);
|
| | $settingsClose.addEventListener('click', closeSettings);
|
| |
|
| |
|
| | $settingsPanel.addEventListener('click', (e) => {
|
| | if (e.target === $settingsPanel) closeSettings();
|
| | });
|
| |
|
| |
|
| |
|
| |
|
| | 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);
|
| | });
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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);
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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);
|
| | });
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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();
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | function enterConversation() {
|
| | inConversation = true;
|
| | $centerCard.style.display = 'none';
|
| | $conversation.classList.remove('hidden');
|
| | $convInput.focus();
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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;
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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;
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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] || '';
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | $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();
|
| | });
|
| |
|
| |
|
| |
|
| |
|
| | connect();
|
| |
|
| | })();
|
| |
|