ai-group-chat-ui / script.js
SuperPauly's picture
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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
}
// 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();