fix: Inline Prompts - UX-Verbesserungen
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:
parent
65500c899b
commit
8f6d60681e
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user