From b21d80900e0a3defe3f65ab02b2ae86db70279aa Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 11 Mar 2026 16:14:11 +1300 Subject: [PATCH] feat: inline variable highlights in PromptConstructor, update PromptNode label - Replace warning banner with inline highlights: blue for resolved @vars, red for unresolved - Add "N vars missing" notice in footer bar - Change PromptNode empty variable label from "@var" to "Add variable" Co-Authored-By: Claude Opus 4.6 --- .../nodes/PromptConstructorNode.tsx | 73 +++++++++++++++---- src/components/nodes/PromptNode.tsx | 2 +- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/components/nodes/PromptConstructorNode.tsx b/src/components/nodes/PromptConstructorNode.tsx index 0c11737c..9ef79bb7 100644 --- a/src/components/nodes/PromptConstructorNode.tsx +++ b/src/components/nodes/PromptConstructorNode.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useState, useEffect, useMemo, useRef } from "react"; +import { useCallback, useState, useEffect, useMemo, useRef, ReactNode } from "react"; import { Handle, Position, NodeProps, Node } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; import { usePromptAutocomplete } from "@/hooks/usePromptAutocomplete"; @@ -140,6 +140,40 @@ export function PromptConstructorNode({ id, data, selected }: NodeProps(null); + + // Sync highlight overlay scroll with textarea + const handleScroll = useCallback(() => { + if (textareaRef.current && highlightRef.current) { + highlightRef.current.scrollTop = textareaRef.current.scrollTop; + highlightRef.current.scrollLeft = textareaRef.current.scrollLeft; + } + }, []); + + // Build highlighted text with blue for resolved, red for unresolved variables + const highlightedContent = useMemo((): ReactNode[] => { + const availableNames = new Set(availableVariables.map(v => v.name)); + const pattern = /@(\w+)/g; + const parts: ReactNode[] = []; + let lastIndex = 0; + const matches = localTemplate.matchAll(pattern); + for (const match of matches) { + const idx = match.index!; + if (idx > lastIndex) { + parts.push(localTemplate.slice(lastIndex, idx)); + } + const isResolved = availableNames.has(match[1]); + parts.push( + {match[0]} + ); + lastIndex = idx + match[0].length; + } + if (lastIndex < localTemplate.length) { + parts.push(localTemplate.slice(lastIndex)); + } + return parts; + }, [localTemplate, availableVariables]); + const handleFocus = useCallback(() => { setIsEditing(true); }, []); @@ -169,15 +203,18 @@ export function PromptConstructorNode({ id, data, selected }: NodeProps - {/* Warning badge for unresolved variables - overlay at top */} - {unresolvedVars.length > 0 && ( -
- Unresolved: {unresolvedVars.map(v => `@${v}`).join(', ')} -
- )} - - {/* Template textarea with autocomplete */} + {/* Template textarea with highlight overlay for @variables */}
+ {/* Highlight overlay - blue for resolved, red for unresolved @vars */} + {highlightedContent.length > 0 && ( +
0 || unresolvedVars.length > 0 ? "pb-7" : ""}`} + aria-hidden="true" + > + {highlightedContent} +
+ )}