mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
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:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
|
||||
@@ -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/
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -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(', ')}`,
|
||||
|
||||
Reference in New Issue
Block a user