Spaces:
Running
Running
WhatsApp group chat interface, 1 human user, multiple agent users. Upload image buttons, upload code button, connect to GitHub button, button to selector 1 out of 5 modes "design" "architect" "plan" "debug" "code".
31e1a75 verified | // Theme handling: "undefined mode" defaults to system unless explicitly toggled | |
| (function initTheme() { | |
| const root = document.documentElement; | |
| const saved = localStorage.getItem('theme-mode'); // 'light' | 'dark' | |
| const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; | |
| const apply = (mode) => { | |
| root.classList.toggle('dark', mode === 'dark'); | |
| }; | |
| if (saved === 'light' || saved === 'dark') { | |
| apply(saved); | |
| } else { | |
| apply(prefersDark ? 'dark' : 'light'); | |
| } | |
| })(); | |
| const themeToggle = document.getElementById('themeToggle'); | |
| if (themeToggle) { | |
| themeToggle.addEventListener('click', () => { | |
| const root = document.documentElement; | |
| const isDark = root.classList.toggle('dark'); | |
| localStorage.setItem('theme-mode', isDark ? 'dark' : 'light'); | |
| }); | |
| } | |
| // Elements | |
| const chatBox = document.getElementById('chatBox'); | |
| const composerForm = document.getElementById('composerForm'); | |
| const messageInput = document.getElementById('messageInput'); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| const imageInput = document.getElementById('imageInput'); | |
| const codeInput = document.getElementById('codeInput'); | |
| const uploadImageBtn = document.getElementById('uploadImageBtn'); | |
| const uploadCodeBtn = document.getElementById('uploadCodeBtn'); | |
| const connectGitHubBtn = document.getElementById('connectGitHubBtn'); | |
| const githubConnectHeaderBtn = document.getElementById('githubConnectBtn'); | |
| const attachmentPreview = document.getElementById('attachmentPreview'); | |
| const modeIndicator = document.getElementById('modeIndicator'); | |
| const modeButtons = document.querySelectorAll('.mode-btn'); | |
| let currentMode = 'code'; | |
| updateModeIndicator(); | |
| // Helpers | |
| function formatTime(date = new Date()) { | |
| return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| } | |
| function scrollToBottom() { | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| } | |
| function createAvatar(initials, seed = 0) { | |
| const palette = ['bg-undefined-500', 'bg-blue-500', 'bg-teal-500', 'bg-violet-500', 'bg-rose-500']; | |
| const color = palette[seed % palette.length]; | |
| return `<div class="h-10 w-10 rounded-full ${color} grid place-items-center text-white font-bold">${initials}</div>`; | |
| } | |
| function agentBubble({ name = 'Agent', message = '', attachments = [] }) { | |
| const idx = Math.floor(Math.random() * 5); | |
| return ` | |
| <div class="flex items-start gap-3"> | |
| ${createAvatar(name.slice(0,1).toUpperCase(), idx)} | |
| <div class="max-w-[85%] sm:max-w-[80%]"> | |
| <div class="text-xs text-zinc-600 dark:text-zinc-400 mb-1">${name}</div> | |
| <div class="prose prose-sm dark:prose-invert max-w-none bg-zinc-100 dark:bg-zinc-800 rounded-2xl rounded-tl-sm px-4 py-2"> | |
| ${message ? `<p class="m-0">${escapeHtml(message)}</p>` : ''} | |
| ${attachments.length ? `<div class="mt-2 grid gap-2">${attachments.join('')}</div>` : ''} | |
| </div> | |
| <div class="text-[10px] text-zinc-500 mt-1">${formatTime()}</div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function userBubble({ message = '', attachments = [] }) { | |
| return ` | |
| <div class="flex items-start gap-3 justify-end"> | |
| <div class="max-w-[85%] sm:max-w-[80%]"> | |
| <div class="text-xs text-right text-zinc-600 dark:text-zinc-400 mb-1">You</div> | |
| <div class="bg-undefined-500 text-white rounded-2xl rounded-tr-sm px-4 py-2"> | |
| ${message ? `<p class="m-0">${escapeHtml(message)}</p>` : ''} | |
| ${attachments.length ? `<div class="mt-2 grid gap-2">${attachments.join('')}</div>` : ''} | |
| </div> | |
| <div class="text-[10px] text-right text-zinc-500 mt-1">${formatTime()}</div> | |
| </div> | |
| ${createAvatar('Y')} | |
| </div> | |
| `; | |
| } | |
| function escapeHtml(str) { | |
| return str | |
| .replaceAll('&', '&') | |
| .replaceAll('<', '<') | |
| .replaceAll('>', '>'); | |
| } | |
| // Attachments UI | |
| function showAttachmentPreview(items) { | |
| if (!items.length) { | |
| attachmentPreview.classList.add('hidden'); | |
| attachmentPreview.innerHTML = ''; | |
| return; | |
| } | |
| attachmentPreview.classList.remove('hidden'); | |
| attachmentPreview.innerHTML = items.map((item) => { | |
| if (item.type.startsWith('image/')) { | |
| return `<div class="rounded-lg border border-zinc-200 dark:border-zinc-700 p-2 bg-zinc-50 dark:bg-zinc-900"> | |
| <div class="flex items-center gap-3"> | |
| <img src="${item.url}" alt="preview" class="h-12 w-12 object-cover rounded-md"/> | |
| <div class="text-xs"><div class="font-medium truncate">${item.file?.name || 'image'}</div><div class="text-zinc-600 dark:text-zinc-400">${(item.file?.size / 1024).toFixed(1)} KB</div></div> | |
| <button class="ml-auto text-sm text-red-600 hover:underline" data-remove>Remove</button> | |
| </div> | |
| </div>`; | |
| } | |
| // code file preview | |
| return `<div class="rounded-lg border border-zinc-200 dark:border-zinc-700 p-2 bg-zinc-50 dark:bg-zinc-900"> | |
| <div class="flex items-center gap-2"> | |
| <span class="text-sm">📄</span> | |
| <div class="text-xs"> | |
| <div class="font-medium truncate">${item.file?.name || 'code'}</div> | |
| <div class="text-zinc-600 dark:text-zinc-400">${(item.file?.size / 1024).toFixed(1)} KB</div> | |
| </div> | |
| <button class="ml-auto text-sm text-red-600 hover:underline" data-remove>Remove</button> | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| // Remove handler | |
| attachmentPreview.querySelectorAll('[data-remove]').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const card = btn.closest('[class*=rounded-lg]'); | |
| const index = Array.from(attachmentPreview.children).indexOf(card); | |
| pendingAttachments.splice(index, 1); | |
| showAttachmentPreview(pendingAttachments); | |
| }); | |
| }); | |
| } | |
| const pendingAttachments = []; | |
| // Mode handling | |
| function updateModeIndicator() { | |
| if (modeIndicator) modeIndicator.textContent = `Mode: ${currentMode}`; | |
| modeButtons.forEach(b => { | |
| const isActive = b.dataset.mode === currentMode; | |
| b.classList.toggle('bg-undefined-500', isActive); | |
| b.classList.toggle('text-white', isActive); | |
| b.classList.toggle('hover:opacity-90', isActive); | |
| }); | |
| } | |
| modeButtons.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| currentMode = btn.dataset.mode; | |
| updateModeIndicator(); | |
| // Post a small status message | |
| const statusMsg = `Mode switched to: ${currentMode}`; | |
| chatBox.insertAdjacentHTML('beforeend', agentBubble({ name: 'System', message: statusMsg })); | |
| scrollToBottom(); | |
| }); | |
| }); | |
| // Composer events | |
| composerForm.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| const message = messageInput.value.trim(); | |
| const attachments = pendingAttachments.slice(); | |
| if (!message && !attachments.length) return; | |
| chatBox.insertAdjacentHTML('beforeend', userBubble({ message, attachments })); | |
| scrollToBottom(); | |
| // Simulate agent response | |
| setTimeout(() => { | |
| const reply = agentReply(message, currentMode, attachments); | |
| chatBox.insertAdjacentHTML('beforeend', reply); | |
| scrollToBottom(); | |
| }, 600); | |
| // Reset composer | |
| messageInput.value = ''; | |
| pendingAttachments.length = 0; | |
| showAttachmentPreview(pendingAttachments); | |
| messageInput.focus(); | |
| }); | |
| function agentReply(text, mode, attachments) { | |
| let message = ''; | |
| if (attachments.some(a => a.type.startsWith('image/'))) { | |
| message += 'Got your image(s). '; | |
| } | |
| if (attachments.some(a => !a.type.startsWith('image/'))) { | |
| message += 'I also see code/file attachments. '; | |
| } | |
| const modeHints = { | |
| design: 'I can suggest UI/UX and styling tweaks.', | |
| architect: 'I can outline components, data flow, and constraints.', | |
| plan: 'I can draft a plan with milestones and tasks.', | |
| debug: 'I can help triage issues and propose fixes.', | |
| code: 'I can produce and refine code based on your input.' | |
| }; | |
| message += `${modeHints[mode] || ''}${text ? ` Regarding "${escapeHtml(text)}"` : ''} — what would you like to do next?`; | |
| return agentBubble({ name: 'Agent', message }); | |
| } | |
| // Input attachments | |
| uploadImageBtn.addEventListener('click', () => imageInput.click()); | |
| uploadCodeBtn.addEventListener('click', () => codeInput.click()); | |
| imageInput.addEventListener('change', async (e) => { | |
| const files = Array.from(e.target.files || []); | |
| files.forEach(file => { | |
| if (!file.type.startsWith('image/')) return; | |
| const url = URL.createObjectURL(file); | |
| pendingAttachments.push({ type: file.type, file, url }); | |
| }); | |
| showAttachmentPreview(pendingAttachments); | |
| imageInput.value = ''; | |
| }); | |
| codeInput.addEventListener('change', async (e) => { | |
| const files = Array.from(e.target.files || []); | |
| files.forEach(file => { | |
| // accept text-based files | |
| const isText = /(\.|\/)(txt|md|json|js|ts|jsx|tsx|html|css|scss|py|rb|go|rs|java|c|cpp|cs|yml|yaml|ini|env)$/i.test(file.name); | |
| if (!isText) return; | |
| const url = URL.createObjectURL(file); | |
| pendingAttachments.push({ type: 'text/x-code', file, url }); | |
| }); | |
| showAttachmentPreview(pendingAttachments); | |
| codeInput.value = ''; | |
| }); | |
| // GitHub connect | |
| function connectGitHub() { | |
| // In a real app, redirect to your backend OAuth route | |
| const token = prompt('Paste a personal access token (classic or fine-grained) to simulate GitHub connection:'); | |
| if (!token) return; | |
| const shortToken = token.slice(0, 6) + '...' + token.slice(-4); | |
| const msg = `Connected to GitHub (token: ${shortToken}). You can now fetch repos, issues, and PRs.`; | |
| chatBox.insertAdjacentHTML('beforeend', agentBubble({ name: 'GitHub', message: msg })); | |
| scrollToBottom(); | |
| } | |
| connectGitHubBtn?.addEventListener('click', connectGitHub); | |
| githubConnectHeaderBtn?.addEventListener('click', connectGitHub); | |
| // Auto-resize textarea | |
| messageInput.addEventListener('input', () => { | |
| messageInput.style.height = 'auto'; | |
| messageInput.style.height = Math.min(messageInput.scrollHeight, 160) + 'px'; | |
| }); | |
| // Accessibility: focus input on load | |
| messageInput.focus(); |