import { useState, useRef, useEffect, useCallback } from 'react' const MODEL = 'claude-sonnet-4-6' const API_URL = 'https://api.anthropic.com/v1/messages' const LS_KEY = 'wtf_teacher_key' // ── System prompt — rebuilt with live session context on every request ──────── function buildSystemPrompt({ keyInfo, currentChord, bpm, chordHistory }) { const keyStr = keyInfo ? `${keyInfo.root} ${keyInfo.mode}` : 'not detected yet' const chordStr = currentChord?.name ?? 'none detected' const bpmStr = bpm ? `${Math.round(bpm)} BPM` : 'not detected' const histStr = chordHistory?.length ? chordHistory.map(c => c.name).join(' → ') : 'none yet' return `You are an expert music teacher and session musician embedded in JamBuddy, a real-time chord and key detection app for guitarists and keyboard players at live jam sessions. LIVE SESSION CONTEXT (updated in real time): • Detected key: ${keyStr} • Current chord: ${chordStr} • BPM: ${bpmStr} • Recent chord history: ${histStr} YOUR ROLE: - Explain chords, scales, and music theory in plain, friendly language - Suggest what to practice based on the current key and chord progression - Teach playing techniques: fretting, strumming patterns, chord voicings, fingerpicking - Help musicians understand WHY things sound the way they do - Suggest progressions that work with whatever the user is currently playing - Adjust depth to the user — explain basics if they seem new, go deep if they ask for it - Point out interesting connections: "that Dm7 works here because it's the ii chord in C major" STYLE: - Keep responses focused and practical — this is a live jam, not a classroom - Use plain text, not markdown. Short paragraphs. Bullet points with "-" are fine. - If someone asks about the current chord or key, use the live context above - Max ~150 words unless someone asks for a deep dive` } // ── Quick-action chips ──────────────────────────────────────────────────────── const CHIPS = [ { label: 'What should I practice?', msg: 'Based on what I\'m playing right now, what\'s the most useful thing I could practice?' }, { label: 'Explain current chord', msg: 'Explain the current chord I\'m playing — what it is, why it sounds the way it does, and where it tends to appear.' }, { label: 'Scales that work here', msg: 'What scales work over the current key and chord? Which notes sound best to improvise with?' }, { label: 'Suggest a progression', msg: 'Suggest a chord progression that fits the current key. Give me something interesting to try.' }, { label: 'Technique tip', msg: 'Give me one technique tip — something I can work on in the next few minutes to sound better.' }, { label: 'Why does this sound good?', msg: 'Looking at my recent chord history, why do these chords sound good together? What\'s the music theory behind it?' }, ] // ── Simple text renderer (bold + line breaks) ───────────────────────────────── function MessageText({ text }) { const lines = text.split('\n') return (
{lines.map((line, i) => { if (!line.trim()) return
// Bold: **text** const parts = line.split(/(\*\*[^*]+\*\*)/) return (

{parts.map((part, j) => part.startsWith('**') && part.endsWith('**') ? {part.slice(2, -2)} : part )}

) })}
) } // ── Main component ──────────────────────────────────────────────────────────── export default function MusicTeacher({ keyInfo, currentChord, bpm, chordHistory }) { const [open, setOpen] = useState(false) const [apiKey, setApiKey] = useState(() => localStorage.getItem(LS_KEY) ?? '') const [showKeyInput, setShowKeyInput] = useState(false) const [messages, setMessages] = useState([]) // [{role, content}] const [input, setInput] = useState('') const [loading, setLoading] = useState(false) const [streaming, setStreaming] = useState('') // partial response being streamed const [error, setError] = useState(null) const scrollRef = useRef(null) const inputRef = useRef(null) const abortRef = useRef(null) // Always scroll to bottom on new content useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight } }, [messages, streaming]) // Focus input when panel opens useEffect(() => { if (open && apiKey && inputRef.current) { setTimeout(() => inputRef.current?.focus(), 50) } }, [open, apiKey]) function saveKey(k) { setApiKey(k) localStorage.setItem(LS_KEY, k) } function clearKey() { setApiKey('') localStorage.removeItem(LS_KEY) setShowKeyInput(true) } const sendMessage = useCallback(async (userText) => { if (!userText.trim() || loading || !apiKey) return setError(null) const userMsg = { role: 'user', content: userText.trim() } const nextMessages = [...messages, userMsg] setMessages(nextMessages) setInput('') setLoading(true) setStreaming('') const context = { keyInfo, currentChord, bpm, chordHistory } try { const ctrl = new AbortController() abortRef.current = ctrl const res = await fetch(API_URL, { method: 'POST', signal: ctrl.signal, headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'anthropic-dangerous-direct-browser-access': 'true', 'content-type': 'application/json', }, body: JSON.stringify({ model: MODEL, max_tokens: 1024, stream: true, system: buildSystemPrompt(context), messages: nextMessages, }), }) if (!res.ok) { const body = await res.json().catch(() => ({})) throw new Error(body?.error?.message ?? `API error ${res.status}`) } const reader = res.body.getReader() const decoder = new TextDecoder() let full = '' while (true) { const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value, { stream: true }) for (const line of chunk.split('\n')) { if (!line.startsWith('data: ')) continue const data = line.slice(6).trim() if (data === '[DONE]' || !data) continue try { const ev = JSON.parse(data) if (ev.type === 'content_block_delta' && ev.delta?.type === 'text_delta') { full += ev.delta.text setStreaming(full) } } catch {} } } setMessages(prev => [...prev, { role: 'assistant', content: full }]) setStreaming('') } catch (err) { if (err.name !== 'AbortError') { setError(err.message) } } finally { setLoading(false) abortRef.current = null } }, [messages, loading, apiKey, keyInfo, currentChord, bpm, chordHistory]) function stopGeneration() { abortRef.current?.abort() if (streaming) { setMessages(prev => [...prev, { role: 'assistant', content: streaming }]) setStreaming('') } setLoading(false) } function handleKeyDown(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() sendMessage(input) } } const hasKey = apiKey.trim().length > 0 // Dot: purple when API key set, gray otherwise const dotClass = hasKey ? 'bg-accent' : 'bg-gray-700' return (
{/* ── Header ─────────────────────────────────────────────────────────── */}
{/* Key indicator */}
{/* ── Body ───────────────────────────────────────────────────────────── */} {open && (
{/* API key input (shown when no key or user wants to change) */} {(!hasKey || showKeyInput) && (

Enter your Anthropic API key to enable the music teacher. Stored locally on your device only.

setApiKey(e.target.value)} placeholder="sk-ant-..." className="flex-1 px-2.5 py-1.5 bg-surface border border-border rounded-lg text-xs text-gray-300 focus:outline-none focus:border-accent font-mono" /> {hasKey && ( )}
)} {hasKey && ( <> {/* Live session context strip */}
Now: {keyInfo ? ( {keyInfo.root} {keyInfo.mode} ) : ( no key )} · {currentChord ? ( {currentChord.name} ) : ( no chord )} · {bpm ? `${Math.round(bpm)} bpm` : '— bpm'} {messages.length > 0 && ( )}
{/* Chat messages */} {messages.length > 0 || streaming ? (
{messages.map((m, i) => (
{m.role === 'user' ? (
{m.content}
) : (
)}
))} {streaming && (
)} {error && (
{error}
)}
) : ( /* Quick-action chips (shown when no chat history yet) */

Ask something

{CHIPS.map(chip => ( ))}
)} {/* Input row */}