Verbesserung für Tests von Prompts #80
|
|
@ -1,4 +1,4 @@
|
||||||
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'
|
||||||
|
|
@ -66,6 +66,53 @@ function sanitizeDebugForPanel(dbg, resultType) {
|
||||||
return o
|
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) {
|
function renderTestOutput(testResult) {
|
||||||
if (!testResult) return null
|
if (!testResult) return null
|
||||||
if (testResult.error) {
|
if (testResult.error) {
|
||||||
|
|
@ -89,7 +136,7 @@ function renderTestOutput(testResult) {
|
||||||
return <span style={{ color: 'var(--text3)' }}>Keine Ausgabe</span>
|
return <span style={{ color: 'var(--text3)' }}>Keine Ausgabe</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
const proseBox = (content) => (
|
const proseBox = (content, contentKey) => (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: 16,
|
padding: 16,
|
||||||
|
|
@ -102,7 +149,11 @@ function renderTestOutput(testResult) {
|
||||||
textAlign: 'left'
|
textAlign: 'left'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{content}
|
{contentKey != null ? (
|
||||||
|
<ExpandableCollapsible contentKey={contentKey}>{content}</ExpandableCollapsible>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -123,23 +174,27 @@ function renderTestOutput(testResult) {
|
||||||
{key}
|
{key}
|
||||||
</div>
|
</div>
|
||||||
{typeof val === 'string' ? (
|
{typeof val === 'string' ? (
|
||||||
proseBox(<Markdown text={val} />)
|
proseBox(
|
||||||
|
<Markdown text={val} />,
|
||||||
|
`pipe-${key}-${val.length}-${val.slice(0, 120)}`
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<pre
|
<ExpandableCollapsible contentKey={`pipe-json-${key}-${JSON.stringify(val).slice(0, 200)}`} maxRem={22}>
|
||||||
style={{
|
<pre
|
||||||
margin: 0,
|
style={{
|
||||||
padding: 12,
|
margin: 0,
|
||||||
background: 'var(--bg)',
|
padding: 12,
|
||||||
borderRadius: 8,
|
background: 'var(--bg)',
|
||||||
border: '1px solid var(--border)',
|
borderRadius: 8,
|
||||||
fontSize: 12,
|
border: '1px solid var(--border)',
|
||||||
overflow: 'auto',
|
fontSize: 12,
|
||||||
maxHeight: 360,
|
overflow: 'auto',
|
||||||
textAlign: 'left'
|
textAlign: 'left'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{JSON.stringify(val, null, 2)}
|
{JSON.stringify(val, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
|
</ExpandableCollapsible>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -151,47 +206,54 @@ function renderTestOutput(testResult) {
|
||||||
if (fmt === 'json') {
|
if (fmt === 'json') {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(out)
|
const parsed = JSON.parse(out)
|
||||||
|
const jsonStr = JSON.stringify(parsed, null, 2)
|
||||||
return (
|
return (
|
||||||
<pre
|
<ExpandableCollapsible contentKey={`json-out-${jsonStr.length}-${jsonStr.slice(0, 80)}`} maxRem={26}>
|
||||||
style={{
|
<pre
|
||||||
margin: 0,
|
style={{
|
||||||
padding: 12,
|
margin: 0,
|
||||||
background: 'var(--bg)',
|
padding: 12,
|
||||||
borderRadius: 8,
|
background: 'var(--bg)',
|
||||||
border: '1px solid var(--border)',
|
borderRadius: 8,
|
||||||
fontSize: 12,
|
border: '1px solid var(--border)',
|
||||||
overflow: 'auto',
|
fontSize: 12,
|
||||||
maxHeight: 480,
|
overflow: 'auto',
|
||||||
textAlign: 'left'
|
textAlign: 'left'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{JSON.stringify(parsed, null, 2)}
|
{jsonStr}
|
||||||
</pre>
|
</pre>
|
||||||
|
</ExpandableCollapsible>
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
return proseBox(<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>{out}</pre>)
|
return proseBox(
|
||||||
|
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>{out}</pre>,
|
||||||
|
`json-fail-${out.length}-${out.slice(0, 80)}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return proseBox(<Markdown text={out} />)
|
return proseBox(<Markdown text={out} />, `text-out-${out.length}-${out.slice(0, 120)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof out === 'object') {
|
if (typeof out === 'object') {
|
||||||
|
const jsonStr = JSON.stringify(out, null, 2)
|
||||||
return (
|
return (
|
||||||
<pre
|
<ExpandableCollapsible contentKey={`obj-out-${jsonStr.length}-${jsonStr.slice(0, 120)}`} maxRem={26}>
|
||||||
style={{
|
<pre
|
||||||
margin: 0,
|
style={{
|
||||||
padding: 12,
|
margin: 0,
|
||||||
background: 'var(--bg)',
|
padding: 12,
|
||||||
borderRadius: 8,
|
background: 'var(--bg)',
|
||||||
border: '1px solid var(--border)',
|
borderRadius: 8,
|
||||||
fontSize: 12,
|
border: '1px solid var(--border)',
|
||||||
overflow: 'auto',
|
fontSize: 12,
|
||||||
maxHeight: 480,
|
overflow: 'auto',
|
||||||
textAlign: 'left'
|
textAlign: 'left'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{JSON.stringify(out, null, 2)}
|
{jsonStr}
|
||||||
</pre>
|
</pre>
|
||||||
|
</ExpandableCollapsible>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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