Verbesserung für Tests von Prompts #80
|
|
@ -278,7 +278,11 @@ async def execute_base_prompt(
|
||||||
|
|
||||||
if enable_debug:
|
if enable_debug:
|
||||||
debug_info['template'] = template
|
debug_info['template'] = template
|
||||||
debug_info['final_prompt'] = prompt_text[:500] + ('...' if len(prompt_text) > 500 else '')
|
# Volltext für Test-UI (Admin); sehr große Prompts nur weich begrenzen
|
||||||
|
_max = 512 * 1024
|
||||||
|
debug_info['final_prompt'] = (
|
||||||
|
prompt_text if len(prompt_text) <= _max else prompt_text[:_max] + "\n… [gekürzt, >512KB]"
|
||||||
|
)
|
||||||
debug_info['available_variables'] = list(variables.keys())
|
debug_info['available_variables'] = list(variables.keys())
|
||||||
|
|
||||||
# Call AI
|
# Call AI
|
||||||
|
|
@ -397,7 +401,10 @@ async def execute_pipeline_prompt(
|
||||||
if enable_debug:
|
if enable_debug:
|
||||||
prompt_debug['source'] = 'inline'
|
prompt_debug['source'] = 'inline'
|
||||||
prompt_debug['template'] = template
|
prompt_debug['template'] = template
|
||||||
prompt_debug['final_prompt'] = prompt_text[:500] + ('...' if len(prompt_text) > 500 else '')
|
_max = 512 * 1024
|
||||||
|
prompt_debug['final_prompt'] = (
|
||||||
|
prompt_text if len(prompt_text) <= _max else prompt_text[:_max] + "\n… [gekürzt, >512KB]"
|
||||||
|
)
|
||||||
prompt_debug.update(placeholder_debug)
|
prompt_debug.update(placeholder_debug)
|
||||||
|
|
||||||
response = await openrouter_call_func(prompt_text)
|
response = await openrouter_call_func(prompt_text)
|
||||||
|
|
|
||||||
|
|
@ -937,27 +937,60 @@ def export_placeholder_catalog_zip(
|
||||||
|
|
||||||
# ── KI-Assisted Prompt Engineering ───────────────────────────────────────────
|
# ── KI-Assisted Prompt Engineering ───────────────────────────────────────────
|
||||||
|
|
||||||
async def call_openrouter(prompt: str, max_tokens: int = 1500) -> str:
|
async def call_openrouter(prompt: str, max_tokens: int = 4096) -> str:
|
||||||
"""Call OpenRouter API to get AI response."""
|
"""Call OpenRouter API to get AI response."""
|
||||||
if not OPENROUTER_KEY:
|
if not OPENROUTER_KEY:
|
||||||
raise HTTPException(status_code=500, detail="OpenRouter API key not configured")
|
raise HTTPException(status_code=500, detail="OpenRouter API key not configured")
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
try:
|
||||||
resp = await client.post(
|
async with httpx.AsyncClient() as client:
|
||||||
"https://openrouter.ai/api/v1/chat/completions",
|
resp = await client.post(
|
||||||
headers={"Authorization": f"Bearer {OPENROUTER_KEY}"},
|
"https://openrouter.ai/api/v1/chat/completions",
|
||||||
json={
|
headers={"Authorization": f"Bearer {OPENROUTER_KEY}"},
|
||||||
"model": OPENROUTER_MODEL,
|
json={
|
||||||
"messages": [{"role": "user", "content": prompt}],
|
"model": OPENROUTER_MODEL,
|
||||||
"max_tokens": max_tokens
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
},
|
"max_tokens": max_tokens
|
||||||
timeout=60.0
|
},
|
||||||
|
timeout=120.0
|
||||||
|
)
|
||||||
|
except httpx.TimeoutException as e:
|
||||||
|
raise HTTPException(status_code=504, detail=f"OpenRouter-Zeitüberschreitung: {e}") from e
|
||||||
|
except httpx.RequestError as e:
|
||||||
|
raise HTTPException(status_code=502, detail=f"OpenRouter-Verbindungsfehler: {e}") from e
|
||||||
|
|
||||||
|
if resp.status_code != 200:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=resp.status_code,
|
||||||
|
detail=f"OpenRouter API error ({resp.status_code}): {resp.text[:2000]}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if resp.status_code != 200:
|
try:
|
||||||
raise HTTPException(status_code=resp.status_code, detail=f"OpenRouter API error: {resp.text}")
|
data = resp.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502,
|
||||||
|
detail=f"OpenRouter lieferte kein JSON: {resp.text[:800]}",
|
||||||
|
)
|
||||||
|
|
||||||
return resp.json()['choices'][0]['message']['content'].strip()
|
choices = data.get("choices") or []
|
||||||
|
if not choices:
|
||||||
|
err_snip = json.dumps(data.get("error") or data, ensure_ascii=False)[:1200]
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502,
|
||||||
|
detail=f"OpenRouter ohne choices in der Antwort: {err_snip}",
|
||||||
|
)
|
||||||
|
|
||||||
|
message = choices[0].get("message") or {}
|
||||||
|
content = message.get("content")
|
||||||
|
if content is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502,
|
||||||
|
detail="OpenRouter-Antwort ohne message.content (z. B. Tool-Calls oder abweichendes Schema).",
|
||||||
|
)
|
||||||
|
if not isinstance(content, str):
|
||||||
|
content = str(content)
|
||||||
|
return content.strip()
|
||||||
|
|
||||||
|
|
||||||
def collect_example_data(profile_id: str, data_categories: list[str]) -> dict:
|
def collect_example_data(profile_id: str, data_categories: list[str]) -> dict:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,264 @@
|
||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef, useLayoutEffect } from 'react'
|
||||||
import { api } from '../utils/api'
|
import { api } from '../utils/api'
|
||||||
import { X, Plus, Trash2, MoveUp, MoveDown, Code } from 'lucide-react'
|
import { X, Plus, Trash2, MoveUp, MoveDown, Code } from 'lucide-react'
|
||||||
import PlaceholderPicker from './PlaceholderPicker'
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Langer Inhalt: zunächst auf maxRem eingeklappt, „Mehr/Weniger anzeigen“ */
|
||||||
|
function ExpandableCollapsible({ children, contentKey, maxRem = 28 }) {
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
const wrapRef = useRef(null)
|
||||||
|
const [showToggle, setShowToggle] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExpanded(false)
|
||||||
|
}, [contentKey])
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const el = wrapRef.current
|
||||||
|
if (!el) return
|
||||||
|
if (expanded) {
|
||||||
|
setShowToggle(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setShowToggle(el.scrollHeight > el.clientHeight + 2)
|
||||||
|
}, [contentKey, expanded])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
ref={wrapRef}
|
||||||
|
style={{
|
||||||
|
maxHeight: expanded ? 'none' : `${maxRem}rem`,
|
||||||
|
overflow: expanded ? 'visible' : 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{showToggle && (
|
||||||
|
<div style={{ marginTop: 10, textAlign: 'center' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ fontSize: 12, padding: '6px 14px' }}
|
||||||
|
onClick={() => setExpanded((e) => !e)}
|
||||||
|
>
|
||||||
|
{expanded ? 'Weniger anzeigen' : 'Mehr anzeigen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div style={{ color: 'var(--danger)', fontSize: 14, lineHeight: 1.5 }}>
|
||||||
|
{typeof msg === 'string' ? msg : JSON.stringify(msg, null, 2)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = testResult.output
|
||||||
|
const fmt = testResult.output_format
|
||||||
|
const typ = testResult.type
|
||||||
|
|
||||||
|
if (out == null) {
|
||||||
|
return <span style={{ color: 'var(--text3)' }}>Keine Ausgabe</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
const proseBox = (content, contentKey) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 16,
|
||||||
|
background: 'var(--bg)',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 1.55,
|
||||||
|
color: 'var(--text1)',
|
||||||
|
textAlign: 'left'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{contentKey != null ? (
|
||||||
|
<ExpandableCollapsible contentKey={contentKey}>{content}</ExpandableCollapsible>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (typ === 'pipeline' && out && typeof out === 'object' && !Array.isArray(out)) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'grid', gap: 16 }}>
|
||||||
|
{Object.entries(out).map(([key, val]) => (
|
||||||
|
<div key={key}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--text2)',
|
||||||
|
marginBottom: 8,
|
||||||
|
textAlign: 'left'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{key}
|
||||||
|
</div>
|
||||||
|
{typeof val === 'string' ? (
|
||||||
|
proseBox(
|
||||||
|
<Markdown text={val} />,
|
||||||
|
`pipe-${key}-${val.length}-${val.slice(0, 120)}`
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<ExpandableCollapsible contentKey={`pipe-json-${key}-${JSON.stringify(val).slice(0, 200)}`} maxRem={22}>
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
padding: 12,
|
||||||
|
background: 'var(--bg)',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
fontSize: 12,
|
||||||
|
overflow: 'auto',
|
||||||
|
textAlign: 'left'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{JSON.stringify(val, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</ExpandableCollapsible>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof out === 'string') {
|
||||||
|
if (fmt === 'json') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(out)
|
||||||
|
const jsonStr = JSON.stringify(parsed, null, 2)
|
||||||
|
return (
|
||||||
|
<ExpandableCollapsible contentKey={`json-out-${jsonStr.length}-${jsonStr.slice(0, 80)}`} maxRem={26}>
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
padding: 12,
|
||||||
|
background: 'var(--bg)',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
fontSize: 12,
|
||||||
|
overflow: 'auto',
|
||||||
|
textAlign: 'left'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{jsonStr}
|
||||||
|
</pre>
|
||||||
|
</ExpandableCollapsible>
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return proseBox(
|
||||||
|
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>{out}</pre>,
|
||||||
|
`json-fail-${out.length}-${out.slice(0, 80)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return proseBox(<Markdown text={out} />, `text-out-${out.length}-${out.slice(0, 120)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof out === 'object') {
|
||||||
|
const jsonStr = JSON.stringify(out, null, 2)
|
||||||
|
return (
|
||||||
|
<ExpandableCollapsible contentKey={`obj-out-${jsonStr.length}-${jsonStr.slice(0, 120)}`} maxRem={26}>
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
padding: 12,
|
||||||
|
background: 'var(--bg)',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
fontSize: 12,
|
||||||
|
overflow: 'auto',
|
||||||
|
textAlign: 'left'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{jsonStr}
|
||||||
|
</pre>
|
||||||
|
</ExpandableCollapsible>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{String(out)}</span>
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unified Prompt Editor Modal (Issue #28 Phase 3)
|
* Unified Prompt Editor Modal (Issue #28 Phase 3)
|
||||||
|
|
@ -47,6 +304,8 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
|
||||||
const [testing, setTesting] = useState(false)
|
const [testing, setTesting] = useState(false)
|
||||||
const [testResult, setTestResult] = useState(null)
|
const [testResult, setTestResult] = useState(null)
|
||||||
const [showDebug, setShowDebug] = useState(false)
|
const [showDebug, setShowDebug] = useState(false)
|
||||||
|
/** 'response' | 'prompt' | 'debug' */
|
||||||
|
const [testResultTab, setTestResultTab] = useState('response')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAvailablePrompts()
|
loadAvailablePrompts()
|
||||||
|
|
@ -280,6 +539,7 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
|
||||||
try {
|
try {
|
||||||
const result = await api.executeUnifiedPrompt(prompt.slug, null, null, true)
|
const result = await api.executeUnifiedPrompt(prompt.slug, null, null, true)
|
||||||
setTestResult(result)
|
setTestResult(result)
|
||||||
|
setTestResultTab('response')
|
||||||
setShowDebug(true)
|
setShowDebug(true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Show error AND try to extract debug info from error
|
// Show error AND try to extract debug info from error
|
||||||
|
|
@ -290,7 +550,12 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(errorMsg)
|
const parsed = JSON.parse(errorMsg)
|
||||||
if (parsed.detail) {
|
if (parsed.detail) {
|
||||||
setError('Test-Fehler: ' + 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
|
debugData = parsed
|
||||||
} else {
|
} else {
|
||||||
setError('Test-Fehler: ' + errorMsg)
|
setError('Test-Fehler: ' + errorMsg)
|
||||||
|
|
@ -305,6 +570,7 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
|
||||||
error_message: errorMsg,
|
error_message: errorMsg,
|
||||||
debug: debugData || { error: errorMsg }
|
debug: debugData || { error: errorMsg }
|
||||||
})
|
})
|
||||||
|
setTestResultTab('response')
|
||||||
setShowDebug(true) // ALWAYS show debug on test, even on error
|
setShowDebug(true) // ALWAYS show debug on test, even on error
|
||||||
} finally {
|
} finally {
|
||||||
setTesting(false)
|
setTesting(false)
|
||||||
|
|
@ -315,7 +581,12 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
|
||||||
if (!testResult) return
|
if (!testResult) return
|
||||||
|
|
||||||
// Extract all placeholder data from test result
|
// Extract all placeholder data from test result
|
||||||
const debug = testResult.debug || testResult
|
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 = {
|
const exportData = {
|
||||||
export_date: new Date().toISOString(),
|
export_date: new Date().toISOString(),
|
||||||
prompt_slug: prompt?.slug || 'unknown',
|
prompt_slug: prompt?.slug || 'unknown',
|
||||||
|
|
@ -711,7 +982,7 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Debug Output */}
|
{/* Test-Ergebnis: Antwort · Prompt · Debug */}
|
||||||
{showDebug && testResult && (
|
{showDebug && testResult && (
|
||||||
<div style={{
|
<div style={{
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
|
|
@ -724,12 +995,14 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 12
|
marginBottom: 12,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 8
|
||||||
}}>
|
}}>
|
||||||
<h3 style={{ margin: 0, fontSize: 14, fontWeight: 600 }}>
|
<h3 style={{ margin: 0, fontSize: 14, fontWeight: 600 }}>
|
||||||
🔬 Debug-Info
|
Test-Ergebnis
|
||||||
</h3>
|
</h3>
|
||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
<button
|
<button
|
||||||
className="btn"
|
className="btn"
|
||||||
onClick={handleExportPlaceholders}
|
onClick={handleExportPlaceholders}
|
||||||
|
|
@ -739,31 +1012,120 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
|
||||||
background: 'var(--accent)',
|
background: 'var(--accent)',
|
||||||
color: 'white'
|
color: 'white'
|
||||||
}}
|
}}
|
||||||
title="Exportiere alle Platzhalter mit Werten als JSON"
|
title="Exportiere Platzhalter / Kontext als JSON"
|
||||||
>
|
>
|
||||||
📋 Platzhalter exportieren
|
📋 Kontext exportieren
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowDebug(false)}
|
onClick={() => setShowDebug(false)}
|
||||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4 }}
|
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4 }}
|
||||||
|
aria-label="Schließen"
|
||||||
>
|
>
|
||||||
<X size={16} color="var(--text3)" />
|
<X size={16} color="var(--text3)" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<pre style={{
|
|
||||||
fontSize: 11,
|
<div
|
||||||
fontFamily: 'monospace',
|
role="tablist"
|
||||||
background: 'var(--bg)',
|
style={{
|
||||||
padding: 12,
|
display: 'flex',
|
||||||
borderRadius: 6,
|
gap: 6,
|
||||||
overflow: 'auto',
|
marginBottom: 12,
|
||||||
maxHeight: 400,
|
flexWrap: 'wrap',
|
||||||
lineHeight: 1.5,
|
borderBottom: '1px solid var(--border)',
|
||||||
color: 'var(--text2)'
|
paddingBottom: 8
|
||||||
}}>
|
}}
|
||||||
{JSON.stringify(testResult.debug || testResult, null, 2)}
|
>
|
||||||
</pre>
|
{[
|
||||||
|
{ id: 'response', label: 'Antwort' },
|
||||||
|
{ id: 'prompt', label: 'Prompt (an LLM)' },
|
||||||
|
{ id: 'debug', label: 'Debug' }
|
||||||
|
].map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={testResultTab === tab.id}
|
||||||
|
onClick={() => setTestResultTab(tab.id)}
|
||||||
|
style={{
|
||||||
|
padding: '6px 12px',
|
||||||
|
fontSize: 12,
|
||||||
|
borderRadius: 6,
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
background: testResultTab === tab.id ? 'var(--accent)' : 'var(--surface)',
|
||||||
|
color: testResultTab === tab.id ? '#fff' : 'var(--text2)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontWeight: testResultTab === tab.id ? 600 : 400
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
style={{
|
||||||
|
minHeight: 120,
|
||||||
|
maxHeight: testResultTab === 'prompt' ? 480 : 420,
|
||||||
|
overflow: 'auto',
|
||||||
|
textAlign: 'left'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{testResultTab === 'response' && (
|
||||||
|
<div style={{ paddingRight: 4 }}>
|
||||||
|
{renderTestOutput(testResult)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{testResultTab === 'prompt' && (
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: 'ui-monospace, monospace',
|
||||||
|
lineHeight: 1.45,
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
color: 'var(--text1)',
|
||||||
|
padding: 12,
|
||||||
|
background: 'var(--bg)',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px solid var(--border)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{buildPromptTextForDisplay(testResult) || '(Kein Prompt-Text in der Antwort — evtl. Fehler vor Ausführung.)'}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{testResultTab === 'debug' && (
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 11,
|
||||||
|
fontFamily: 'ui-monospace, monospace',
|
||||||
|
lineHeight: 1.45,
|
||||||
|
padding: 12,
|
||||||
|
background: 'var(--bg)',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
color: 'var(--text2)',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{JSON.stringify(
|
||||||
|
sanitizeDebugForPanel(
|
||||||
|
extractExecutionDebug(testResult),
|
||||||
|
testResult.error ? null : testResult.type
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Lightweight Markdown renderer – handles the subset used by the AI:
|
// Lightweight Markdown renderer – handles the subset used by the AI:
|
||||||
// ## Headings, **bold**, bullet lists, numbered lists, line breaks
|
// ## Headings, **bold**, bullet lists, numbered lists, fenced ``` code ```, line breaks
|
||||||
|
|
||||||
export default function Markdown({ text }) {
|
export default function Markdown({ text }) {
|
||||||
if (!text) return null
|
if (!text) return null
|
||||||
|
|
@ -7,6 +7,19 @@ export default function Markdown({ text }) {
|
||||||
const lines = text.split('\n')
|
const lines = text.split('\n')
|
||||||
const elements = []
|
const elements = []
|
||||||
let i = 0
|
let i = 0
|
||||||
|
let blockId = 0
|
||||||
|
|
||||||
|
const codeBlockStyle = {
|
||||||
|
margin: '10px 0',
|
||||||
|
padding: 12,
|
||||||
|
background: 'var(--surface2)',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
overflow: 'auto',
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
fontFamily: 'ui-monospace, Consolas, monospace'
|
||||||
|
}
|
||||||
|
|
||||||
const parseLine = (line) => {
|
const parseLine = (line) => {
|
||||||
// Parse inline **bold** and *italic*
|
// Parse inline **bold** and *italic*
|
||||||
|
|
@ -39,6 +52,44 @@ export default function Markdown({ text }) {
|
||||||
while (i < lines.length) {
|
while (i < lines.length) {
|
||||||
const line = lines[i]
|
const line = lines[i]
|
||||||
|
|
||||||
|
// Fenced code block: ``` or ```lang
|
||||||
|
const trimmedStart = line.trimStart()
|
||||||
|
if (trimmedStart.startsWith('```')) {
|
||||||
|
const lang = trimmedStart.slice(3).trim() || null
|
||||||
|
i++
|
||||||
|
const codeLines = []
|
||||||
|
while (i < lines.length) {
|
||||||
|
if (lines[i].trim().startsWith('```')) {
|
||||||
|
i++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
codeLines.push(lines[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
const code = codeLines.join('\n')
|
||||||
|
elements.push(
|
||||||
|
<div key={`code-${blockId++}`} style={{ margin: '10px 0' }}>
|
||||||
|
{lang && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 10,
|
||||||
|
color: 'var(--text3)',
|
||||||
|
marginBottom: 6,
|
||||||
|
fontFamily: 'ui-monospace, monospace',
|
||||||
|
letterSpacing: 0.02
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{lang}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<pre style={codeBlockStyle}>
|
||||||
|
<code style={{ color: 'var(--text1)', whiteSpace: 'pre', display: 'block' }}>{code}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Skip empty lines (add spacing)
|
// Skip empty lines (add spacing)
|
||||||
if (line.trim() === '') {
|
if (line.trim() === '') {
|
||||||
elements.push(<div key={i} style={{ height: 8 }} />)
|
elements.push(<div key={i} style={{ height: 8 }} />)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user