| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> |
| <meta name="mobile-web-app-capable" content="yes"> |
| <meta name="apple-mobile-web-app-capable" content="yes"> |
| <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> |
| <meta name="theme-color" content="#0d1b0f"> |
| <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"> |
| <meta http-equiv="Pragma" content="no-cache"> |
| <meta http-equiv="Expires" content="0"> |
| <title>Grow A Language</title> |
| |
| <link rel="icon" type="image/png" href="/images/0.png" sizes="32x32"> |
|
|
| <link rel="stylesheet" href="style.pixel.css"> |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css" /> |
| <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=Baloo+2:wght@400;600;700&family=Press+Start+2P&display=swap" rel="stylesheet"> |
|
|
|
|
| </head> |
| <body> |
| |
| <div class="ide-titlebar"> |
| <div class="ide-titlebar-left"> |
| <div class="file-menu-wrap" id="file-menu-wrap"> |
| <button class="ide-menu-btn" id="btn-file">File</button> |
| <div class="file-popup hidden" id="file-popup"> |
| <button class="file-popup-item" id="fp-new">📄 New</button> |
| <button class="file-popup-item" id="fp-open">📂 Open</button> |
| <button class="file-popup-item" id="fp-save">💾 Save</button> |
| <div class="file-popup-divider"></div> |
| <button class="file-popup-item danger" id="fp-clear">🗑 Clear</button> |
| </div> |
| </div> |
| </div> |
| <div class="ide-titlebar-center"> |
| <img class="ide-center-logo" src="/images/0.png" alt="Grow A Language"> |
| </div> |
| <div class="ide-titlebar-right"> |
| <div class="mobile-lexeme-nav-toggle"> |
| <button class="btn-toggle-lexemes" id="toggle-mobile-lexemes-nav" aria-expanded="true"> |
| <span id="lexeme-toggle-icon">β²</span> Lexemes |
| </button> |
| </div> |
| <button class="btn btn-chat-toggle" id="btn-chat-toggle" title="AI Chat Helper"> |
| πͺ <span class="chat-btn-text">Ask</span> |
| </button> |
| <div class="run-dropdown"> |
| <button id="btn-run" class="btn btn-primary ide-run-btn" onclick="runCode()" title="Run"> |
| <span class="ide-run-icon">βΆ</span> <span id="run-mode-text">Run</span> |
| </button> |
| <button class="btn btn-dropdown-toggle" onclick="toggleRunDropdown(event)">βΌ</button> |
| <div class="run-dropdown-menu hidden" id="run-dropdown-menu"> |
| <div class="run-dropdown-item" data-mode="lexer" onclick="selectRunMode('lexer')">Lexer</div> |
| <div class="run-dropdown-item" data-mode="syntax" onclick="selectRunMode('syntax')">Syntax</div> |
| <div class="run-dropdown-item" data-mode="semantic" onclick="selectRunMode('semantic')">Semantic</div> |
| <div class="run-dropdown-item" data-mode="run" onclick="selectRunMode('run')">Run</div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="ide-tabbar"> |
| <div class="ide-tab active"> |
| <span class="ide-tab-icon">π</span> |
| <span class="current-file-name" id="current-file-name" title="Current file">untitled.gal</span> |
| <span class="unsaved-dot" id="unsaved-dot" title="Unsaved changes"></span> |
| </div> |
| <div class="ide-tabbar-fill"></div> |
| </div> |
|
|
| |
| <div class="workspace"> |
| <aside class="sidebar" id="sidebar"> |
| <div class="sd-section">LEXEMES</div> |
| <div class="side-tokens"> |
| <table class="side-token-table" aria-label="Lexeme tokens live update"> |
| <thead> |
| <tr> |
| <th>LEXEME</th> |
| <th>TOKEN</th> |
| <th>TYPE</th> |
| </tr> |
| </thead> |
| <tbody id="tokenBodySide"></tbody> |
| </table> |
| </div> |
| </aside> |
|
|
| <div class="sidebarResizer" title="Drag to resize lexeme panel"></div> |
|
|
| <div class="workspace-main"> |
| <div class="mainCont"> |
| <div class="textFieldCont"> |
| <div id="editor" style="width: 100%; height: 100%;"></div> |
| </div> |
| <div class="widthResizer"></div> |
| <div class="outputCont"> |
| <table class="tokenTable"> |
| <thead> |
| <tr> |
| <th>LEXEME</th> |
| <th>TOKEN</th> |
| <th>TYPE</th> |
| </tr> |
| </thead> |
| <tbody id="tokenBody"> |
| </tbody> |
| </table> |
| </div> |
| </div> |
|
|
| <div class="heightResizer" title="Drag to resize terminal"></div> |
|
|
| <div class="terminalCont"> |
| <div class="terminal-header"> |
| <div class="term-title"> |
| <span class="dot dot-green" aria-hidden="true"></span> |
| OUTPUT |
| </div> |
| <div class="term-actions"> |
| <button class="term-btn" id="term-autoscroll" aria-pressed="true" title="Toggle auto-scroll">Auto</button> |
| <button class="term-btn" id="term-font-dec" title="Decrease font size">-</button> |
| <button class="term-btn" id="term-font-inc" title="Increase font size">+</button> |
| <button class="term-btn" id="term-copy" title="Copy output">Copy</button> |
| <button class="term-btn danger" id="term-clear" title="Clear output">Clear</button> |
| </div> |
| </div> |
| |
| <div class="terminal-body"> |
| <div id="terminal"></div> |
| </div> |
| </div> |
|
|
| |
| <div class="mobile-tokens-backdrop" id="mobile-tokens-backdrop"></div> |
|
|
| |
| <div class="mobile-tokens" aria-label="Lexemes (mobile)"> |
| <div class="mobile-tokens-header"> |
| <h3 class="mobile-tokens-title">LEXEME TABLE</h3> |
| <button class="mobile-tokens-close" id="mobile-tokens-close" aria-label="Close lexeme table">β</button> |
| </div> |
| <table class="tokenTable"> |
| <thead> |
| <tr> |
| <th>LEXEME</th> |
| <th>TOKEN</th> |
| <th>TYPE</th> |
| </tr> |
| </thead> |
| <tbody id="tokenBodyMobile"></tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
|
|
| |
|
|
| |
| <div class="ide-statusbar"> |
| <div class="ide-statusbar-left" role="status" aria-label="Analysis status"> |
| <div class="status-chip" id="status-lex">Lexical: β</div> |
| <div class="status-chip" id="status-syn">Syntax: β</div> |
| <div class="status-chip" id="status-sem">Semantic: β</div> |
| <div class="status-chip" id="status-exe">Execution: β</div> |
| </div> |
| <div class="ide-statusbar-right"> |
| <span class="ide-status-lang">GAL</span> |
| </div> |
| </div> |
|
|
| |
| <button class="mobile-status-fab" id="mobile-status-fab" aria-label="Show status"> |
| <span class="mobile-status-fab-icon">β</span> |
| </button> |
| <div class="mobile-status-backdrop" id="mobile-status-backdrop"></div> |
| <div class="mobile-status-popup" id="mobile-status-popup"> |
| <div class="mobile-status-header"> |
| <h3 class="mobile-status-title">STATUS</h3> |
| <button class="mobile-status-close" id="mobile-status-close" aria-label="Close status">β</button> |
| </div> |
| <div class="mobile-status-body"> |
| <div class="mobile-status-chip" data-mirror="status-lex">Lexical: β</div> |
| <div class="mobile-status-chip" data-mirror="status-syn">Syntax: β</div> |
| <div class="mobile-status-chip" data-mirror="status-sem">Semantic: β</div> |
| <div class="mobile-status-chip" data-mirror="status-exe">Execution: β</div> |
| </div> |
| </div> |
|
|
| |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script> |
|
|
| <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.min.js"></script> |
|
|
| <script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.43.0/min/vs/loader.js"></script> |
|
|
| |
| <script src="main.js?v=20260331a"></script> |
|
|
| |
| <script> |
| (function() { |
| var API = (location.port && location.port !== '5000') ? 'http://localhost:5000' : ''; |
| var _inlineSock = null; |
| |
| function getSock() { |
| if (window._galSocket && window._galSocket.connected) return window._galSocket; |
| if (_inlineSock && _inlineSock.connected) return _inlineSock; |
| try { |
| _inlineSock = io(API || window.location.origin, { |
| transports: ['websocket','polling'], |
| reconnection: true |
| }); |
| console.log('[GAL] Created inline socket'); |
| } catch(e) { console.error('[GAL] Socket create error:', e); } |
| return _inlineSock; |
| } |
| |
| function tw(msg) { |
| var t = window._galTerm; |
| if (t && t.write) { t.write(msg); return; } |
| var el = document.getElementById('terminal'); |
| if (el) { |
| var span = document.createElement('span'); |
| span.textContent = msg.replace(/\r\n/g, '\n'); |
| span.style.cssText = 'white-space:pre-wrap;color:#b6ffb6;font-family:monospace'; |
| el.appendChild(span); |
| } |
| } |
| |
| function setChip(id, cls, txt) { |
| var el = document.getElementById(id); |
| if (!el) return; |
| el.classList.remove('ok','err'); |
| if (cls) el.classList.add(cls); |
| el.textContent = txt; |
| } |
| function resetChips() { |
| setChip('status-lex','','Lexical: \u2014'); |
| setChip('status-syn','','Syntax: \u2014'); |
| setChip('status-sem','','Semantic: \u2014'); |
| setChip('status-exe','','Execution: \u2014'); |
| } |
| function applyChips(stage, ok) { |
| if (stage==='lexical') { setChip('status-lex','err','Lexical: Error'); } |
| else if (stage==='syntax') { setChip('status-lex','ok','Lexical: OK'); setChip('status-syn','err','Syntax: Error'); } |
| else if (stage==='semantic') { setChip('status-lex','ok','Lexical: OK'); setChip('status-syn','ok','Syntax: OK'); setChip('status-sem','err','Semantic: Error'); } |
| else if (stage==='execution') { setChip('status-lex','ok','Lexical: OK'); setChip('status-syn','ok','Syntax: OK'); setChip('status-sem','ok','Semantic: OK'); setChip('status-exe', ok?'ok':'err', ok?'Execution: OK':'Execution: Error'); } |
| } |
| |
| |
| |
| |
| |
| function install() { |
| window.runCode = inlineRunCode; |
| console.log('[GAL] Inline run override v4 installed'); |
| } |
| |
| install(); |
| |
| var _rc = setInterval(function() { |
| if (window.runCode !== inlineRunCode) { |
| window.runCode = inlineRunCode; |
| console.log('[GAL] Re-installed inline run override'); |
| } |
| }, 500); |
| setTimeout(function() { clearInterval(_rc); }, 30000); |
| |
| |
| |
| |
| |
| function updateRunBtnState() { |
| var btn = document.getElementById('btn-run'); |
| if (!btn) return; |
| var code = ''; |
| try { code = monaco.editor.getModels()[0].getValue(); } catch(e) { return; } |
| var isEmpty = (code || '').trim() === ''; |
| btn.disabled = isEmpty; |
| btn.title = isEmpty ? 'Editor is empty β nothing to run' : 'Run'; |
| btn.style.opacity = isEmpty ? '0.45' : ''; |
| btn.style.cursor = isEmpty ? 'not-allowed' : ''; |
| } |
| var _btnWatch = setInterval(function() { |
| try { |
| var model = monaco.editor.getModels()[0]; |
| if (!model) return; |
| clearInterval(_btnWatch); |
| model.onDidChangeContent(updateRunBtnState); |
| updateRunBtnState(); |
| } catch(e) { } |
| }, 200); |
| setTimeout(function() { clearInterval(_btnWatch); }, 30000); |
| |
| async function inlineRunCode() { |
| |
| try { |
| var _src = monaco.editor.getModels()[0].getValue(); |
| if (!_src || _src.trim() === '') { |
| var t = window._galTerm; |
| if (t) { t.write('\x1b[2J\x1b[3J\x1b[H'); t.write('\x1b[33mEditor is empty β nothing to run.\x1b[0m\r\n'); } |
| return; |
| } |
| } catch(e) { } |
| |
| var modeEl = document.getElementById('run-mode-text'); |
| var modeText = modeEl ? modeEl.textContent : ''; |
| var term = window._galTerm; |
| |
| |
| if (modeText.indexOf('Lexer') !== -1) { |
| if (term) { term.write('\x1b[2J\x1b[3J\x1b[H'); } |
| resetChips(); |
| if (window._clearErrorHighlights) window._clearErrorHighlights(); |
| if (window.runLexer) return window.runLexer({ silent: false }); |
| console.warn('[GAL] runLexer not ready yet'); |
| return; |
| } |
| if (modeText.indexOf('Syntax') !== -1) { |
| if (term) { term.write('\x1b[2J\x1b[3J\x1b[H'); } |
| resetChips(); |
| if (window._clearErrorHighlights) window._clearErrorHighlights(); |
| if (window.runSyntax) return window.runSyntax({ silent: false }); |
| console.warn('[GAL] runSyntax not ready yet'); |
| return; |
| } |
| if (modeText.indexOf('Semantic') !== -1) { |
| if (term) { term.write('\x1b[2J\x1b[3J\x1b[H'); } |
| resetChips(); |
| if (window._clearErrorHighlights) window._clearErrorHighlights(); |
| if (window.runSemantic) return window.runSemantic({ silent: false }); |
| console.warn('[GAL] runSemantic not ready yet'); |
| return; |
| } |
| |
| var code = ''; |
| try { code = monaco.editor.getModels()[0].getValue(); } |
| catch(e) { tw('Error: Editor not ready.\r\n'); return; } |
| |
| if (term) { term.write('\x1b[2J\x1b[3J\x1b[H'); } |
| window._needsNewline = false; |
| resetChips(); |
| |
| if (window._clearErrorHighlights) window._clearErrorHighlights(); |
| |
| var sock0 = window._galSocket; |
| if (sock0) sock0.removeAllListeners('execution_complete'); |
| |
| |
| if (window.runLexer) await window.runLexer({ silent: true }); |
| |
| var hasWater = /\bwater\s*\(/.test(code); |
| |
| if (!hasWater) { |
| |
| try { |
| var resp = await fetch(API + '/api/run', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ source_code: code }) |
| }); |
| var result = await resp.json(); |
| if (result.output && result.output.length > 0) { |
| result.output.forEach(function(line) { tw(String(line) + '\r\n'); }); |
| } |
| if (result.errors && result.errors.length > 0) { |
| result.errors.forEach(function(err) { tw('\x1b[31m' + String(err) + '\x1b[0m\r\n'); }); |
| if (window._highlightErrors) window._highlightErrors(result.errors); |
| } else { |
| if (window._clearErrorHighlights) window._clearErrorHighlights(); |
| } |
| applyChips(result.stage, result.success); |
| if (result.stage === 'execution') { |
| tw(result.success ? '\x1b[32mCode execution successful.\x1b[0m\r\n' : '\x1b[31mCode execution failed.\x1b[0m\r\n'); |
| } |
| } catch(err) { |
| console.error('[GAL] REST error:', err); |
| tw('\x1b[31mError: Could not connect to server.\x1b[0m\r\n'); |
| } |
| } else { |
| |
| var sock = getSock(); |
| if (!sock) { tw('\x1b[31mError: Socket not available.\x1b[0m\r\n'); return; } |
| |
| |
| if (window._resetInputState) window._resetInputState(); |
| |
| window._runGeneration = (window._runGeneration || 0) + 1; |
| if (window._socketOutputLog) window._socketOutputLog.length = 0; |
| |
| sock.removeAllListeners('execution_complete'); |
| |
| |
| if (!sock.connected) { |
| sock.connect(); |
| await new Promise(function(resolve) { |
| if (sock.connected) return resolve(); |
| sock.once('connect', resolve); |
| setTimeout(resolve, 4000); |
| }); |
| } |
| if (!sock.connected) { tw('\x1b[31mError: Could not connect.\x1b[0m\r\n'); return; } |
| |
| |
| if (window._socketOutputLog) window._socketOutputLog.length = 0; |
| |
| |
| sock.once('execution_complete', function(data) { |
| applyChips(data.stage, data.success); |
| if (data.stage === 'execution') { |
| tw(data.success ? '\r\n\x1b[32mCode execution successful.\x1b[0m\r\n' : '\r\n\x1b[31mCode execution failed.\x1b[0m\r\n'); |
| } |
| |
| if (!data.success && window._socketOutputLog && window._socketOutputLog.length > 0) { |
| if (window._highlightErrors) window._highlightErrors(window._socketOutputLog); |
| } else { |
| if (window._clearErrorHighlights) window._clearErrorHighlights(); |
| } |
| }); |
| |
| |
| sock.emit('run_code', { source_code: code }); |
| } |
| } |
| })(); |
| </script> |
|
|
| |
| <script> |
| (function() { |
| function wireFileMenu() { |
| var btnFile = document.getElementById('btn-file'); |
| var popup = document.getElementById('file-popup'); |
| var fpNew = document.getElementById('fp-new'); |
| var fpOpen = document.getElementById('fp-open'); |
| var fpSave = document.getElementById('fp-save'); |
| var fpClear = document.getElementById('fp-clear'); |
| if (!btnFile || !popup || btnFile._galWired) return; |
| btnFile._galWired = true; |
| |
| function closePopup() { popup.classList.add('hidden'); } |
| function togglePopup(e) { e.stopPropagation(); popup.classList.toggle('hidden'); } |
| |
| btnFile.onclick = togglePopup; |
| document.addEventListener('click', function(e) { |
| if (!popup.contains(e.target) && e.target !== btnFile) closePopup(); |
| }); |
| |
| var fileLabel = document.getElementById('current-file-name'); |
| var unsavedDot = document.getElementById('unsaved-dot'); |
| window._galFileHandle = null; |
| window._galDirty = false; |
| function setFileName(name) { |
| window._currentGalFile = name; |
| if (fileLabel) { fileLabel.textContent = name; fileLabel.title = name; } |
| } |
| function markDirty() { |
| if (!window._galDirty) { |
| window._galDirty = true; |
| if (unsavedDot) unsavedDot.classList.add('visible'); |
| } |
| } |
| function markClean() { |
| window._galDirty = false; |
| if (unsavedDot) unsavedDot.classList.remove('visible'); |
| } |
| window.markDirty = markDirty; |
| window.markClean = markClean; |
| |
| |
| fpNew.onclick = function() { |
| closePopup(); |
| if (window.editor && window.editor.getValue && window.editor.getValue().trim()) { |
| if (!confirm('Clear current code and start new?')) return; |
| } |
| window.location.reload(); |
| }; |
| |
| |
| fpOpen.onclick = async function() { |
| closePopup(); |
| |
| function clearOnOpen() { |
| var t = window._galTerm; |
| if (t) { t.reset(); t.clear(); } |
| if (window._clearErrorHighlights) window._clearErrorHighlights(); |
| var tbody = document.getElementById('tokenBody'); |
| if (tbody) tbody.innerHTML = ''; |
| var tbodySide = document.getElementById('tokenBodySide'); |
| if (tbodySide) tbodySide.innerHTML = ''; |
| ['status-lex','status-syn','status-sem','status-exe'].forEach(function(id) { |
| var el = document.getElementById(id); |
| if (el) { el.classList.remove('ok','err'); el.textContent = el.textContent.replace(/:.*/, ': \u2014'); } |
| }); |
| } |
| |
| |
| if (window.showOpenFilePicker) { |
| try { |
| var handles = await window.showOpenFilePicker({ |
| types: [{ description: 'GAL Source File', accept: { 'text/plain': ['.gal'] } }], |
| multiple: false |
| }); |
| var handle = handles[0]; |
| var file = await handle.getFile(); |
| var text = await file.text(); |
| window._galFileHandle = handle; |
| setFileName(file.name); |
| if (window.editor) window.editor.setValue(text); |
| clearOnOpen(); |
| markClean(); |
| return; |
| } catch (e) { |
| if (e.name === 'AbortError') return; |
| console.warn('[GAL] Open picker failed, using fallback:', e); |
| } |
| } |
| |
| var input = document.createElement('input'); |
| input.type = 'file'; |
| input.accept = '.gal'; |
| input.onchange = function() { |
| var file = input.files[0]; |
| if (!file) return; |
| window._galFileHandle = null; |
| setFileName(file.name); |
| var reader = new FileReader(); |
| reader.onload = function() { |
| if (window.editor) window.editor.setValue(reader.result); |
| clearOnOpen(); |
| markClean(); |
| }; |
| reader.readAsText(file); |
| }; |
| input.click(); |
| }; |
| |
| |
| fpSave.onclick = async function() { |
| closePopup(); |
| var code = ''; |
| if (window.editor && window.editor.getValue) code = window.editor.getValue(); |
| if (!code) { alert('Editor is empty.'); return; } |
| |
| |
| if (window._galFileHandle) { |
| try { |
| var writable = await window._galFileHandle.createWritable(); |
| await writable.write(code); |
| await writable.close(); |
| console.log('[GAL] Saved to', window._currentGalFile); |
| markClean(); |
| return; |
| } catch (e) { |
| if (e.name === 'AbortError') return; |
| console.warn('[GAL] Direct save failed, falling through to Save As:', e); |
| } |
| } |
| |
| |
| if (window.showSaveFilePicker) { |
| try { |
| var handle = await window.showSaveFilePicker({ |
| suggestedName: window._currentGalFile || 'untitled.gal', |
| types: [{ description: 'GAL Source File', accept: { 'text/plain': ['.gal'] } }] |
| }); |
| window._galFileHandle = handle; |
| setFileName(handle.name); |
| var writable = await handle.createWritable(); |
| await writable.write(code); |
| await writable.close(); |
| markClean(); |
| return; |
| } catch (e) { |
| if (e.name === 'AbortError') return; |
| } |
| } |
| var name = prompt('Save file as:', 'untitled.gal'); |
| if (!name) return; |
| if (!name.endsWith('.gal')) name += '.gal'; |
| var blob = new Blob([code], { type: 'text/plain' }); |
| var url = URL.createObjectURL(blob); |
| var a = document.createElement('a'); |
| a.href = url; a.download = name; |
| document.body.appendChild(a); a.click(); |
| document.body.removeChild(a); |
| URL.revokeObjectURL(url); |
| markClean(); |
| }; |
| |
| |
| fpClear.onclick = function() { |
| closePopup(); |
| if (window.editor) window.editor.setValue(''); |
| if (window._clearErrorHighlights) window._clearErrorHighlights(); |
| var t = window._galTerm; |
| if (t) { t.reset(); t.clear(); } |
| var tbody = document.getElementById('tokenBody'); |
| if (tbody) tbody.innerHTML = ''; |
| var tbodySide = document.getElementById('tokenBodySide'); |
| if (tbodySide) tbodySide.innerHTML = ''; |
| ['status-lex','status-syn','status-sem','status-exe'].forEach(function(id) { |
| var el = document.getElementById(id); |
| if (el) { el.classList.remove('ok','err'); el.textContent = el.textContent.replace(/:.*/, ': \u2014'); } |
| }); |
| }; |
| |
| console.log('[GAL] File menu wired'); |
| } |
| |
| wireFileMenu(); |
| document.addEventListener('DOMContentLoaded', wireFileMenu); |
| setTimeout(wireFileMenu, 1000); |
| setTimeout(wireFileMenu, 3000); |
| })(); |
| </script> |
|
|
| |
| <div class="chat-panel" id="chat-panel"> |
| <div class="chat-header"> |
| <div class="chat-header-left"> |
| <span class="chat-title">π± GAL AI Assistant</span> |
| <span class="chat-subtitle">Compiler-aware assistant Β· Ready</span> |
| </div> |
| <div class="chat-header-actions"> |
| <button class="chat-header-btn" id="chat-clear" title="Clear chat">π</button> |
| <button class="chat-header-btn" id="chat-close" title="Close chat">β</button> |
| </div> |
| </div> |
| <div class="chat-messages" id="chat-messages"> |
| |
| <div class="chat-empty-state" id="chat-empty-state"> |
| <div class="chat-watermark"><img src="/images/0.png" alt="GAL" class="chat-watermark-logo"></div> |
| <div class="chat-welcome-title">Hi! I'm your GAL AI Assistant.</div> |
| <div class="chat-welcome-sub">Ask me about:</div> |
| <div class="chat-welcome-topics"> |
| <span>π± GAL syntax</span> |
| <span>π GAL Reserved words</span> |
| <span>βοΈ Code writing</span> |
| <span>π Semantic issues</span> |
| </div> |
| <div class="chat-suggestion-chips" id="chat-suggestion-chips"> |
| <button class="chat-chip" data-msg="What is GAL?">What is GAL?</button> |
| <button class="chat-chip" data-msg="What are the reserved words of GAL?">What are the reserved words of GAL?</button> |
| <button class="chat-chip" data-msg="Create a switch statement">Create a switch statement</button> |
| <button class="chat-chip" data-msg="Generate a GAL loop">Generate a GAL loop</button> |
| </div> |
| </div> |
| </div> |
| <div class="chat-input-area"> |
| <div class="chat-context-hint" id="chat-context-hint"> |
| <label><input type="checkbox" id="chat-include-code" checked> Include editor code as context</label> |
| </div> |
| <div class="chat-input-row"> |
| <div class="chat-input-wrap"> |
| <span class="chat-input-icon">π±</span> |
| <textarea class="chat-input" id="chat-input" placeholder="Ask about GAL..." rows="1"></textarea> |
| </div> |
| <button class="chat-send-btn" id="chat-send" title="Send"> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg> |
| </button> |
| </div> |
| </div> |
| </div> |
| <div class="chat-backdrop" id="chat-backdrop"></div> |
|
|
| |
| <script> |
| (function() { |
| var API = (location.port && location.port !== '5000') ? 'http://localhost:5000' : ''; |
| var chatPanel = document.getElementById('chat-panel'); |
| var chatBackdrop = document.getElementById('chat-backdrop'); |
| var chatMessages = document.getElementById('chat-messages'); |
| var chatInput = document.getElementById('chat-input'); |
| var chatSendBtn = document.getElementById('chat-send'); |
| var chatToggleBtn = document.getElementById('btn-chat-toggle'); |
| var chatCloseBtn = document.getElementById('chat-close'); |
| var chatClearBtn = document.getElementById('chat-clear'); |
| var chatIncludeCode = document.getElementById('chat-include-code'); |
| var chatSessionId = 'session_' + Date.now(); |
| var chatBusy = false; |
| |
| function hideEmptyState() { |
| var es = document.getElementById('chat-empty-state'); |
| if (es) es.remove(); |
| } |
| |
| function bindChips() { |
| var chips = document.querySelectorAll('.chat-chip'); |
| chips.forEach(function(chip) { |
| chip.addEventListener('click', function() { |
| if (chatBusy) return; |
| chatInput.value = this.getAttribute('data-msg'); |
| sendMessage(); |
| }); |
| }); |
| } |
| bindChips(); |
| |
| function openChat() { |
| chatPanel.classList.add('open'); |
| chatBackdrop.classList.add('show'); |
| setTimeout(function() { chatInput.focus(); }, 300); |
| } |
| function closeChat() { |
| chatPanel.classList.remove('open'); |
| chatBackdrop.classList.remove('show'); |
| } |
| |
| if (chatToggleBtn) chatToggleBtn.addEventListener('click', function() { |
| chatPanel.classList.contains('open') ? closeChat() : openChat(); |
| }); |
| if (chatCloseBtn) chatCloseBtn.addEventListener('click', closeChat); |
| if (chatBackdrop) chatBackdrop.addEventListener('click', closeChat); |
| |
| function escapeHtml(str) { |
| var div = document.createElement('div'); |
| div.textContent = str; |
| return div.innerHTML; |
| } |
| |
| function formatReply(text) { |
| |
| var codeBlocks = []; |
| text = text.replace(/```(\w*)\n([\s\S]*?)```/g, function(_, lang, code) { |
| var label = lang || 'code'; |
| var ph = '\x00CB' + codeBlocks.length + '\x00'; |
| codeBlocks.push('<div class="code-block-wrap"><div class="code-block-header"><span class="code-block-lang">' + escapeHtml(label) + '</span><button class="chat-copy-btn" onclick="copyCodeBlock(this)">Copy</button></div><pre><code>' + escapeHtml(code.trim()) + '</code></pre></div>'); |
| return ph; |
| }); |
| |
| text = text.replace(/`([^`]+)`/g, '<code>$1</code>'); |
| |
| text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>'); |
| |
| text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>'); |
| |
| text = text.replace(/\n/g, '<br>'); |
| |
| for (var ci = 0; ci < codeBlocks.length; ci++) { |
| text = text.replace('\x00CB' + ci + '\x00', codeBlocks[ci]); |
| } |
| return text; |
| } |
| |
| |
| function detectTags(text) { |
| var tags = []; |
| var lower = text.toLowerCase(); |
| if (/\bsyntax\b/.test(lower) || /\bparse\b/.test(lower) || /\bexpected\b.*\bgot\b/.test(lower)) |
| tags.push({ label: 'Syntax', cls: 'tag-syntax' }); |
| if (/\bsemantic\b/.test(lower) || /\bundeclared\b/.test(lower) || /\btype\s*(mis)?match\b/.test(lower)) |
| tags.push({ label: 'Semantic', cls: 'tag-semantic' }); |
| if (/```/.test(text)) |
| tags.push({ label: 'Code Example', cls: 'tag-code' }); |
| if (/\bfix\b/.test(lower) || /\bsolution\b/.test(lower) || /\binstead\b/.test(lower) || /\bcorrect\b/.test(lower)) |
| tags.push({ label: 'Fix Suggestion', cls: 'tag-fix' }); |
| if (tags.length === 0) |
| tags.push({ label: 'Info', cls: 'tag-info' }); |
| return tags; |
| } |
| |
| function buildTagsHtml(tags) { |
| return '<div class="chat-msg-tags">' + tags.map(function(t) { |
| return '<span class="chat-tag ' + t.cls + '">' + t.label + '</span>'; |
| }).join('') + '</div>'; |
| } |
| |
| function buildActionsHtml() { |
| return '<div class="chat-actions">' + |
| '<button class="chat-action-btn" onclick="chatActionInsert(this)" title="Insert code into editor"><span class="action-icon">π</span>Insert into editor</button>' + |
| '</div>'; |
| } |
| |
| function addMessage(role, html) { |
| hideEmptyState(); |
| var div = document.createElement('div'); |
| div.className = 'chat-msg ' + role; |
| var avatar = document.createElement('div'); |
| avatar.className = 'chat-avatar'; |
| avatar.textContent = role === 'user' ? 'π€' : 'πΏ'; |
| var bubble = document.createElement('div'); |
| bubble.className = 'chat-bubble'; |
| bubble.innerHTML = html; |
| div.appendChild(avatar); |
| div.appendChild(bubble); |
| chatMessages.appendChild(div); |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| return div; |
| } |
| |
| |
| function typewriterMessage(rawText, onDone, prefixHtml) { |
| var div = document.createElement('div'); |
| div.className = 'chat-msg assistant'; |
| var avatar = document.createElement('div'); |
| avatar.className = 'chat-avatar'; |
| avatar.textContent = 'πΏ'; |
| var bubble = document.createElement('div'); |
| bubble.className = 'chat-bubble'; |
| |
| |
| if (prefixHtml) { |
| var prefixDiv = document.createElement('div'); |
| prefixDiv.innerHTML = prefixHtml; |
| while (prefixDiv.firstChild) bubble.appendChild(prefixDiv.firstChild); |
| } |
| |
| |
| var tags = detectTags(rawText); |
| var tagsDiv = document.createElement('div'); |
| tagsDiv.innerHTML = buildTagsHtml(tags); |
| while (tagsDiv.firstChild) bubble.appendChild(tagsDiv.firstChild); |
| |
| var contentWrap = document.createElement('div'); |
| contentWrap.className = 'chat-bubble-content'; |
| bubble.appendChild(contentWrap); |
| |
| div.appendChild(avatar); |
| div.appendChild(bubble); |
| chatMessages.appendChild(div); |
| |
| |
| var chunks = []; |
| var remaining = rawText; |
| |
| var codeBlockRe = /```[\s\S]*?```/g; |
| var lastIdx = 0; |
| var m; |
| while ((m = codeBlockRe.exec(remaining)) !== null) { |
| if (m.index > lastIdx) { |
| var before = remaining.slice(lastIdx, m.index); |
| before.split(/(\s+)/).forEach(function(w) { if (w) chunks.push(w); }); |
| } |
| chunks.push(m[0]); |
| lastIdx = m.index + m[0].length; |
| } |
| if (lastIdx < remaining.length) { |
| remaining.slice(lastIdx).split(/(\s+)/).forEach(function(w) { if (w) chunks.push(w); }); |
| } |
| |
| var i = 0; |
| var accumulated = ''; |
| var speed = 18; |
| |
| function tick() { |
| if (i >= chunks.length) { |
| contentWrap.innerHTML = formatReply(accumulated); |
| |
| var actDiv = document.createElement('div'); |
| actDiv.innerHTML = buildActionsHtml(); |
| while (actDiv.firstChild) bubble.appendChild(actDiv.firstChild); |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| if (onDone) onDone(); |
| return; |
| } |
| accumulated += chunks[i]; |
| i++; |
| |
| |
| var batch = 2; |
| while (batch-- > 0 && i < chunks.length && !/```/.test(chunks[i])) { |
| accumulated += chunks[i]; |
| i++; |
| } |
| |
| contentWrap.innerHTML = formatReply(accumulated); |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| requestAnimationFrame(function() { setTimeout(tick, speed); }); |
| } |
| tick(); |
| return div; |
| } |
| |
| function addTyping() { |
| var div = document.createElement('div'); |
| div.className = 'chat-msg assistant'; |
| div.id = 'chat-typing-indicator'; |
| div.innerHTML = '<div class="chat-avatar">πΏ</div>' |
| + '<div class="chat-thinking">' |
| + '<div class="chat-thinking-status" id="think-status">' |
| + '<span class="think-icon"></span>' |
| + '<span class="think-text">Thinking...</span>' |
| + '</div>' |
| + '<div class="chat-thinking-progress"><div class="chat-thinking-progress-bar"></div></div>' |
| + '</div>'; |
| chatMessages.appendChild(div); |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| |
| |
| setTimeout(function() { |
| var statusEl = document.getElementById('think-status'); |
| if (statusEl) { |
| var textEl = statusEl.querySelector('.think-text'); |
| if (textEl) { |
| statusEl.classList.add('fading'); |
| setTimeout(function() { |
| textEl.textContent = 'Analyzing your question...'; |
| statusEl.classList.remove('fading'); |
| }, 300); |
| } |
| } |
| }, 1200); |
| |
| |
| setTimeout(function() { |
| var statusEl = document.getElementById('think-status'); |
| if (statusEl) { |
| var textEl = statusEl.querySelector('.think-text'); |
| if (textEl) { |
| statusEl.classList.add('fading'); |
| setTimeout(function() { |
| textEl.textContent = 'Preparing response...'; |
| statusEl.classList.remove('fading'); |
| }, 300); |
| } |
| } |
| }, 2200); |
| } |
| function removeTyping() { |
| var el = document.getElementById('chat-typing-indicator'); |
| if (el) el.remove(); |
| } |
| |
| function setChatBusy(busy) { |
| chatBusy = busy; |
| chatSendBtn.disabled = busy; |
| chatMessages.querySelectorAll('.chat-retry-btn').forEach(function(btn) { |
| btn.disabled = busy; |
| }); |
| if (!busy) chatInput.focus(); |
| } |
| |
| function addFailureMessage(errorText, requestBody) { |
| var errorHtml = '<div class="chat-error-content">' |
| + '<em class="chat-error-text">' + escapeHtml(errorText) + '</em>' |
| + '<button class="chat-retry-btn" type="button" title="Retry message" aria-label="Retry message" disabled>' |
| + '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7"></path><path d="M3 4v6h6"></path></svg>' |
| + '<span>Retry</span></button></div>'; |
| var errorMessage = addMessage('assistant', errorHtml); |
| errorMessage.classList.add('chat-msg-error'); |
| var retryBody = { |
| message: requestBody.message, |
| session_id: requestBody.session_id, |
| editor_code: requestBody.editor_code || '' |
| }; |
| var retryButton = errorMessage.querySelector('.chat-retry-btn'); |
| retryButton.addEventListener('click', function() { |
| if (chatBusy) return; |
| errorMessage.remove(); |
| submitChatRequest(retryBody); |
| }); |
| } |
| |
| |
| var _thinkStart = 0; |
| var THINK_MIN_MS = 1500; |
| |
| async function submitChatRequest(body) { |
| setChatBusy(true); |
| _thinkStart = Date.now(); |
| addTyping(); |
| |
| try { |
| var resp = await fetch(API + '/api/chat', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(body) |
| }); |
| var data = await resp.json(); |
| |
| |
| var elapsed = Date.now() - _thinkStart; |
| var wait = Math.max(0, THINK_MIN_MS - elapsed); |
| await new Promise(function(r) { setTimeout(r, wait); }); |
| |
| removeTyping(); |
| |
| var subtitle = document.querySelector('.chat-subtitle'); |
| if (data.mode === 'fallback') { |
| if (subtitle) subtitle.textContent = 'Offline mode Β· Rule-based'; |
| if (subtitle) subtitle.style.color = '#f5c842'; |
| } else { |
| if (subtitle) subtitle.textContent = 'Compiler-aware assistant Β· Ready'; |
| if (subtitle) subtitle.style.color = ''; |
| } |
| if (data.error) { |
| addFailureMessage(data.error, body); |
| setChatBusy(false); |
| } else { |
| var modePrefix = data.mode === 'fallback' |
| ? '<div class="chat-fallback-badge">β Offline mode β AI unavailable, using built-in knowledge base</div>' |
| : ''; |
| typewriterMessage(data.reply || 'No response.', function() { |
| setChatBusy(false); |
| }, modePrefix); |
| } |
| } catch(err) { |
| removeTyping(); |
| addFailureMessage('Could not connect to server.', body); |
| setChatBusy(false); |
| } |
| } |
| |
| function sendMessage() { |
| if (chatBusy) return; |
| var msg = chatInput.value.trim(); |
| if (!msg) return; |
| |
| chatInput.value = ''; |
| chatInput.style.height = 'auto'; |
| addMessage('user', escapeHtml(msg)); |
| |
| var body = { |
| message: msg, |
| session_id: chatSessionId, |
| editor_code: '' |
| }; |
| if (chatIncludeCode && chatIncludeCode.checked && window.editor) { |
| try { body.editor_code = window.editor.getValue(); } catch(e) {} |
| } |
| submitChatRequest(body); |
| } |
| |
| if (chatSendBtn) chatSendBtn.addEventListener('click', sendMessage); |
| if (chatInput) { |
| chatInput.addEventListener('keydown', function(e) { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| sendMessage(); |
| } |
| }); |
| |
| chatInput.addEventListener('input', function() { |
| this.style.height = 'auto'; |
| this.style.height = Math.min(this.scrollHeight, 120) + 'px'; |
| }); |
| } |
| |
| |
| if (chatClearBtn) chatClearBtn.addEventListener('click', async function() { |
| |
| chatMessages.innerHTML = document.getElementById('chat-panel').querySelector('.chat-empty-state') ? '' : ''; |
| |
| var emptyState = document.createElement('div'); |
| emptyState.className = 'chat-empty-state'; |
| emptyState.id = 'chat-empty-state'; |
| emptyState.innerHTML = '<div class="chat-watermark"><img src="/images/0.png" alt="GAL" class="chat-watermark-logo"></div><div class="chat-welcome-title">Hi! I\'m your GAL AI Assistant.</div><div class="chat-welcome-sub">Ask me about:</div><div class="chat-welcome-topics"><span>π± GAL syntax</span><span>π Debugging errors</span><span>βοΈ Code writing</span><span>π Semantic issues</span></div><div class="chat-suggestion-chips" id="chat-suggestion-chips"><button class="chat-chip" data-msg="What is GAL?">What is GAL?</button><button class="chat-chip" data-msg="What are the reserved words of GAL?">What are the reserved words of GAL?</button><button class="chat-chip" data-msg="Create a switch statement">Create a switch statement</button><button class="chat-chip" data-msg="Generate a GAL loop">Generate a GAL loop</button></div>'; |
| chatMessages.appendChild(emptyState); |
| bindChips(); |
| |
| chatSessionId = 'session_' + Date.now(); |
| try { |
| await fetch(API + '/api/chat/clear', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ session_id: chatSessionId }) |
| }); |
| } catch(e) {} |
| }); |
| })(); |
| |
| |
| function copyCodeBlock(btn) { |
| var wrap = btn.closest('.code-block-wrap'); |
| var pre = wrap ? wrap.querySelector('pre') : btn.parentElement.querySelector('pre'); |
| if (!pre) return; |
| var text = pre.textContent || pre.innerText; |
| navigator.clipboard.writeText(text).then(function() { |
| btn.textContent = 'Copied!'; |
| btn.classList.add('copied'); |
| setTimeout(function() { |
| btn.textContent = 'Copy'; |
| btn.classList.remove('copied'); |
| }, 1500); |
| }); |
| } |
| |
| |
| function chatActionInsert(btn) { |
| |
| var msg = btn.closest('.chat-msg'); |
| if (!msg || !window.editor) return; |
| var codeEls = msg.querySelectorAll('pre code'); |
| if (codeEls.length === 0) return; |
| |
| var code = Array.from(codeEls).map(function(el) { return el.textContent; }).join('\n\n'); |
| var cur = window.editor.getValue(); |
| window.editor.setValue(cur ? cur + '\n\n' + code : code); |
| btn.innerHTML = '<span class="action-icon">β
</span>Inserted!'; |
| setTimeout(function() { btn.innerHTML = '<span class="action-icon">π</span>Insert into editor'; }, 1500); |
| } |
| |
| function chatActionSimplify(btn) { |
| var msg = btn.closest('.chat-msg'); |
| if (!msg) return; |
| var bubble = msg.querySelector('.chat-bubble-content'); |
| var text = bubble ? bubble.textContent.substring(0, 200) : ''; |
| var chatInput = document.getElementById('chat-input'); |
| if (chatInput) { |
| chatInput.value = 'Can you simplify this explanation? "' + text.trim() + '..."'; |
| chatInput.dispatchEvent(new Event('input')); |
| chatInput.focus(); |
| } |
| } |
| |
| function chatActionRegenerate(btn) { |
| |
| var msg = btn.closest('.chat-msg'); |
| if (!msg) return; |
| var prev = msg.previousElementSibling; |
| while (prev && !prev.classList.contains('user')) prev = prev.previousElementSibling; |
| if (!prev) return; |
| var userBubble = prev.querySelector('.chat-bubble'); |
| if (!userBubble) return; |
| var chatInput = document.getElementById('chat-input'); |
| if (chatInput) { |
| chatInput.value = userBubble.textContent.trim(); |
| chatInput.dispatchEvent(new Event('input')); |
| chatInput.focus(); |
| |
| msg.remove(); |
| } |
| } |
| </script> |
|
|
| </body> |
| </html> |
|
|