Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Codette Chat</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" /> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #0b0c10; | |
| --surface: #13141a; | |
| --border: #1f2130; | |
| --accent: #c8f078; | |
| --accent2: #78d4f0; | |
| --text: #e8eaf0; | |
| --muted: #5a5f78; | |
| --user-bg: #1a1f35; | |
| --bot-bg: #111318; | |
| --radius: 14px; | |
| --font-mono: 'JetBrains Mono', monospace; | |
| --font-sans: 'Inter', system-ui, sans-serif; | |
| } | |
| html { | |
| height: 100%; | |
| background: var(--bg); | |
| } | |
| body { | |
| min-height: 100%; | |
| background: var(--bg); | |
| font-family: var(--font-sans); | |
| padding: 40px 20px; | |
| } | |
| /* ── Widget Shell ── */ | |
| #hc-chat { | |
| width: 100%; | |
| max-width: 700px; | |
| margin: 0 auto; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| box-shadow: | |
| 0 0 0 1px #ffffff08 inset, | |
| 0 32px 80px #00000060; | |
| } | |
| /* ── Header ── */ | |
| #hc-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| padding: 18px 24px; | |
| border-bottom: 1px solid var(--border); | |
| background: linear-gradient(135deg, #0f1118 0%, #141825 100%); | |
| } | |
| .hc-avatar { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 10px; | |
| background: linear-gradient(135deg, var(--accent) 0%, var(--accent2) 100%); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 20px; | |
| flex-shrink: 0; | |
| } | |
| .hc-title { | |
| font-weight: 600; | |
| color: var(--text); | |
| font-size: 16px; | |
| letter-spacing: 0.01em; | |
| } | |
| .hc-subtitle { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--accent); | |
| letter-spacing: 0.06em; | |
| text-transform: uppercase; | |
| margin-top: 3px; | |
| } | |
| .hc-dots { | |
| margin-left: auto; | |
| display: flex; | |
| gap: 6px; | |
| align-items: center; | |
| } | |
| .hc-dot { | |
| width: 7px; height: 7px; border-radius: 50%; | |
| background: var(--border); | |
| } | |
| .hc-dot.on { | |
| background: var(--accent); | |
| box-shadow: 0 0 6px var(--accent); | |
| animation: pulse 1.8s ease-in-out infinite; | |
| } | |
| @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} } | |
| /* ── Log ── */ | |
| #hc-log { | |
| min-height: 340px; | |
| max-height: 440px; | |
| overflow-y: auto; | |
| padding: 24px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--border) transparent; | |
| } | |
| .hc-row { display: flex; flex-direction: column; } | |
| .hc-row.user { align-items: flex-end; } | |
| .hc-row.bot { align-items: flex-start; } | |
| .hc-label { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| font-weight: 500; | |
| letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| margin-bottom: 6px; | |
| padding: 0 4px; | |
| } | |
| .hc-bubble { | |
| max-width: 82%; | |
| padding: 13px 17px; | |
| border-radius: var(--radius); | |
| font-size: 15px; | |
| line-height: 1.7; | |
| white-space: pre-wrap; | |
| border: 1px solid var(--border); | |
| letter-spacing: 0.01em; | |
| } | |
| .hc-row.user .hc-bubble { | |
| background: var(--user-bg); | |
| color: var(--text); | |
| border-color: #2a3050; | |
| border-bottom-right-radius: 4px; | |
| } | |
| .hc-row.bot .hc-bubble { | |
| background: var(--bot-bg); | |
| color: var(--text); | |
| border-color: var(--border); | |
| border-bottom-left-radius: 4px; | |
| } | |
| .hc-cursor::after { | |
| content: '▋'; | |
| color: var(--accent); | |
| animation: blink .7s step-end infinite; | |
| } | |
| @keyframes blink { 50%{opacity:0} } | |
| /* ── Input bar ── */ | |
| #hc-bar { | |
| display: flex; | |
| gap: 10px; | |
| padding: 16px 20px; | |
| border-top: 1px solid var(--border); | |
| background: var(--surface); | |
| } | |
| #hc-input { | |
| flex: 1; | |
| padding: 12px 16px; | |
| background: var(--bg); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| color: var(--text); | |
| font-family: var(--font-sans); | |
| font-size: 15px; | |
| line-height: 1.5; | |
| outline: none; | |
| transition: border-color .2s; | |
| } | |
| #hc-input::placeholder { color: var(--muted); } | |
| #hc-input:focus { border-color: #2e3555; } | |
| #hc-send { | |
| padding: 12px 20px; | |
| background: var(--accent); | |
| color: #0b0c10; | |
| border: none; | |
| border-radius: 10px; | |
| font-family: var(--font-sans); | |
| font-weight: 600; | |
| font-size: 14px; | |
| cursor: pointer; | |
| transition: opacity .15s, transform .1s; | |
| letter-spacing: 0.02em; | |
| white-space: nowrap; | |
| } | |
| #hc-send:hover { opacity: .88; } | |
| #hc-send:active { transform: scale(.97); } | |
| #hc-send:disabled { opacity: .35; cursor: not-allowed; } | |
| /* ── Footer ── */ | |
| #hc-footer { | |
| padding: 10px 24px 14px; | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| color: var(--muted); | |
| letter-spacing: 0.05em; | |
| text-align: center; | |
| } | |
| @media (max-width: 480px) { | |
| body { padding: 16px 12px; } | |
| .hc-bubble { max-width: 92%; font-size: 14px; } | |
| #hc-log { max-height: 380px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="hc-chat"> | |
| <div id="hc-header"> | |
| <div class="hc-avatar">🎵</div> | |
| <div> | |
| <div class="hc-title">Codette</div> | |
| <div class="hc-subtitle">Music Production AI</div> | |
| </div> | |
| <div class="hc-dots"> | |
| <div class="hc-dot on" style="animation-delay:.0s"></div> | |
| <div class="hc-dot on" style="animation-delay:.3s"></div> | |
| <div class="hc-dot on" style="animation-delay:.6s"></div> | |
| <div class="hc-dot"></div> | |
| </div> | |
| </div> | |
| <div id="hc-log"></div> | |
| <div id="hc-bar"> | |
| <input id="hc-input" type="text" placeholder="Ask about chords, mixing, arrangement…" autocomplete="off" /> | |
| <button id="hc-send">Send</button> | |
| </div> | |
| <div id="hc-footer"> | |
| CODETTE-LLAMA · LLAMA 3.2-1B · NO CHAT HISTORY SAVED | |
| </div> | |
| </div> | |
| <script> | |
| (() => { | |
| const API_BASE = "https://raiff1982-codette-ai.hf.space"; | |
| const log = document.getElementById("hc-log"); | |
| const input = document.getElementById("hc-input"); | |
| const sendBtn = document.getElementById("hc-send"); | |
| const messages = [ | |
| { | |
| role: "system", | |
| content: | |
| "You are Codette, an AI music production assistant with expertise in " + | |
| "music theory, mixing, composition, harmonic progressions, genre analysis, " + | |
| "ear training, and arrangement. You assist musicians and producers with " + | |
| "creative and technical guidance. Be concise, practical, and musically informed." | |
| } | |
| ]; | |
| function addBubble(role, text = "") { | |
| const row = document.createElement("div"); | |
| row.className = `hc-row ${role === "user" ? "user" : "bot"}`; | |
| const label = document.createElement("div"); | |
| label.className = "hc-label"; | |
| label.textContent = role === "user" ? "You" : "Codette"; | |
| const bubble = document.createElement("div"); | |
| bubble.className = "hc-bubble"; | |
| if (role === "assistant" && !text) bubble.classList.add("hc-cursor"); | |
| bubble.textContent = text; | |
| row.appendChild(label); | |
| row.appendChild(bubble); | |
| log.appendChild(row); | |
| log.scrollTop = log.scrollHeight; | |
| return bubble; | |
| } | |
| async function streamReply() { | |
| const text = input.value.trim(); | |
| if (!text) return; | |
| input.value = ""; | |
| messages.push({ role: "user", content: text }); | |
| addBubble("user", text); | |
| const bubble = addBubble("assistant", ""); | |
| sendBtn.disabled = true; | |
| input.disabled = true; | |
| try { | |
| const resp = await fetch(`${API_BASE}/api/chat`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ messages, stream: true }) | |
| }); | |
| if (!resp.ok) { | |
| bubble.textContent = `Error ${resp.status}: ${await resp.text()}`; | |
| bubble.classList.remove("hc-cursor"); | |
| return; | |
| } | |
| const reader = resp.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let full = "", buffer = ""; | |
| while (true) { | |
| const { value, done } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split("\n"); | |
| buffer = lines.pop(); | |
| for (const line of lines) { | |
| if (!line.trim()) continue; | |
| try { | |
| const chunk = JSON.parse(line); | |
| const token = chunk?.message?.content ?? ""; | |
| full += token; | |
| bubble.textContent = full || ""; | |
| bubble.classList.add("hc-cursor"); | |
| log.scrollTop = log.scrollHeight; | |
| if (chunk.done) { | |
| messages.push({ role: "assistant", content: full }); | |
| bubble.classList.remove("hc-cursor"); | |
| } | |
| } catch { /* skip malformed */ } | |
| } | |
| } | |
| bubble.classList.remove("hc-cursor"); | |
| if (!full) bubble.textContent = "(no response)"; | |
| } catch (err) { | |
| bubble.classList.remove("hc-cursor"); | |
| bubble.textContent = err.message.includes("fetch") | |
| ? "⚠️ Cannot reach Codette server. The Space may be starting up — try again in 30 seconds." | |
| : `Error: ${err.message}`; | |
| } finally { | |
| sendBtn.disabled = false; | |
| input.disabled = false; | |
| input.focus(); | |
| } | |
| } | |
| sendBtn.addEventListener("click", streamReply); | |
| input.addEventListener("keydown", e => { | |
| if (e.key === "Enter" && !e.shiftKey) streamReply(); | |
| }); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |