feat: Add expandable collapsible component for improved content display
- Introduced `ExpandableCollapsible` component to manage the visibility of lengthy content, allowing users to toggle between expanded and collapsed views. - Updated `renderTestOutput` to utilize the new component for displaying test results, JSON outputs, and object representations, enhancing user experience by reducing clutter. - Enhanced `Markdown` component to support fenced code blocks, improving the rendering of code snippets with language labels and better styling. These changes improve the readability and organization of content within the application, providing users with a more interactive and manageable interface.
This commit is contained in:
parent
4b6e1bed11
commit
08c9cccdcc
|
|
@ -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