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:
1 parent
816f8fbd2f
commit
e0ce9e4b6c
4 files changed
+304
-33
No files matched your search
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user