fix: Inline Prompts - UX-Verbesserungen
Some checks failed
Deploy Development / deploy (push) Failing after 39s
Build Test / pytest-backend (push) Successful in 3s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Failing after 11s

Problem 1: Selbst-Referenzierung verhindern
- PlaceholderPicker erhält currentNodeId prop
- Node kann sich nicht mehr selbst in Placeholders sehen
- extractWorkflowPlaceholders() filtert aktuellen Node aus

Problem 2: Radio-Button State-Management
- IIFE mit Helper-Funktion für Mode-Bestimmung
- isInlineMode/isReferenceMode basierend auf data.inline_template
- Korrekte Conditional Rendering Logic
- Beim Wechsel Reference→Inline bleibt prompt_slug erhalten
- Beim Wechsel Inline→Reference bleibt inline_template erhalten

Problem 3: Layout-Breite optimiert
- Sidebar: 250px → 220px (schmaler)
- Config Panel: 400px → 520px (breiter für bessere Lesbarkeit)
- Responsive: Config Panel bei <1200px: 450px statt 350px

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-04-11 08:58:46 +02:00
parent 65500c899b
commit 8f6d60681e
3 changed files with 111 additions and 95 deletions

View File

@ -6,17 +6,19 @@ import { api } from '../../../utils/api'
* *
* Props: * Props:
* - nodes: Array of workflow nodes (to extract workflow-specific placeholders) * - nodes: Array of workflow nodes (to extract workflow-specific placeholders)
* - currentNodeId: ID des aktuellen Nodes (wird aus Placeholders ausgeschlossen)
* - onSelect: (placeholderString) => void - Callback when placeholder is selected * - onSelect: (placeholderString) => void - Callback when placeholder is selected
* - onClose: () => void * - onClose: () => void
* *
* Features: * Features:
* - Lädt registrierte Platzhalter vom Backend (~120+) * - Lädt registrierte Platzhalter vom Backend (~120+)
* - Extrahiert Workflow-spezifische Node-Outputs * - Extrahiert Workflow-spezifische Node-Outputs
* - Filtert Selbst-Referenzierung (Node kann sich nicht selbst referenzieren)
* - Zeigt Node-Namen (nicht nur IDs) * - Zeigt Node-Namen (nicht nur IDs)
* - Kategorisiert: System + Workflow * - Kategorisiert: System + Workflow
* - Suchfunktion über alle Kategorien * - Suchfunktion über alle Kategorien
*/ */
export function PlaceholderPicker({ nodes, onSelect, onClose }) { export function PlaceholderPicker({ nodes, currentNodeId, onSelect, onClose }) {
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [systemPlaceholders, setSystemPlaceholders] = useState([]) const [systemPlaceholders, setSystemPlaceholders] = useState([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@ -61,8 +63,8 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
loadPlaceholders() loadPlaceholders()
}, []) }, [])
// Extrahiere Workflow-spezifische Platzhalter // Extrahiere Workflow-spezifische Platzhalter (ohne aktuellen Node)
const workflowPlaceholders = extractWorkflowPlaceholders(nodes) const workflowPlaceholders = extractWorkflowPlaceholders(nodes, currentNodeId)
// Kombiniere beide Listen // Kombiniere beide Listen
const allPlaceholders = [ const allPlaceholders = [
@ -341,14 +343,19 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
/** /**
* Extrahiert Workflow-spezifische Platzhalter aus Nodes * Extrahiert Workflow-spezifische Platzhalter aus Nodes
*
* @param {Array} nodes - Alle Workflow-Nodes
* @param {string} currentNodeId - ID des aktuellen Nodes (wird ausgeschlossen)
*/ */
function extractWorkflowPlaceholders(nodes) { function extractWorkflowPlaceholders(nodes, currentNodeId) {
const placeholders = [] const placeholders = []
console.log('🔍 Extracting workflow placeholders from nodes:', nodes) console.log('🔍 Extracting workflow placeholders from nodes:', nodes)
console.log('🚫 Excluding current node:', currentNodeId)
nodes.forEach(node => { nodes.forEach(node => {
if (node.type === 'end') return // End Node hat keine Outputs if (node.type === 'end') return // End Node hat keine Outputs
if (node.id === currentNodeId) return // Selbst-Referenzierung verhindern
const nodeId = node.id const nodeId = node.id
const nodeLabel = node.data?.label || nodeId const nodeLabel = node.data?.label || nodeId

View File

@ -486,101 +486,109 @@ export default function WorkflowEditorPage() {
</div> </div>
{/* Type-spezifische Konfiguration */} {/* Type-spezifische Konfiguration */}
{selectedNode.type === 'analysis' && ( {selectedNode.type === 'analysis' && (() => {
<> // Helper: Bestimme aktuellen Mode basierend auf node.data
{/* Prompt Source Selector */} const isInlineMode = selectedNode.data.inline_template !== null && selectedNode.data.inline_template !== undefined
<div className="config-section"> const isReferenceMode = !isInlineMode
<label className="form-label">Prompt-Quelle</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> return (
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}> <>
<input {/* Prompt Source Selector */}
type="radio" <div className="config-section">
name={`promptSource-${selectedNode.id}`} <label className="form-label">Prompt-Quelle</label>
checked={!!selectedNode.data.prompt_slug} <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
onChange={() => { <label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type="radio"
name={`promptSource-${selectedNode.id}`}
checked={isReferenceMode}
onChange={() => {
// Wechsel zu Reference Mode
handleNodeUpdate(selectedNode.id, {
inline_template: null, // Inline löschen
prompt_slug: selectedNode.data.prompt_slug || '' // Behalte existierenden slug
})
}}
style={{ marginRight: '8px' }}
/>
<span style={{ fontSize: '14px' }}>📚 Basis-Prompt referenzieren</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type="radio"
name={`promptSource-${selectedNode.id}`}
checked={isInlineMode}
onChange={() => {
// Wechsel zu Inline Mode
handleNodeUpdate(selectedNode.id, {
prompt_slug: null, // Reference löschen
inline_template: selectedNode.data.inline_template || '' // Behalte existierendes template
})
}}
style={{ marginRight: '8px' }}
/>
<span style={{ fontSize: '14px' }}> Inline-Template erstellen</span>
</label>
</div>
</div>
{/* Conditional Rendering: Reference Mode */}
{isReferenceMode && (
<div className="config-section">
<label>Basis-Prompt auswählen</label>
<select
value={selectedNode.data.prompt_slug || ''}
onChange={(e) => {
const promptSlug = e.target.value
const selectedPrompt = availablePrompts.find(p => p.slug === promptSlug)
handleNodeUpdate(selectedNode.id, { handleNodeUpdate(selectedNode.id, {
prompt_slug: '', prompt_slug: promptSlug || '',
prompt_name: selectedPrompt?.name || null,
inline_template: null inline_template: null
}) })
}} }}
style={{ marginRight: '8px' }} style={{
/> width: '100%',
<span style={{ fontSize: '14px' }}>📚 Basis-Prompt referenzieren</span> padding: '8px',
</label> borderRadius: '4px',
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}> border: '1px solid var(--border)',
<input background: 'var(--surface)',
type="radio" color: 'var(--text1)'
name={`promptSource-${selectedNode.id}`}
checked={!!selectedNode.data.inline_template || (!selectedNode.data.prompt_slug && !selectedNode.data.inline_template)}
onChange={() => {
handleNodeUpdate(selectedNode.id, {
prompt_slug: null,
inline_template: ''
})
}} }}
style={{ marginRight: '8px' }} >
/> <option value="">-- Basis-Prompt wählen --</option>
<span style={{ fontSize: '14px' }}> Inline-Template erstellen</span> {availablePrompts.map(prompt => (
</label> <option key={prompt.id} value={prompt.slug}>
</div> {prompt.name}
</div> </option>
))}
</select>
{selectedNode.data.prompt_slug && (
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--text3)' }}>
Prompt: {selectedNode.data.prompt_slug} ({selectedNode.data.prompt_name || 'unbekannt'})
</div>
)}
</div>
)}
{/* Conditional Rendering: Reference oder Inline */} {/* Conditional Rendering: Inline Mode */}
{selectedNode.data.prompt_slug !== null && !selectedNode.data.inline_template && ( {isInlineMode && (
<div className="config-section"> <InlineTemplateEditor
<label>Basis-Prompt auswählen</label> value={selectedNode.data.inline_template || ''}
<select onChange={(template) => handleNodeUpdate(selectedNode.id, {
value={selectedNode.data.prompt_slug ? String(selectedNode.data.prompt_slug) : ''} inline_template: template,
onChange={(e) => { prompt_slug: null
const promptSlug = e.target.value })}
console.log('🎯 Prompt selected:', promptSlug, 'Type:', typeof promptSlug) onPlaceholderPick={() => {
const selectedPrompt = availablePrompts.find(p => p.slug === promptSlug) setPlaceholderPickerTarget('inline')
console.log('📋 Selected prompt object:', selectedPrompt) setShowPlaceholderPicker(true)
handleNodeUpdate(selectedNode.id, {
prompt_slug: promptSlug || null,
prompt_name: selectedPrompt?.name || null,
inline_template: null
})
}} }}
style={{ textareaRef={inlineTemplateTextareaRef}
width: '100%', />
padding: '8px', )}
borderRadius: '4px', </>
border: '1px solid var(--border)', )
background: 'var(--surface)', })()}
color: 'var(--text1)'
}}
>
<option value="">-- Basis-Prompt wählen --</option>
{availablePrompts.map(prompt => (
<option key={prompt.id} value={prompt.slug}>
{prompt.name}
</option>
))}
</select>
{selectedNode.data.prompt_slug && (
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--text3)' }}>
Prompt: {selectedNode.data.prompt_slug} ({selectedNode.data.prompt_name || 'unbekannt'})
</div>
)}
</div>
)}
{/* Inline Template Editor */}
{(selectedNode.data.inline_template !== null || !selectedNode.data.prompt_slug) && (
<InlineTemplateEditor
value={selectedNode.data.inline_template || ''}
onChange={(template) => handleNodeUpdate(selectedNode.id, {
inline_template: template,
prompt_slug: null
})}
onPlaceholderPick={() => {
setPlaceholderPickerTarget('inline')
setShowPlaceholderPicker(true)
}}
textareaRef={inlineTemplateTextareaRef}
/>
)}
<QuestionAugmentationPanel node={selectedNode} onChange={handleNodeUpdate} /> <QuestionAugmentationPanel node={selectedNode} onChange={handleNodeUpdate} />
<FallbackConfig node={selectedNode} edges={edges} nodes={nodes} onChange={handleNodeUpdate} /> <FallbackConfig node={selectedNode} edges={edges} nodes={nodes} onChange={handleNodeUpdate} />
@ -661,6 +669,7 @@ export default function WorkflowEditorPage() {
{showPlaceholderPicker && ( {showPlaceholderPicker && (
<PlaceholderPicker <PlaceholderPicker
nodes={nodes} nodes={nodes}
currentNodeId={selectedNode?.id}
onSelect={handlePlaceholderSelect} onSelect={handlePlaceholderSelect}
onClose={() => setShowPlaceholderPicker(false)} onClose={() => setShowPlaceholderPicker(false)}
/> />

View File

@ -27,7 +27,7 @@
/* ── Sidebar (Node Palette) ─────────────────────────────────────────────── */ /* ── Sidebar (Node Palette) ─────────────────────────────────────────────── */
.workflow-sidebar { .workflow-sidebar {
width: 250px; width: 220px;
background: var(--surface); background: var(--surface);
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
padding: 16px; padding: 16px;
@ -268,7 +268,7 @@
/* ── Config Panel ────────────────────────────────────────────────────────── */ /* ── Config Panel ────────────────────────────────────────────────────────── */
.workflow-config-panel { .workflow-config-panel {
width: 400px; width: 520px;
background: var(--surface); background: var(--surface);
border-left: 1px solid var(--border); border-left: 1px solid var(--border);
padding: 16px; padding: 16px;
@ -453,7 +453,7 @@
} }
.workflow-config-panel { .workflow-config-panel {
width: 350px; width: 450px;
} }
} }