diff --git a/dev-chat.html b/dev-chat.html new file mode 100644 index 0000000..6ef8a4d --- /dev/null +++ b/dev-chat.html @@ -0,0 +1,697 @@ + + + + + + Dev Chat Widget + + + + + + + + +
+
+

+ + + + Dev Chat + DEV +

+ +
+ +
+ + + + + +
+ +
+
+ Dev mode active. Talk to test your app or modify files. +
+
+ +
+ + +
+
+ + + + + diff --git a/static/dev-chat.js b/static/dev-chat.js new file mode 100644 index 0000000..d4dd624 --- /dev/null +++ b/static/dev-chat.js @@ -0,0 +1,587 @@ +/** + * Dev Chat Widget - Injectable Script + * + * Add to any page: + * Or inject dynamically in dev mode + * + * Uses user_data virtual table for storage (one table for all) + */ + +(function() { + 'use strict'; + + // Only run in dev mode + const isDevMode = window.location.search.includes('dev=1') || + document.cookie.includes('dev_mode=1') || + window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1'; + + if (!isDevMode) return; + + const CONFIG = { + apiEndpoint: '/api/chat/dev', + wsEndpoint: '/ws/dev', + userDataEndpoint: '/api/db/user_data', + maxHistory: 50 + }; + + let isOpen = false; + let ws = null; + let isTyping = false; + + // Inject styles + const styles = ` + #gb-dev-chat-btn { + position: fixed; + bottom: 20px; + right: 20px; + width: 56px; + height: 56px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + cursor: pointer; + box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4); + z-index: 99999; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.2s, box-shadow 0.2s; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + } + #gb-dev-chat-btn:hover { + transform: scale(1.1); + box-shadow: 0 6px 24px rgba(102, 126, 234, 0.6); + } + #gb-dev-chat-btn svg { width: 28px; height: 28px; fill: white; } + #gb-dev-chat-btn .badge { + position: absolute; + top: -4px; + right: -4px; + background: #ef4444; + color: white; + font-size: 10px; + font-weight: bold; + padding: 2px 6px; + border-radius: 10px; + display: none; + } + #gb-dev-panel { + position: fixed; + bottom: 90px; + right: 20px; + width: 380px; + height: 520px; + background: #0f172a; + border-radius: 16px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + z-index: 99998; + display: none; + flex-direction: column; + overflow: hidden; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + } + #gb-dev-panel.open { + display: flex; + animation: gbDevSlideUp 0.3s ease; + } + @keyframes gbDevSlideUp { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } + } + #gb-dev-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 16px; + display: flex; + align-items: center; + justify-content: space-between; + } + #gb-dev-header h3 { + margin: 0; + color: white; + font-size: 16px; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; + } + #gb-dev-header .dev-badge { + background: rgba(255,255,255,0.2); + padding: 2px 8px; + border-radius: 4px; + font-size: 10px; + text-transform: uppercase; + } + #gb-dev-header button { + background: rgba(255,255,255,0.2); + border: none; + color: white; + width: 28px; + height: 28px; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + } + #gb-dev-header button:hover { background: rgba(255,255,255,0.3); } + #gb-dev-actions { + padding: 8px 16px; + background: #1e293b; + display: flex; + gap: 6px; + flex-wrap: wrap; + } + .gb-dev-action { + background: #334155; + border: none; + color: #94a3b8; + padding: 4px 10px; + border-radius: 4px; + font-size: 11px; + cursor: pointer; + } + .gb-dev-action:hover { background: #475569; color: white; } + #gb-dev-messages { + flex: 1; + overflow-y: auto; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; + } + #gb-dev-messages::-webkit-scrollbar { width: 6px; } + #gb-dev-messages::-webkit-scrollbar-track { background: transparent; } + #gb-dev-messages::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; } + .gb-dev-msg { + max-width: 85%; + padding: 10px 14px; + border-radius: 12px; + font-size: 14px; + line-height: 1.4; + word-wrap: break-word; + } + .gb-dev-msg.user { + background: #667eea; + color: white; + align-self: flex-end; + border-bottom-right-radius: 4px; + } + .gb-dev-msg.bot { + background: #1e293b; + color: #e2e8f0; + align-self: flex-start; + border-bottom-left-radius: 4px; + } + .gb-dev-msg.system { + background: #064e3b; + color: #6ee7b7; + align-self: center; + font-size: 12px; + padding: 6px 12px; + } + .gb-dev-msg.error { + background: #7f1d1d; + color: #fca5a5; + } + .gb-dev-msg pre { + background: #000; + padding: 8px; + border-radius: 6px; + overflow-x: auto; + margin: 8px 0 0 0; + font-size: 12px; + } + .gb-dev-file { + background: #1e293b; + border-left: 3px solid #22c55e; + padding: 8px 12px; + margin: 4px 0; + border-radius: 0 8px 8px 0; + font-size: 12px; + color: #94a3b8; + } + .gb-dev-file.modified { border-color: #f59e0b; } + .gb-dev-file.deleted { border-color: #ef4444; } + .gb-dev-typing { + display: flex; + gap: 4px; + padding: 12px 16px; + background: #1e293b; + border-radius: 12px; + align-self: flex-start; + } + .gb-dev-typing span { + width: 8px; + height: 8px; + background: #64748b; + border-radius: 50%; + animation: gbDevTyping 1.4s infinite ease-in-out; + } + .gb-dev-typing span:nth-child(2) { animation-delay: 0.2s; } + .gb-dev-typing span:nth-child(3) { animation-delay: 0.4s; } + @keyframes gbDevTyping { + 0%, 60%, 100% { transform: translateY(0); } + 30% { transform: translateY(-6px); } + } + #gb-dev-input-area { + padding: 12px 16px; + background: #1e293b; + border-top: 1px solid #334155; + display: flex; + gap: 8px; + } + #gb-dev-input { + flex: 1; + background: #0f172a; + border: 1px solid #334155; + border-radius: 8px; + padding: 10px 14px; + color: #e2e8f0; + font-size: 14px; + outline: none; + resize: none; + min-height: 20px; + max-height: 100px; + font-family: inherit; + } + #gb-dev-input:focus { border-color: #667eea; } + #gb-dev-input::placeholder { color: #64748b; } + #gb-dev-send { + background: #667eea; + border: none; + border-radius: 8px; + width: 40px; + height: 40px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + } + #gb-dev-send:hover { background: #5a67d8; } + #gb-dev-send:disabled { background: #334155; cursor: not-allowed; } + #gb-dev-send svg { width: 20px; height: 20px; fill: white; } + `; + + // Inject stylesheet + const styleEl = document.createElement('style'); + styleEl.textContent = styles; + document.head.appendChild(styleEl); + + // Create HTML + const html = ` + +
+
+

+ + + + Dev Chat + DEV +

+ +
+
+ + + + + +
+
+
Dev mode active. Talk to test your app.
+
+
+ + +
+
+ `; + + // Insert into DOM + const container = document.createElement('div'); + container.id = 'gb-dev-chat-container'; + container.innerHTML = html; + document.body.appendChild(container); + + // Elements + const btn = document.getElementById('gb-dev-chat-btn'); + const panel = document.getElementById('gb-dev-panel'); + const closeBtn = document.getElementById('gb-dev-close'); + const messages = document.getElementById('gb-dev-messages'); + const input = document.getElementById('gb-dev-input'); + const sendBtn = document.getElementById('gb-dev-send'); + const actions = document.querySelectorAll('.gb-dev-action'); + + // Toggle panel + function toggle() { + isOpen = !isOpen; + panel.classList.toggle('open', isOpen); + if (isOpen) { + input.focus(); + connectWS(); + } + } + + btn.addEventListener('click', toggle); + closeBtn.addEventListener('click', toggle); + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + if (e.ctrlKey && e.shiftKey && e.key === 'D') { + e.preventDefault(); + toggle(); + } + if (e.key === 'Escape' && isOpen) { + toggle(); + } + }); + + // Input handling + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + send(); + } + }); + + sendBtn.addEventListener('click', send); + + // Quick actions + actions.forEach(btn => { + btn.addEventListener('click', () => { + const cmd = btn.dataset.cmd; + if (cmd === 'clear') { + clearChat(); + } else { + input.value = cmd; + send(); + } + }); + }); + + // Get app context + function getContext() { + const match = location.pathname.match(/\/apps\/([^\/]+)/); + return { + app: match ? match[1] : 'default', + url: location.href, + path: location.pathname + }; + } + + // Add message + function addMsg(text, type = 'bot') { + const msg = document.createElement('div'); + msg.className = `gb-dev-msg ${type}`; + + if (type === 'bot' && text.includes('```')) { + msg.innerHTML = text.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => { + return `
${escapeHtml(code.trim())}
`; + }).replace(/\n/g, '
'); + } else { + msg.textContent = text; + } + + messages.appendChild(msg); + messages.scrollTop = messages.scrollHeight; + saveToUserData(); + } + + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + // File change indicator + function showFileChange(path, type = 'modified') { + const el = document.createElement('div'); + el.className = `gb-dev-file ${type}`; + const icon = type === 'created' ? '➕' : type === 'deleted' ? '➖' : '✏️'; + el.innerHTML = `${icon} ${path}`; + messages.appendChild(el); + messages.scrollTop = messages.scrollHeight; + } + + // Typing indicator + function showTyping() { + if (isTyping) return; + isTyping = true; + const el = document.createElement('div'); + el.id = 'gb-dev-typing'; + el.className = 'gb-dev-typing'; + el.innerHTML = ''; + messages.appendChild(el); + messages.scrollTop = messages.scrollHeight; + } + + function hideTyping() { + isTyping = false; + const el = document.getElementById('gb-dev-typing'); + if (el) el.remove(); + } + + // Send message + async function send() { + const text = input.value.trim(); + if (!text) return; + + addMsg(text, 'user'); + input.value = ''; + showTyping(); + + try { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + type: 'dev_message', + content: text, + context: getContext() + })); + } else { + const res = await fetch(CONFIG.apiEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + message: text, + context: getContext() + }) + }); + hideTyping(); + if (res.ok) { + handleResponse(await res.json()); + } else { + addMsg('Error: ' + res.statusText, 'error'); + } + } + } catch (err) { + hideTyping(); + addMsg('Connection error: ' + err.message, 'error'); + } + } + + // Handle response + function handleResponse(data) { + hideTyping(); + if (data.message) addMsg(data.message, 'bot'); + if (data.files_changed) { + data.files_changed.forEach(f => showFileChange(f.path, f.type)); + } + if (data.reload) { + addMsg('Reloading...', 'system'); + setTimeout(() => location.reload(), 1000); + } + } + + // WebSocket + function connectWS() { + if (ws && ws.readyState === WebSocket.OPEN) return; + + try { + const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; + ws = new WebSocket(`${proto}//${location.host}${CONFIG.wsEndpoint}`); + + ws.onopen = () => addMsg('Connected', 'system'); + + ws.onmessage = (e) => { + const data = JSON.parse(e.data); + switch (data.type) { + case 'message': + hideTyping(); + addMsg(data.content, 'bot'); + break; + case 'file_changed': + showFileChange(data.path, data.change_type); + break; + case 'reload': + addMsg('Files changed. Reloading...', 'system'); + setTimeout(() => location.reload(), 500); + break; + case 'error': + hideTyping(); + addMsg(data.content, 'error'); + break; + } + }; + + ws.onclose = () => setTimeout(connectWS, 3000); + } catch (err) { + console.error('Dev chat WS error:', err); + } + } + + // Clear chat + function clearChat() { + messages.innerHTML = '
Chat cleared.
'; + clearUserData(); + } + + // user_data virtual table storage (one table for all) + function saveToUserData() { + const history = Array.from(messages.children).map(m => ({ + type: m.classList[1], + html: m.innerHTML + })).slice(-CONFIG.maxHistory); + + fetch(CONFIG.userDataEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + namespace: 'dev_chat', + key: getContext().app + '_history', + value: JSON.stringify(history) + }) + }).catch(() => {}); + } + + function loadFromUserData() { + fetch(`${CONFIG.userDataEndpoint}?namespace=dev_chat&key=${getContext().app}_history`) + .then(r => r.json()) + .then(data => { + if (data && data.value) { + const history = JSON.parse(data.value); + history.forEach(m => { + const el = document.createElement('div'); + el.className = `gb-dev-msg ${m.type}`; + el.innerHTML = m.html; + messages.appendChild(el); + }); + } + }) + .catch(() => {}); + } + + function clearUserData() { + fetch(`${CONFIG.userDataEndpoint}?namespace=dev_chat&key=${getContext().app}_history`, { + method: 'DELETE' + }).catch(() => {}); + } + + // Load history on init + loadFromUserData(); + + // Expose API for external use + window.gbDevChat = { + open: () => { if (!isOpen) toggle(); }, + close: () => { if (isOpen) toggle(); }, + send: (msg) => { input.value = msg; send(); }, + addMessage: addMsg, + showFileChange + }; + +})();