fix(code-subblock): added validation to not parse non-variables as variables in the code subblock (#1240)

* fix(code-subblock): added validation to not parse non-variables as variables in the code subblock

* fix wand prompt bar styling

* fix error message for available connected blocks to only show connected available blocks, not block ID's

* ui
This commit is contained in:
Waleed
2025-09-03 16:09:02 -07:00
committed by GitHub
parent 3656d3d7ad
commit 26243b99e8
8 changed files with 124 additions and 68 deletions

View File

@@ -81,15 +81,15 @@ export function WandPromptBar({
<div
ref={promptBarRef}
className={cn(
'-top-20 absolute right-0 left-0',
'rounded-xl border bg-background shadow-lg',
'-translate-y-3 absolute right-0 bottom-full left-0 gap-2',
'rounded-lg border bg-background shadow-lg',
'z-9999999 transition-all duration-150',
isExiting ? 'opacity-0' : 'opacity-100',
className
)}
>
<div className='flex items-center gap-2 p-2'>
<div className={cn('status-indicator ml-1', isStreaming && 'streaming')} />
<div className={cn('status-indicator ml-2 self-center', isStreaming && 'streaming')} />
<div className='relative flex-1'>
<Input
@@ -98,7 +98,7 @@ export function WandPromptBar({
placeholder={placeholder}
className={cn(
'rounded-xl border-0 text-foreground text-sm placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0',
isStreaming && 'text-primary',
isStreaming && 'text-foreground/70',
(isLoading || isStreaming) && 'loading-placeholder'
)}
onKeyDown={(e) => {
@@ -111,11 +111,6 @@ export function WandPromptBar({
disabled={isLoading || isStreaming}
autoFocus={!isStreaming}
/>
{isStreaming && (
<div className='pointer-events-none absolute inset-0 h-full w-full overflow-hidden'>
<div className='shimmer-effect' />
</div>
)}
</div>
<Button
@@ -141,14 +136,6 @@ export function WandPromptBar({
</div>
<style jsx global>{`
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
@keyframes smoke-pulse {
0%,
@@ -164,8 +151,8 @@ export function WandPromptBar({
.status-indicator {
position: relative;
width: 16px;
height: 16px;
width: 12px;
height: 12px;
border-radius: 50%;
overflow: hidden;
background-color: hsl(var(--muted-foreground) / 0.5);
@@ -183,36 +170,20 @@ export function WandPromptBar({
border-radius: 50%;
background: radial-gradient(
circle,
hsl(var(--primary) / 0.7) 0%,
hsl(var(--primary) / 0.2) 60%,
hsl(var(--primary) / 0.9) 0%,
hsl(var(--primary) / 0.4) 60%,
transparent 80%
);
animation: smoke-pulse 1.8s ease-in-out infinite;
opacity: 0.9;
}
.shimmer-effect {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.4) 50%,
rgba(255, 255, 255, 0) 100%
);
animation: shimmer 2s infinite;
.dark .status-indicator.streaming::before {
background: #6b7280;
opacity: 0.9;
animation: smoke-pulse 1.8s ease-in-out infinite;
}
.dark .shimmer-effect {
background: linear-gradient(
90deg,
rgba(50, 50, 50, 0) 0%,
rgba(80, 80, 80, 0.4) 50%,
rgba(50, 50, 50, 0) 100%
);
}
`}</style>
</div>
)

View File

@@ -404,10 +404,8 @@ IMPORTANT FORMATTING RULES:
<div
className={cn(
'group relative min-h-[100px] rounded-md border border-input bg-background font-mono text-sm transition-colors',
isConnecting && 'ring-2 ring-blue-500 ring-offset-2',
!isValidJson && 'border-destructive bg-destructive/10'
isConnecting && 'ring-2 ring-blue-500 ring-offset-2'
)}
title={!isValidJson ? 'Invalid JSON' : undefined}
onDragOver={(e) => e.preventDefault()}
onDrop={handleDrop}
>
@@ -419,7 +417,7 @@ IMPORTANT FORMATTING RULES:
onClick={isPromptVisible ? hidePromptInline : showPromptInline}
disabled={isAiLoading || isAiStreaming}
aria-label='Generate code with AI'
className='h-8 w-8 rounded-full border border-transparent bg-muted/80 text-muted-foreground shadow-sm transition-all duration-200 hover:border-primary/20 hover:bg-muted hover:text-primary hover:shadow'
className='h-8 w-8 rounded-full border border-transparent bg-muted/80 text-muted-foreground shadow-sm transition-all duration-200 hover:border-primary/20 hover:bg-muted hover:text-foreground hover:shadow'
>
<Wand2 className='h-4 w-4' />
</Button>

View File

@@ -426,7 +426,7 @@ export function LongInput({
}
disabled={wandHook.isLoading || wandHook.isStreaming || disabled}
aria-label='Generate content with AI'
className='h-8 w-8 rounded-full border border-transparent bg-muted/80 text-muted-foreground shadow-sm transition-all duration-200 hover:border-primary/20 hover:bg-muted hover:text-primary hover:shadow'
className='h-8 w-8 rounded-full border border-transparent bg-muted/80 text-muted-foreground shadow-sm transition-all duration-200 hover:border-primary/20 hover:bg-muted hover:text-foreground hover:shadow'
>
<Wand2 className='h-4 w-4' />
</Button>

View File

@@ -436,7 +436,7 @@ export function ShortInput({
}
disabled={wandHook.isLoading || wandHook.isStreaming || disabled}
aria-label='Generate content with AI'
className='h-8 w-8 rounded-full border border-transparent bg-muted/80 text-muted-foreground shadow-sm transition-all duration-200 hover:border-primary/20 hover:bg-muted hover:text-primary hover:shadow'
className='h-8 w-8 rounded-full border border-transparent bg-muted/80 text-muted-foreground shadow-sm transition-all duration-200 hover:border-primary/20 hover:bg-muted hover:text-foreground hover:shadow'
>
<Wand2 className='h-4 w-4' />
</Button>

View File

@@ -6,6 +6,7 @@ import 'prismjs/components/prism-json'
import 'prismjs/themes/prism.css'
import { Wand2 } from 'lucide-react'
import Editor from 'react-simple-code-editor'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
interface CodeEditorProps {
@@ -213,19 +214,16 @@ export function CodeEditor({
)}
>
{showWandButton && onWandClick && (
<button
<Button
variant='ghost'
size='icon'
onClick={onWandClick}
disabled={wandButtonDisabled}
className={cn(
'absolute top-2 right-2 z-10 flex h-8 w-8 items-center justify-center rounded-full border border-transparent bg-muted/80 p-0 text-foreground shadow-sm transition-all duration-200',
'hover:border-primary/20 hover:bg-muted hover:text-foreground hover:shadow',
'opacity-0 transition-opacity group-hover:opacity-100',
wandButtonDisabled && 'cursor-not-allowed opacity-50'
)}
aria-label='Generate with AI'
className='absolute top-2 right-3 z-10 h-8 w-8 rounded-full border border-transparent bg-muted/80 text-muted-foreground opacity-0 shadow-sm transition-all duration-200 hover:border-primary/20 hover:bg-muted hover:text-foreground hover:shadow group-hover:opacity-100'
>
<Wand2 className='h-4 w-4' />
</button>
</Button>
)}
{!showWandButton && code.split('\n').length > 5 && (

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { Code, FileJson, Trash2, X } from 'lucide-react'
import { AlertTriangle, Code, FileJson, Trash2, X } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
AlertDialog,
@@ -934,11 +934,18 @@ try {
<Label htmlFor='json-schema' className='font-medium'>
JSON Schema
</Label>
{schemaError &&
!schemaGeneration.isStreaming && ( // Hide schema error while streaming
<Tooltip>
<TooltipTrigger asChild>
<AlertTriangle className='h-4 w-4 cursor-pointer text-destructive' />
</TooltipTrigger>
<TooltipContent side='top'>
<p>Invalid JSON</p>
</TooltipContent>
</Tooltip>
)}
</div>
{schemaError &&
!schemaGeneration.isStreaming && ( // Hide schema error while streaming
<div className='ml-4 break-words text-red-600 text-sm'>{schemaError}</div>
)}
</div>
<CodeEditor
value={jsonSchema}
@@ -975,7 +982,6 @@ try {
}`}
minHeight='360px'
className={cn(
schemaError && !schemaGeneration.isStreaming ? 'border-red-500' : '',
(schemaGeneration.isLoading || schemaGeneration.isStreaming) &&
'cursor-not-allowed opacity-50'
)}

View File

@@ -1610,7 +1610,7 @@ describe('InputResolver', () => {
}
expect(() => connectionResolver.resolveInputs(testBlock, contextWithConnections)).toThrow(
/Available connected blocks:.*Agent Block.*agent-1.*start/
/Available connected blocks:.*Agent Block.*Start/
)
})

View File

@@ -463,10 +463,18 @@ export class InputResolver {
const blockMatches = value.match(/<([^>]+)>/g)
if (!blockMatches) return value
// If we're in an API block body, check each match to see if it looks like XML rather than a reference
// Filter out patterns that are clearly not variable references (e.g., comparison operators)
const validBlockMatches = blockMatches.filter((match) => this.isValidVariableReference(match))
// If no valid matches found after filtering, return original value
if (validBlockMatches.length === 0) {
return value
}
// If we're in an API block body, check each valid match to see if it looks like XML rather than a reference
if (
currentBlock.metadata?.id === 'api' &&
blockMatches.some((match) => {
validBlockMatches.some((match) => {
const innerContent = match.slice(1, -1)
// Patterns that suggest this is XML, not a block reference:
return (
@@ -490,7 +498,7 @@ export class InputResolver {
value.includes('}') &&
value.includes('`')
for (const match of blockMatches) {
for (const match of validBlockMatches) {
// Skip variables - they've already been processed
if (match.startsWith('<variable.')) {
continue
@@ -814,6 +822,63 @@ export class InputResolver {
return resolvedValue
}
/**
* Validates if a match with < and > is actually a variable reference.
* Valid variable references must:
* - Have no space after the opening <
* - Contain a dot (.)
* - Have no spaces until the closing >
* - Not be comparison operators or HTML tags
*
* @param match - The matched string including < and >
* @returns Whether this is a valid variable reference
*/
private isValidVariableReference(match: string): boolean {
const innerContent = match.slice(1, -1)
if (!innerContent.includes('.')) {
return false
}
const dotIndex = innerContent.indexOf('.')
const beforeDot = innerContent.substring(0, dotIndex)
const afterDot = innerContent.substring(dotIndex + 1)
if (afterDot.includes(' ')) {
return false
}
if (
beforeDot.match(/^\s*[<>=!]+\s*$/) ||
beforeDot.match(/\s[<>=!]+\s/) ||
beforeDot.match(/^[<>=!]+\s/)
) {
return false
}
if (innerContent.startsWith(' ')) {
return false
}
if (innerContent.match(/^[a-zA-Z][a-zA-Z0-9]*$/) && !innerContent.includes('.')) {
return false
}
if (innerContent.match(/^[<>=!]+\s/)) {
return false
}
if (beforeDot.match(/[+*/=<>!]/)) {
return false
}
if (afterDot.match(/[+\-*/=<>!]/)) {
return false
}
return true
}
/**
* Determines if a string contains a properly formatted environment variable reference.
* Valid references are either:
@@ -1145,6 +1210,24 @@ export class InputResolver {
return [...new Set(names)] // Remove duplicates
}
/**
* Gets user-friendly block names for error messages.
* Only returns the actual block names that users see in the UI.
*/
private getAccessibleBlockNamesForError(currentBlockId: string): string[] {
const accessibleBlockIds = this.getAccessibleBlocks(currentBlockId)
const names: string[] = []
for (const blockId of accessibleBlockIds) {
const block = this.blockById.get(blockId)
if (block?.metadata?.name) {
names.push(block.metadata.name)
}
}
return [...new Set(names)] // Remove duplicates
}
/**
* Checks if a block reference could potentially be valid without throwing errors.
* Used to filter out non-block patterns like <test> from block reference resolution.
@@ -1197,7 +1280,7 @@ export class InputResolver {
}
if (!sourceBlock) {
const accessibleNames = this.getAccessibleBlockNames(currentBlockId)
const accessibleNames = this.getAccessibleBlockNamesForError(currentBlockId)
return {
isValid: false,
errorMessage: `Block "${blockRef}" was not found. Available connected blocks: ${accessibleNames.join(', ')}`,
@@ -1207,7 +1290,7 @@ export class InputResolver {
// Check if block is accessible (connected)
const accessibleBlocks = this.getAccessibleBlocks(currentBlockId)
if (!accessibleBlocks.has(sourceBlock.id)) {
const accessibleNames = this.getAccessibleBlockNames(currentBlockId)
const accessibleNames = this.getAccessibleBlockNamesForError(currentBlockId)
return {
isValid: false,
errorMessage: `Block "${blockRef}" is not connected to this block. Available connected blocks: ${accessibleNames.join(', ')}`,