feat: Maestro Mode - context-aware prompt generation

- Backend: POST /api/text-gen/maestro endpoint with dual modes
  - Empty prompt → generate from scratch (temp 1.0)
  - Non-empty prompt → complete/extend (temp 0.85)
  - structured=true → returns JSON fields for Advanced/Pro mode
  - Three system prompts: MAESTRO_GENERATE, MAESTRO_COMPLETE, MAESTRO_STRUCTURED
  - Trailing sentence cleanup for plain text mode
  - JSON fallback parsing for structured mode

- Frontend: MaestroButton component
  - Context-aware labels: ♪ Maestro Mode (empty) / ♪ Keep Composing (has content)
  - Loading states: animating ♩ with Dreaming.../Composing...
  - structured prop for Pro mode → populates structured fields
  - onStructuredResult callback merges fields into StructuredPrompt

- UX: Replaced 'Get Suggestions ✨' with Maestro Mode
  - Suggestions required text in the box; Maestro works with or without
  - Single button, context-aware behavior, cleaner UI
  - Advanced/Pro mode gets structured fields from Maestro
  - Removed SuggestionsModal dependency

- Rename: 'Compose for Me' → 'Keep Composing'
This commit is contained in:
rin.kagamine committed 2026-05-31 10:13:43 -04:00
1 parent 816f8fbd2f
commit e0ce9e4b6c
4 files changed
+304 -33

No files matched your search

+33 -31
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import StructuredPromptEditor, { PromptStructure } from './StructuredPromptEditor'
import SuggestionsModal from './SuggestionsModal'
import MaestroButton from './MaestroButton'
interface ModelInfo {
name: string
@@ -24,7 +24,7 @@ export default function Generator({ onGenerate }: Props) {
const [models, setModels] = useState<Record<string, ModelInfo>>({})
const [promptMode, setPromptMode] = useState<PromptMode>('basic')
const [structuredPrompt, setStructuredPrompt] = useState<PromptStructure>({})
const [suggestionsOpen, setSuggestionsOpen] = useState(false)
useEffect(() => {
fetch('/api/models')
@@ -185,14 +185,16 @@ export default function Generator({ onGenerate }: Props) {
<div>
<div className="flex items-center justify-between mb-1.5">
<label className="text-xs text-kithara-muted">Prompt</label>
<button
type="button"
onClick={() => setSuggestionsOpen(true)}
disabled={!prompt.trim()}
className="text-xs text-kithara-accent hover:text-kithara-accent/80 transition-colors disabled:opacity-40"
>
Get Suggestions ✨
</button>
<MaestroButton
prompt={prompt}
onResult={(result, mode) => {
setPrompt(result)
if (mode === 'generate') {
setError('')
}
}}
onError={(msg) => setError(msg)}
/>
</div>
<textarea
value={prompt}
@@ -208,14 +210,26 @@ export default function Generator({ onGenerate }: Props) {
<div>
<div className="flex items-center justify-between mb-1.5">
<label className="text-xs text-kithara-muted">Structured Prompt</label>
<button
type="button"
onClick={() => setSuggestionsOpen(true)}
disabled={Object.values(structuredPrompt).every(v => !v?.trim())}
className="text-xs text-kithara-accent hover:text-kithara-accent/80 transition-colors disabled:opacity-40"
>
Get Suggestions ✨
</button>
<MaestroButton
prompt={Object.values(structuredPrompt).filter(Boolean).join(' ')}
structured={isPro}
onResult={(result, mode) => {
// Fallback: plain text mode for non-Pro, put result in 'additional'
setStructuredPrompt(prev => ({ ...prev, additional: result }))
if (mode === 'generate') setError('')
}}
onStructuredResult={(fields, mode) => {
// Structured mode: merge fields into structuredPrompt, preserving user edits
setStructuredPrompt(prev => ({
...prev,
...Object.fromEntries(
Object.entries(fields).filter(([_, v]) => v?.trim())
),
}))
if (mode === 'generate') setError('')
}}
onError={(msg) => setError(msg)}
/>
</div>
<StructuredPromptEditor
value={structuredPrompt}
@@ -237,19 +251,7 @@ export default function Generator({ onGenerate }: Props) {
</button>
</form>
<SuggestionsModal
isOpen={suggestionsOpen}
onClose={() => setSuggestionsOpen(false)}
onSelect={(suggestion) => {
if (promptMode === 'basic' || !isPro) {
setPrompt(suggestion)
} else {
// For advanced mode, put it in the 'style' field or additional
setStructuredPrompt(prev => ({ ...prev, additional: suggestion }))
}
}}
userInput={promptMode === 'basic' || !isPro ? prompt : Object.values(structuredPrompt).filter(Boolean).join(' ')}
/>
</div>
)
}
+98
View File
@@ -0,0 +1,98 @@
import { useState } from 'react'
import type { PromptStructure } from './StructuredPromptEditor'
interface Props {
/** Current prompt text. Empty = generate mode, non-empty = complete mode. */
prompt: string
/** Called with the Maestro result — the generated or completed prompt. */
onResult: (result: string, mode: 'generate' | 'complete') => void
/** Called when Maestro encounters an error. */
onError: (message: string) => void
/** Optional CSS class for the button container. */
className?: string
/**
* If true, send structured=true and return structured fields.
* The onStructuredResult callback will be called instead of onResult.
*/
structured?: boolean
/** Called with structured fields when structured mode returns. */
onStructuredResult?: (fields: PromptStructure, mode: 'generate' | 'complete') => void
}
export default function MaestroButton({
prompt,
onResult,
onError,
className = '',
structured = false,
onStructuredResult,
}: Props) {
const [isMaestroing, setIsMaestroing] = useState(false)
const handleMaestro = async () => {
setIsMaestroing(true)
try {
const res = await fetch('/api/text-gen/maestro', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
hint: prompt.trim(),
structured,
}),
})
if (!res.ok) {
try {
const errorData = await res.json()
throw new Error(errorData.detail || 'Maestro generation failed')
} catch (parseErr) {
if (parseErr instanceof Error && parseErr.message !== 'Maestro generation failed') throw parseErr
throw new Error('Maestro generation failed')
}
}
const data = await res.json()
if (structured && data.fields && onStructuredResult) {
onStructuredResult(data.fields as PromptStructure, data.mode)
} else {
onResult(data.prompt, data.mode)
}
} catch (err) {
onError(err instanceof Error ? err.message : 'Maestro generation failed')
} finally {
setIsMaestroing(false)
}
}
const mode = prompt.trim() ? 'complete' : 'generate'
const label = mode === 'generate'
? '♪ Maestro Mode'
: '♪ Keep Composing'
return (
<button
type="button"
onClick={handleMaestro}
disabled={isMaestroing}
className={`${className} flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-all duration-200 ${
isMaestroing
? 'bg-kithara-accent/20 text-kithara-accent animate-pulse'
: 'bg-kithara-accent/10 text-kithara-accent hover:bg-kithara-accent/25 border border-kithara-accent/30 hover:border-kithara-accent/50'
}`}
title={mode === 'generate'
? "Let Maestro write a creative prompt for you"
: "Let Maestro complete your prompt with musical details"
}
>
{isMaestroing ? (
<>
<span className="inline-block animate-spin">♩</span>
{mode === 'generate' ? 'Dreaming...' : 'Composing...'}
</>
) : (
label
)}
</button>
)
}