|
|
|
|
|
|
|
|
|
|
|
|
|
|
| (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();
|
|
|
| })();
|
|
|