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:
* - 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
* - onClose: () => void
*
* Features:
* - Lädt registrierte Platzhalter vom Backend (~120+)
* - Extrahiert Workflow-spezifische Node-Outputs
* - Filtert Selbst-Referenzierung (Node kann sich nicht selbst referenzieren)
* - Zeigt Node-Namen (nicht nur IDs)
* - Kategorisiert: System + Workflow
* - Suchfunktion über alle Kategorien
*/
export function PlaceholderPicker({ nodes, onSelect, onClose }) {
export function PlaceholderPicker({ nodes, currentNodeId, onSelect, onClose }) {
const [searchQuery, setSearchQuery] = useState('')
const [systemPlaceholders, setSystemPlaceholders] = useState([])
const [loading, setLoading] = useState(true)
@ -61,8 +63,8 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
loadPlaceholders()
}, [])
// Extrahiere Workflow-spezifische Platzhalter
const workflowPlaceholders = extractWorkflowPlaceholders(nodes)
// Extrahiere Workflow-spezifische Platzhalter (ohne aktuellen Node)
const workflowPlaceholders = extractWorkflowPlaceholders(nodes, currentNodeId)
// Kombiniere beide Listen
const allPlaceholders = [
@ -341,14 +343,19 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
/**
* 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 = []
console.log('🔍 Extracting workflow placeholders from nodes:', nodes)
console.log('🚫 Excluding current node:', currentNodeId)
nodes.forEach(node => {
if (node.type === 'end') return // End Node hat keine Outputs
if (node.id === currentNodeId) return // Selbst-Referenzierung verhindern
const nodeId = node.id
const nodeLabel = node.data?.label || nodeId

View File

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

View File

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