import { useState, useEffect, useRef } from 'react' import { api } from '../utils/api' import { X, Plus, Trash2, MoveUp, MoveDown, Code } from 'lucide-react' import PlaceholderPicker from './PlaceholderPicker' import Markdown from '../utils/Markdown' /** Debug aus erfolgreicher Ausführung oder aus FastAPI-Fehlerdetail */ function extractExecutionDebug(testResult) { if (!testResult) return null const raw = testResult.debug ?? testResult if (raw && typeof raw === 'object' && raw.detail && raw.detail.debug) { return { ...raw.detail.debug, ...raw.detail } } return raw } /** Vollständiger Prompt-Text für die Anzeige (Basis oder Pipeline-Stufen) */ function buildPromptTextForDisplay(testResult) { if (!testResult || testResult.error) return '' const dbg = extractExecutionDebug(testResult) const t = testResult.type if (t === 'base' && dbg?.final_prompt) return dbg.final_prompt if (t === 'pipeline' && dbg?.stages?.length) { const parts = [] for (const st of dbg.stages) { for (const p of st.prompts || []) { const label = p.source === 'reference' ? `Stage ${st.stage} · Ref: ${p.ref_slug || '?'}` : `Stage ${st.stage} · Inline` const text = p.final_prompt || (p.ref_debug && typeof p.ref_debug === 'object' ? p.ref_debug.final_prompt : '') || '' if (text) parts.push(`── ${label} ──\n${text}`) } } return parts.join('\n\n') } return dbg?.final_prompt || '' } function sanitizeDebugForPanel(dbg, resultType) { if (!dbg) return {} const o = { ...dbg } delete o.final_prompt delete o.template delete o.ai_response_preview if (o.stages && resultType === 'pipeline') { o.stages = o.stages.map((s) => ({ stage: s.stage, available_variables: s.available_variables, output: s.output, prompts: (s.prompts || []).map((p) => ({ source: p.source, ref_slug: p.ref_slug, output_key: p.output_key, context_var_key: p.context_var_key, resolved_placeholders: p.resolved_placeholders || p.ref_debug?.resolved_placeholders, unresolved_placeholders: p.unresolved_placeholders || p.ref_debug?.unresolved_placeholders, warnings: p.warnings || p.ref_debug?.warnings, ai_response_length: p.ai_response_length })) })) } return o } function renderTestOutput(testResult) { if (!testResult) return null if (testResult.error) { const d = testResult.debug?.detail const msg = typeof d === 'string' ? d : d?.error || d?.message || testResult.error_message || 'Unbekannter Fehler' return (
{JSON.stringify(val, null, 2)}
)}
{JSON.stringify(parsed, null, 2)}
)
} catch {
return proseBox({out})
}
}
return proseBox(
{JSON.stringify(out, null, 2)}
)
}
return {String(out)}
}
/**
* Unified Prompt Editor Modal (Issue #28 Phase 3)
*
* Supports both prompt types:
* - Base: Single reusable template
* - Pipeline: Multi-stage workflow with dynamic stages
*/
export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
const [name, setName] = useState('')
const [slug, setSlug] = useState('')
const [displayName, setDisplayName] = useState('')
const [description, setDescription] = useState('')
const [type, setType] = useState('pipeline') // 'base' or 'pipeline'
const [category, setCategory] = useState('ganzheitlich')
const [active, setActive] = useState(true)
const [sortOrder, setSortOrder] = useState(0)
// Base prompt fields
const [template, setTemplate] = useState('')
const [outputFormat, setOutputFormat] = useState('text')
// Pipeline prompt fields
const [stages, setStages] = useState([
{
stage: 1,
prompts: []
}
])
// Available prompts for reference selection
const [availablePrompts, setAvailablePrompts] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [showPlaceholderPicker, setShowPlaceholderPicker] = useState(false)
const [pickerTarget, setPickerTarget] = useState(null) // 'base' or {stage, promptIdx}
const [cursorPosition, setCursorPosition] = useState(null) // Track cursor position for insertion
const baseTemplateRef = useRef(null)
const stageTemplateRefs = useRef({}) // Map of stage_promptIdx -> ref
// Test functionality
const [testing, setTesting] = useState(false)
const [testResult, setTestResult] = useState(null)
const [showDebug, setShowDebug] = useState(false)
/** 'response' | 'prompt' | 'debug' */
const [testResultTab, setTestResultTab] = useState('response')
useEffect(() => {
loadAvailablePrompts()
if (prompt) {
// Edit mode
setName(prompt.name || '')
setSlug(prompt.slug || '')
setDisplayName(prompt.display_name || '')
setDescription(prompt.description || '')
setType(prompt.type || 'pipeline')
setCategory(prompt.category || 'ganzheitlich')
setActive(prompt.active ?? true)
setSortOrder(prompt.sort_order || 0)
setTemplate(prompt.template || '')
setOutputFormat(prompt.output_format || 'text')
// Parse stages if editing pipeline
if (prompt.type === 'pipeline' && prompt.stages) {
try {
const parsedStages = typeof prompt.stages === 'string'
? JSON.parse(prompt.stages)
: prompt.stages
setStages(parsedStages.length > 0 ? parsedStages : [{ stage: 1, prompts: [] }])
} catch (e) {
console.error('Failed to parse stages:', e)
setStages([{ stage: 1, prompts: [] }])
}
}
}
}, [prompt])
const loadAvailablePrompts = async () => {
try {
const prompts = await api.listAdminPrompts()
setAvailablePrompts(prompts)
} catch (e) {
setError('Fehler beim Laden der Prompts: ' + e.message)
}
}
const addStage = () => {
const nextStageNum = Math.max(...stages.map(s => s.stage), 0) + 1
setStages([...stages, { stage: nextStageNum, prompts: [] }])
}
const removeStage = (stageNum) => {
if (stages.length === 1) {
setError('Mindestens eine Stage erforderlich')
return
}
setStages(stages.filter(s => s.stage !== stageNum))
}
const moveStage = (stageNum, direction) => {
const idx = stages.findIndex(s => s.stage === stageNum)
if (idx === -1) return
const newStages = [...stages]
if (direction === 'up' && idx > 0) {
[newStages[idx], newStages[idx - 1]] = [newStages[idx - 1], newStages[idx]]
} else if (direction === 'down' && idx < newStages.length - 1) {
[newStages[idx], newStages[idx + 1]] = [newStages[idx + 1], newStages[idx]]
}
// Renumber stages
newStages.forEach((s, i) => s.stage = i + 1)
setStages(newStages)
}
const addPromptToStage = (stageNum) => {
setStages(stages.map(s => {
if (s.stage === stageNum) {
return {
...s,
prompts: [...s.prompts, {
source: 'inline',
template: '',
output_key: `output_${Date.now()}`,
output_format: 'text'
}]
}
}
return s
}))
}
const removePromptFromStage = (stageNum, promptIdx) => {
setStages(stages.map(s => {
if (s.stage === stageNum) {
return {
...s,
prompts: s.prompts.filter((_, i) => i !== promptIdx)
}
}
return s
}))
}
const updateStagePrompt = (stageNum, promptIdx, field, value) => {
setStages(stages.map(s => {
if (s.stage === stageNum) {
const newPrompts = [...s.prompts]
newPrompts[promptIdx] = { ...newPrompts[promptIdx], [field]: value }
return { ...s, prompts: newPrompts }
}
return s
}))
}
const handleSave = async () => {
// Validation
if (!name.trim() || !slug.trim()) {
setError('Name und Slug sind Pflichtfelder')
return
}
if (type === 'base' && !template.trim()) {
setError('Basis-Prompts benötigen ein Template')
return
}
if (type === 'pipeline' && stages.length === 0) {
setError('Pipeline-Prompts benötigen mindestens eine Stage')
return
}
if (type === 'pipeline') {
// Validate all stages have at least one prompt
const emptyStages = stages.filter(s => s.prompts.length === 0)
if (emptyStages.length > 0) {
setError(`Stage ${emptyStages[0].stage} hat keine Prompts`)
return
}
// Validate all prompts have required fields
for (const stage of stages) {
for (const p of stage.prompts) {
if (!p.output_key) {
setError(`Stage ${stage.stage}: Output-Key fehlt`)
return
}
if (p.source === 'inline' && !p.template) {
setError(`Stage ${stage.stage}: Inline-Prompt ohne Template`)
return
}
if (p.source === 'reference' && !p.slug) {
setError(`Stage ${stage.stage}: Referenz-Prompt ohne Slug`)
return
}
}
}
}
setLoading(true)
setError(null)
try {
const data = {
name,
slug,
display_name: displayName,
description,
type,
category,
active,
sort_order: sortOrder,
output_format: outputFormat
}
if (type === 'base') {
data.template = template
data.stages = null
} else {
data.template = null
data.stages = stages
}
if (prompt?.id) {
await api.updateUnifiedPrompt(prompt.id, data)
} else {
await api.createUnifiedPrompt(data)
}
onSave()
} catch (e) {
setError(e.message)
} finally {
setLoading(false)
}
}
const handleExport = () => {
// Export complete prompt configuration as JSON
const exportData = {
name,
slug,
display_name: displayName,
description,
type,
category,
active,
sort_order: sortOrder,
output_format: outputFormat,
template: type === 'base' ? template : null,
stages: type === 'pipeline' ? stages : null
}
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `prompt-${slug || 'new'}-${new Date().toISOString().split('T')[0]}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
const handleTest = async () => {
// Can only test existing prompts (need slug in database)
if (!prompt?.slug) {
setError('Bitte erst speichern, dann testen')
return
}
setTesting(true)
setError(null)
setTestResult(null)
try {
const result = await api.executeUnifiedPrompt(prompt.slug, null, null, true)
setTestResult(result)
setTestResultTab('response')
setShowDebug(true)
} catch (e) {
// Show error AND try to extract debug info from error
const errorMsg = e.message
let debugData = null
// Try to parse error message for embedded debug info
try {
const parsed = JSON.parse(errorMsg)
if (parsed.detail) {
const d = parsed.detail
const msg =
typeof d === 'string'
? d
: d.error || d.message || d.msg || JSON.stringify(d).slice(0, 500)
setError('Test-Fehler: ' + msg)
debugData = parsed
} else {
setError('Test-Fehler: ' + errorMsg)
}
} catch {
setError('Test-Fehler: ' + errorMsg)
}
// Set result with error info so debug viewer shows it
setTestResult({
error: true,
error_message: errorMsg,
debug: debugData || { error: errorMsg }
})
setTestResultTab('response')
setShowDebug(true) // ALWAYS show debug on test, even on error
} finally {
setTesting(false)
}
}
const handleExportPlaceholders = () => {
if (!testResult) return
// Extract all placeholder data from test result
const raw = testResult.debug || testResult
// API-Fehler: FastAPI liefert { detail: { error, debug: { resolved_placeholders, ... } } }
const debug =
raw && typeof raw === 'object' && raw.detail && raw.detail.debug
? { ...raw.detail.debug, ...raw.detail }
: raw
const exportData = {
export_date: new Date().toISOString(),
prompt_slug: prompt?.slug || 'unknown',
prompt_name: name || 'Unnamed Prompt',
placeholders: {}
}
// For pipeline prompts, collect from all stages
if (debug.stages && Array.isArray(debug.stages)) {
debug.stages.forEach(stage => {
exportData.placeholders[`stage_${stage.stage}`] = {
available_variables: stage.available_variables || [],
prompts: stage.prompts?.map(p => ({
source: p.source,
resolved: p.resolved_placeholders || p.ref_debug?.resolved_placeholders || {},
unresolved: p.unresolved_placeholders || p.ref_debug?.unresolved_placeholders || []
})) || []
}
})
}
// For base prompts or direct execution
if (debug.resolved_placeholders) {
exportData.placeholders.resolved = debug.resolved_placeholders
}
if (debug.unresolved_placeholders) {
exportData.placeholders.unresolved = debug.unresolved_placeholders
}
if (debug.available_variables) {
exportData.available_variables = debug.available_variables
}
if (debug.initial_variables) {
exportData.initial_variables = debug.initial_variables
}
// Download as JSON
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `placeholders-${prompt?.slug || 'test'}-${new Date().toISOString().split('T')[0]}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
return (
{buildPromptTextForDisplay(testResult) || '(Kein Prompt-Text in der Antwort — evtl. Fehler vor Ausführung.)'}
)}
{testResultTab === 'debug' && (
{JSON.stringify(
sanitizeDebugForPanel(
extractExecutionDebug(testResult),
testResult.error ? null : testResult.type
),
null,
2
)}
)}