Sorrowful Studio

What are you crafting today?

Launch a fresh build with Sorrowful's premium AI coding suite. Your assistant will reason, draft, and iterate alongside you.

\n\n' }, { id: id(), type: 'file', name: 'style.css', content: 'body {\n font-family: sans-serif;\n background: #0f172a;\n color: #e2e8f0;\n display: grid;\n place-items: center;\n height: 100vh;\n}\n' }, { id: id(), type: 'file', name: 'app.js', content: "console.log('Sorrowful is live');" }, ]; const getDefaultProject = () => ([{ id: id(), type: 'folder', name: 'src', children: JSON.parse(JSON.stringify(defaultFiles)) }]); function ensureUsername() { if (!state.username) { elements.usernameModal.classList.remove('hidden'); elements.usernameInput.focus(); } else { elements.studioGreeting.textContent = `Welcome back, ${state.username}`; } } function dismissUsernameModal(persist) { elements.usernameModal.classList.add('hidden'); if (persist) { const handle = elements.usernameInput.value.trim() || 'Creator'; state.username = handle; localStorage.setItem('sorrowful-username', handle); elements.studioGreeting.textContent = `Welcome back, ${handle}`; } else if (!state.username) { state.username = 'Creator'; elements.studioGreeting.textContent = 'Welcome back, Creator'; } } elements.usernameSave.addEventListener('click', () => dismissUsernameModal(true)); elements.usernameCancel.addEventListener('click', () => dismissUsernameModal(false)); elements.usernameInput.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); dismissUsernameModal(true); } }); function renderChat() { const fragment = document.createDocumentFragment(); state.messages.forEach((message) => { const wrapper = document.createElement('div'); wrapper.className = `mb-6 rounded-2xl border border-border/70 bg-surfaceRaised/30 p-4 ${message.role === 'user' ? 'border-accent/40' : ''}`; const header = document.createElement('div'); header.className = 'flex items-center gap-2 text-xs uppercase tracking-[0.2em] text-subtle'; header.innerHTML = message.role === 'user' ? 'You' : 'Studio AI'; const body = document.createElement('div'); body.className = 'message-markdown prose prose-invert max-w-none text-sm leading-relaxed'; body.innerHTML = marked.parse(message.content || (message.streaming ? '...' : '')); wrapper.appendChild(header); wrapper.appendChild(body); fragment.appendChild(wrapper); }); elements.chatThread.innerHTML = ''; elements.chatThread.appendChild(fragment); elements.chatThread.scrollTo({ top: elements.chatThread.scrollHeight, behavior: 'smooth' }); } function setReasoning(text, active) { elements.reasoningOutput.textContent = text; if (active) { elements.reasoningPanel.classList.add('border-accent/40'); elements.reasoningPanel.classList.remove('border-border'); } else { elements.reasoningPanel.classList.remove('border-accent/40'); elements.reasoningPanel.classList.add('border-border'); } } function showCodeStream() { elements.codeStreamPanel.classList.remove('hidden'); } function hideCodeStream() { elements.codeStreamPanel.classList.add('hidden'); elements.codeStreamContent.innerHTML = ''; } elements.closeCodeStream.addEventListener('click', () => { hideCodeStream(); }); function createMessage(role, content) { const message = { id: id(), role, content, streaming: false }; state.messages.push(message); renderChat(); return message; } async function streamCompletion(prompt) { createMessage('user', prompt); const assistantMessage = createMessage('assistant', ''); assistantMessage.streaming = true; state.streamingMessageId = assistantMessage.id; setReasoning('Analyzing requirements...', true); showCodeStream(); const body = { model: 'openai/gpt-oss-120b', messages: state.messages.map(({ role, content }) => ({ role, content })), temperature: 1, max_completion_tokens: 8192, top_p: 1, reasoning_effort: 'high', stream: true, }; if (state.webSearch) { body.tools = [{ type: 'browser_search' }]; } try { const response = await fetch('https://api.groq.com/openai/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}`, }, body: JSON.stringify(body), }); if (!response.ok || !response.body) { throw new Error('Unable to reach Sorrowful engines.'); } const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let fullContent = ''; let codeBuffer = ''; let reasoningBuffer = ''; while (true) { const { value, done } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n'); for (const line of lines) { if (!line.startsWith('data:')) continue; const payload = line.replace('data:', '').trim(); if (!payload || payload === '[DONE]') continue; try { const data = JSON.parse(payload); const delta = data.choices?.[0]?.delta || {}; if (delta.reasoning_content) { reasoningBuffer += delta.reasoning_content; setReasoning(reasoningBuffer, true); } if (delta.content) { fullContent += delta.content; assistantMessage.content = fullContent; renderChat(); const codeMatch = fullContent.match(/```[a-zA-Z0-9]*\n([\s\S]*?)```/); if (codeMatch) { codeBuffer = codeMatch[1]; elements.codeStreamContent.innerHTML = `
${escapeHtml(codeBuffer)}
`; } } } catch (error) { console.warn('Streaming parse issue', error); } } } assistantMessage.streaming = false; state.streamingMessageId = null; setReasoning('Complete.', false); if (!assistantMessage.content) { assistantMessage.content = 'Sorrowful has completed the request.'; } renderChat(); } catch (error) { console.error(error); assistantMessage.streaming = false; assistantMessage.content = 'Something went wrong while reaching the studio engines. Try again shortly.'; state.streamingMessageId = null; setReasoning('Idle.', false); renderChat(); } } function escapeHtml(str) { return str.replace(/&/g, '&').replace(//g, '>'); } elements.browserSearchToggle.addEventListener('change', (event) => { state.webSearch = event.target.checked; }); elements.chatForm.addEventListener('submit', (event) => { event.preventDefault(); const prompt = elements.chatInput.value.trim(); if (!prompt) return; elements.chatInput.value = ''; streamCompletion(prompt); }); elements.startQueryForm.addEventListener('submit', (event) => { event.preventDefault(); const prompt = elements.startQuery.value.trim(); if (!prompt) return; elements.startQuery.value = ''; elements.startupScreen.classList.add('hidden'); elements.appShell.classList.remove('hidden'); setTimeout(() => { elements.chatInput.focus(); }, 400); streamCompletion(prompt); }); elements.newChat.addEventListener('click', () => { state.messages = []; renderChat(); hideCodeStream(); setReasoning('Idle.', false); }); elements.connectVSCode.addEventListener('click', () => { alert('Extension will sync files with your VS Code.'); }); elements.discordLogin.addEventListener('click', () => { alert('Discord login coming soon.'); }); function findItemById(items, itemId) { for (const item of items) { if (item.id === itemId) return item; if (item.type === 'folder') { const found = findItemById(item.children, itemId); if (found) return found; } } return null; } function removeItemById(items, itemId) { return items.filter((item) => { if (item.id === itemId) return false; if (item.type === 'folder') { item.children = removeItemById(item.children, itemId); } return true; }); } function renderFileTree() { elements.fileTree.innerHTML = ''; const fragment = document.createDocumentFragment(); state.project.forEach((node) => fragment.appendChild(renderNode(node, 0))); elements.fileTree.appendChild(fragment); } function renderNode(node, depth) { const row = document.createElement('div'); row.className = 'group rounded-xl px-3 py-2 hover:bg-surfaceRaised/40 transition cursor-pointer'; row.style.paddingLeft = `${depth * 14 + 12}px`; row.dataset.id = node.id; row.innerHTML = node.type === 'folder' ? `
${node.name}
` : `
${node.name}
`; row.addEventListener('click', (event) => { event.stopPropagation(); if (node.type === 'folder') { if (state.openFolders.has(node.id)) { state.openFolders.delete(node.id); } else { state.openFolders.add(node.id); } renderFileTree(); } else { openFile(node.id); } }); const container = document.createElement('div'); container.appendChild(row); if (node.type === 'folder' && state.openFolders.has(node.id)) { node.children.forEach((child) => container.appendChild(renderNode(child, depth + 1))); } return container; } function openFile(fileId) { const file = findItemById(state.project, fileId); if (!file || file.type !== 'file') return; state.activeFileId = fileId; elements.activeFileName.textContent = file.name; if (state.editor) { const model = state.editor.getModel(); if (!model || model.uri.path !== `/${fileId}`) { const uri = state.monaco.Uri.parse(`inmemory://model/${fileId}`); const newModel = state.monaco.editor.createModel(file.content, undefined, uri); state.editor.setModel(newModel); } else { state.editor.setValue(file.content); } } renderFileTree(); refreshOutput(); } function updateActiveFileContent(value) { const file = findItemById(state.project, state.activeFileId); if (file && file.type === 'file') { file.content = value; } } function promptForName(label, defaultValue = '') { const result = prompt(label, defaultValue); return result ? result.trim() : null; } elements.createFile.addEventListener('click', () => { const name = promptForName('File name', 'untitled.js'); if (!name) return; const targetFolder = ensureDefaultRoot(); targetFolder.children.push({ id: id(), type: 'file', name, content: '' }); renderFileTree(); }); elements.createFolder.addEventListener('click', () => { const name = promptForName('Folder name', 'new-folder'); if (!name) return; state.project.push({ id: id(), type: 'folder', name, children: [] }); state.openFolders.add(state.project[state.project.length - 1].id); renderFileTree(); }); function ensureDefaultRoot() { if (!state.project.length) { state.project = getDefaultProject(); state.project.forEach((item) => state.openFolders.add(item.id)); } const rootFolder = state.project[0]; if (rootFolder.type === 'folder') return rootFolder; const folder = { id: id(), type: 'folder', name: 'src', children: state.project.slice() }; state.project = [folder]; return folder; } elements.renameItem.addEventListener('click', () => { if (!state.activeFileId) { alert('Select a file to rename.'); return; } const file = findItemById(state.project, state.activeFileId); if (!file) return; const name = promptForName('Rename item', file.name); if (!name) return; file.name = name; elements.activeFileName.textContent = file.name; renderFileTree(); }); elements.deleteItem.addEventListener('click', () => { if (!state.activeFileId) { alert('Select a file to delete.'); return; } if (!confirm('Delete selected item?')) return; state.project = removeItemById(state.project, state.activeFileId); state.activeFileId = null; elements.activeFileName.textContent = 'No file selected'; if (state.editor) { state.editor.setValue(''); } renderFileTree(); }); elements.exportZip.addEventListener('click', async () => { const zip = new JSZip(); addItemsToZip(zip, state.project); const blob = await zip.generateAsync({ type: 'blob' }); saveAs(blob, 'sorrowful-project.zip'); }); function addItemsToZip(zip, items, path = '') { items.forEach((item) => { if (item.type === 'folder') { const folder = zip.folder(`${path}${item.name}`); addItemsToZip(folder, item.children, ''); } else { zip.file(`${path}${item.name}`, item.content || ''); } }); } elements.runOutput.addEventListener('click', () => { refreshOutput(true); }); elements.clearOutput.addEventListener('click', () => { elements.previewFrame.srcdoc = 'Output cleared.'; }); function refreshOutput(force = false) { const file = findItemById(state.project, state.activeFileId); if (!file || file.type !== 'file') return; if (!force && !file.name.endsWith('.html') && !file.name.endsWith('.js') && !file.name.endsWith('.css')) { return; } if (file.name.endsWith('.html')) { elements.previewFrame.srcdoc = file.content; return; } if (file.name.endsWith('.js')) { elements.previewFrame.srcdoc = `