From 08c9cccdcc0ee7359a38c701e6346c646b12b8e1 Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 12 Apr 2026 11:10:39 +0200 Subject: [PATCH] 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. --- .../src/components/UnifiedPromptModal.jsx | 164 ++++++++++++------ frontend/src/utils/Markdown.jsx | 53 +++++- 2 files changed, 165 insertions(+), 52 deletions(-) diff --git a/frontend/src/components/UnifiedPromptModal.jsx b/frontend/src/components/UnifiedPromptModal.jsx index 3052f06..4281866 100644 --- a/frontend/src/components/UnifiedPromptModal.jsx +++ b/frontend/src/components/UnifiedPromptModal.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect, useRef, useLayoutEffect } from 'react' import { api } from '../utils/api' import { X, Plus, Trash2, MoveUp, MoveDown, Code } from 'lucide-react' import PlaceholderPicker from './PlaceholderPicker' @@ -66,6 +66,53 @@ function sanitizeDebugForPanel(dbg, resultType) { 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 ( +
+
+ {children} +
+ {showToggle && ( +
+ +
+ )} +
+ ) +} + function renderTestOutput(testResult) { if (!testResult) return null if (testResult.error) { @@ -89,7 +136,7 @@ function renderTestOutput(testResult) { return Keine Ausgabe } - const proseBox = (content) => ( + const proseBox = (content, contentKey) => (
- {content} + {contentKey != null ? ( + {content} + ) : ( + content + )}
) @@ -123,23 +174,27 @@ function renderTestOutput(testResult) { {key} {typeof val === 'string' ? ( - proseBox() + proseBox( + , + `pipe-${key}-${val.length}-${val.slice(0, 120)}` + ) ) : ( -
-                {JSON.stringify(val, null, 2)}
-              
+ +
+                  {JSON.stringify(val, null, 2)}
+                
+
)} ))} @@ -151,47 +206,54 @@ function renderTestOutput(testResult) { if (fmt === 'json') { try { const parsed = JSON.parse(out) + const jsonStr = JSON.stringify(parsed, null, 2) return ( -
-            {JSON.stringify(parsed, null, 2)}
-          
+ +
+              {jsonStr}
+            
+
) } catch { - return proseBox(
{out}
) + return proseBox( +
{out}
, + `json-fail-${out.length}-${out.slice(0, 80)}` + ) } } - return proseBox() + return proseBox(, `text-out-${out.length}-${out.slice(0, 120)}`) } if (typeof out === 'object') { + const jsonStr = JSON.stringify(out, null, 2) return ( -
-        {JSON.stringify(out, null, 2)}
-      
+ +
+          {jsonStr}
+        
+
) } diff --git a/frontend/src/utils/Markdown.jsx b/frontend/src/utils/Markdown.jsx index 328fa8b..ece6419 100644 --- a/frontend/src/utils/Markdown.jsx +++ b/frontend/src/utils/Markdown.jsx @@ -1,5 +1,5 @@ // 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 }) { if (!text) return null @@ -7,6 +7,19 @@ export default function Markdown({ text }) { const lines = text.split('\n') const elements = [] 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) => { // Parse inline **bold** and *italic* @@ -39,6 +52,44 @@ export default function Markdown({ text }) { while (i < lines.length) { 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( +
+ {lang && ( +
+ {lang} +
+ )} +
+            {code}
+          
+
+ ) + continue + } + // Skip empty lines (add spacing) if (line.trim() === '') { elements.push(
)