feat: Text generation suggestions via Gemini models

Backend:
- New text_gen.py module with Gemini text generation client
  - suggest_prompts(): generates 3 prompt suggestions from user input
  - get_text_gen_config(): loads config from .env (model, API key, safety)
  - get_available_models(): returns list of Gemini text models
  - Safety settings: harassment, hate_speech, sexually_explicit, dangerous
    Configurable via TEXT_GEN_SAFETY_* env vars (default: BLOCK_ONLY_HIGH)
- New endpoints:
  * POST /api/text-gen/suggest — generates 3 prompt suggestions
  * GET /api/text-gen/models — lists available text models
  * GET /api/text-gen/config — returns safe config (API key masked)
  * POST /api/text-gen/config — updates config in-memory
- Default model: gemini-2.5-flash
- Fallback API key: LYRIA_API_KEY if TEXT_GEN_API_KEY not set
- System prompt: expert music producer persona for rich musical detail

Frontend:
- New SuggestionsModal component for displaying generated suggestions
  - Auto-fetches when opened
  - Shows loading spinner, error state, or suggestion list
  - Each suggestion clickable to populate the prompt field
  - Hover effect: 'Click to use this prompt →'
- Generator component updated with 'Get Suggestions ✨' button
  - Appears next to prompt label in both Basic and Advanced modes
  - Disabled when no input present
  - Clicking suggestion populates the prompt/structured fields
- SettingsModal expanded with Text Generation section (collapsible)
  - Same pattern as Image Generation: model, API key status, safety filters
  - Editable dropdowns with Save Changes button
  - Refactored to use shared useGenSettings hook for both sections

Models Available:
- gemini-2.5-flash (GA, default) — best price-to-performance
- gemini-2.5-pro (GA) — highest quality reasoning
- gemini-3.5-flash (Preview) — next-gen Flash
- gemini-3.1-flash (Preview) — latest with thinking

Architecture KB and Tasks KB updated.
This commit is contained in:
rin.kagamine committed 2026-05-29 22:37:35 -04:00
1 parent 70beb89ad9
commit e687f7d457
5 files changed
+547 -168

No files matched your search

+38 -6
View File
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'
import StructuredPromptEditor, { PromptStructure } from './StructuredPromptEditor'
import SuggestionsModal from './SuggestionsModal'
interface ModelInfo {
name: string
@@ -23,6 +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')
@@ -181,9 +183,17 @@ export default function Generator({ onGenerate }: Props) {
{/* Basic Prompt */}
{(promptMode === 'basic' || !isPro) && (
<div>
<label className="block text-xs text-lyria-muted mb-1.5">
Prompt
</label>
<div className="flex items-center justify-between mb-1.5">
<label className="text-xs text-lyria-muted">Prompt</label>
<button
type="button"
onClick={() => setSuggestionsOpen(true)}
disabled={!prompt.trim()}
className="text-xs text-lyria-accent hover:text-lyria-accent/80 transition-colors disabled:opacity-40"
>
Get Suggestions ✨
</button>
</div>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
@@ -196,9 +206,17 @@ export default function Generator({ onGenerate }: Props) {
{/* Advanced Prompt */}
{promptMode === 'advanced' && isPro && (
<div>
<label className="block text-xs text-lyria-muted mb-1.5">
Structured Prompt
</label>
<div className="flex items-center justify-between mb-1.5">
<label className="text-xs text-lyria-muted">Structured Prompt</label>
<button
type="button"
onClick={() => setSuggestionsOpen(true)}
disabled={Object.values(structuredPrompt).every(v => !v?.trim())}
className="text-xs text-lyria-accent hover:text-lyria-accent/80 transition-colors disabled:opacity-40"
>
Get Suggestions ✨
</button>
</div>
<StructuredPromptEditor
value={structuredPrompt}
onChange={setStructuredPrompt}
@@ -218,6 +236,20 @@ export default function Generator({ onGenerate }: Props) {
{isLoading ? 'Generating...' : 'Generate'}
</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>
)
}