Improvement: subblocks (#2850)

* improvement(panel): increased default width

* improvement: subblocks

* fix: generate, mcp, ring
This commit is contained in:
Emir Karabeg
2026-01-15 23:02:51 -08:00
committed by GitHub
parent 55700b9bf4
commit 6ff68b39ce
27 changed files with 572 additions and 211 deletions
+1 -45
View File
@@ -11,7 +11,7 @@
*/
:root {
--sidebar-width: 232px; /* SIDEBAR_WIDTH.DEFAULT */
--panel-width: 290px; /* PANEL_WIDTH.DEFAULT */
--panel-width: 320px; /* PANEL_WIDTH.DEFAULT */
--toolbar-triggers-height: 300px; /* TOOLBAR_TRIGGERS_HEIGHT.DEFAULT */
--editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */
--terminal-height: 155px; /* TERMINAL_HEIGHT.DEFAULT */
@@ -77,24 +77,6 @@
cursor: grabbing !important;
}
/**
* Selected node ring indicator
* Uses a pseudo-element overlay to match the original behavior (absolute inset-0 z-40)
*/
.react-flow__node.selected > div > div {
position: relative;
}
.react-flow__node.selected > div > div::after {
content: "";
position: absolute;
inset: 0;
z-index: 40;
border-radius: 8px;
box-shadow: 0 0 0 1.75px var(--brand-secondary);
pointer-events: none;
}
/**
* Color tokens - single source of truth for all colors
* Light mode: Warm theme
@@ -576,32 +558,6 @@ input[type="search"]::-ms-clear {
transition-duration: 300ms;
}
.streaming-effect {
@apply relative overflow-hidden;
}
.streaming-effect::after {
content: "";
@apply pointer-events-none absolute left-0 top-0 h-full w-full;
background: linear-gradient(
90deg,
rgba(128, 128, 128, 0) 0%,
rgba(128, 128, 128, 0.1) 50%,
rgba(128, 128, 128, 0) 100%
);
animation: code-shimmer 1.5s infinite;
z-index: 10;
}
.dark .streaming-effect::after {
background: linear-gradient(
90deg,
rgba(180, 180, 180, 0) 0%,
rgba(180, 180, 180, 0.1) 50%,
rgba(180, 180, 180, 0) 100%
);
}
.loading-placeholder::placeholder {
animation: placeholder-pulse 1.5s ease-in-out infinite;
}
@@ -336,6 +336,10 @@ export function Code({
setCode('')
}
handleStreamChunkRef.current = (chunk: string) => {
setCode((prev) => prev + chunk)
}
handleGeneratedContentRef.current = (generatedCode: string) => {
setCode(generatedCode)
if (!isPreview && !disabled) {
@@ -691,11 +695,7 @@ export function Code({
/>
)}
<CodeEditor.Container
onDragOver={(e) => e.preventDefault()}
onDrop={handleDrop}
isStreaming={isAiStreaming}
>
<CodeEditor.Container onDragOver={(e) => e.preventDefault()} onDrop={handleDrop}>
<div className='absolute top-2 right-3 z-10 flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
{wandConfig?.enabled &&
!isAiStreaming &&
@@ -761,6 +761,11 @@ export function Code({
}}
onFocus={() => {
hasEditedSinceFocusRef.current = false
// Show tag dropdown on focus when code is empty
if (!isPreview && !disabled && !readOnly && code.trim() === '') {
setShowTags(true)
setCursorPosition(0)
}
}}
highlight={createHighlightFunction(effectiveLanguage, shouldHighlightReference)}
{...getCodeEditorProps({ isStreaming: isAiStreaming, isPreview, disabled })}
@@ -115,6 +115,7 @@ export function ConditionInput({
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
const containerRef = useRef<HTMLDivElement>(null)
const inputRefs = useRef<Map<string, HTMLTextAreaElement>>(new Map())
/**
* Determines if a reference string should be highlighted in the editor.
@@ -728,6 +729,20 @@ export function ConditionInput({
})
}, [conditionalBlocks.length])
// Capture textarea refs from Editor components (condition mode)
useEffect(() => {
if (!isRouterMode && containerRef.current) {
conditionalBlocks.forEach((block) => {
const textarea = containerRef.current?.querySelector(
`[data-block-id="${block.id}"] textarea`
) as HTMLTextAreaElement | null
if (textarea) {
inputRefs.current.set(block.id, textarea)
}
})
}
}, [conditionalBlocks, isRouterMode])
// Show loading or empty state if not ready or no blocks
if (!isReady || conditionalBlocks.length === 0) {
return (
@@ -842,6 +857,9 @@ export function ConditionInput({
onDrop={(e) => handleDrop(block.id, e)}
>
<Textarea
ref={(el) => {
if (el) inputRefs.current.set(block.id, el)
}}
data-router-block-id={block.id}
value={block.value}
onChange={(e) => {
@@ -869,6 +887,15 @@ export function ConditionInput({
)
}
}}
onFocus={() => {
if (!isPreview && !disabled && block.value.trim() === '') {
setConditionalBlocks((blocks) =>
blocks.map((b) =>
b.id === block.id ? { ...b, showTags: true, cursorPosition: 0 } : b
)
)
}
}}
onBlur={() => {
setTimeout(() => {
setConditionalBlocks((blocks) =>
@@ -929,6 +956,11 @@ export function ConditionInput({
)
)
}}
inputRef={
{
current: inputRefs.current.get(block.id) || null,
} as React.RefObject<HTMLTextAreaElement>
}
/>
)}
</div>
@@ -1006,6 +1038,15 @@ export function ConditionInput({
)
}
}}
onFocus={() => {
if (!isPreview && !disabled && block.value.trim() === '') {
setConditionalBlocks((blocks) =>
blocks.map((b) =>
b.id === block.id ? { ...b, showTags: true, cursorPosition: 0 } : b
)
)
}
}}
highlight={(codeToHighlight) => {
const placeholders: {
placeholder: string
@@ -1113,6 +1154,11 @@ export function ConditionInput({
)
)
}}
inputRef={
{
current: inputRefs.current.get(block.id) || null,
} as React.RefObject<HTMLTextAreaElement>
}
/>
)}
</div>
@@ -288,6 +288,7 @@ export function DocumentTagEntry({
onKeyDown={handlers.onKeyDown}
onDrop={handlers.onDrop}
onDragOver={handlers.onDragOver}
onFocus={handlers.onFocus}
onScroll={(e) => syncOverlayScroll(cellKey, e.currentTarget.scrollLeft)}
onPaste={() =>
setTimeout(() => {
@@ -214,6 +214,7 @@ export function EvalInput({
onKeyDown={handlers.onKeyDown}
onDrop={handlers.onDrop}
onDragOver={handlers.onDragOver}
onFocus={handlers.onFocus}
placeholder='How accurate is the response?'
disabled={isPreview || disabled}
className={cn(
@@ -237,6 +237,7 @@ function InputMappingField({
onKeyDown={handlers.onKeyDown}
onDrop={handlers.onDrop}
onDragOver={handlers.onDragOver}
onFocus={handlers.onFocus}
onScroll={(e) => handleScroll(e)}
onPaste={() =>
setTimeout(() => {
@@ -276,6 +276,7 @@ export function KnowledgeTagFilters({
onKeyDown={handlers.onKeyDown}
onDrop={handlers.onDrop}
onDragOver={handlers.onDragOver}
onFocus={handlers.onFocus}
onScroll={(e) => syncOverlayScroll(cellKey, e.currentTarget.scrollLeft)}
onPaste={() =>
setTimeout(() => {
@@ -1,3 +1,4 @@
import type React from 'react'
import {
useCallback,
useEffect,
@@ -159,6 +160,27 @@ export function LongInput({
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
/**
* Callback to show tag dropdown when input is empty and focused
*/
const shouldForceTagDropdown = useCallback(
({
value,
}: {
value: string
cursor: number
event: 'focus'
}): { show: boolean } | undefined => {
if (isPreview || disabled) return { show: false }
// Show tag dropdown on focus when input is empty
if (value.trim() === '') {
return { show: true }
}
return { show: false }
},
[isPreview, disabled]
)
// During streaming, use local content; otherwise use the controller value
const value = useMemo(() => {
if (wandHook.isStreaming) return localContent
@@ -294,6 +316,7 @@ export function LongInput({
disabled={disabled}
isStreaming={wandHook.isStreaming}
previewValue={previewValue}
shouldForceTagDropdown={shouldForceTagDropdown}
>
{({ ref, onChange: handleChange, onKeyDown, onDrop, onDragOver, onFocus }) => {
const setRefs = (el: HTMLTextAreaElement | null) => {
@@ -303,7 +326,7 @@ export function LongInput({
return (
<div
ref={containerRef}
className={cn('group relative w-full', wandHook.isStreaming && 'streaming-effect')}
className='group relative w-full'
style={{ height: `${height}px` }}
>
<Textarea
@@ -1,3 +1,4 @@
import type { RefObject } from 'react'
import { useCallback, useMemo, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useParams } from 'next/navigation'
@@ -111,7 +112,14 @@ function McpInputWithTags({
data-lpignore='true'
data-1p-ignore
readOnly
onFocus={(e) => e.currentTarget.removeAttribute('readOnly')}
onFocus={(e) => {
e.currentTarget.removeAttribute('readOnly')
// Show tag dropdown on focus when input is empty
if (!disabled && (value?.trim() === '' || !value)) {
setShowTags(true)
setCursorPosition(0)
}
}}
className={cn(!isPassword && 'text-transparent caret-foreground')}
/>
{!isPassword && (
@@ -136,6 +144,7 @@ function McpInputWithTags({
setShowTags(false)
setActiveSourceBlockId(null)
}}
inputRef={inputRef as RefObject<HTMLInputElement>}
/>
</div>
)
@@ -225,6 +234,13 @@ function McpTextareaWithTags({
onChange={handleChange}
onDrop={handleDrop}
onDragOver={handleDragOver}
onFocus={() => {
// Show tag dropdown on focus when input is empty
if (!disabled && (value?.trim() === '' || !value)) {
setShowTags(true)
setCursorPosition(0)
}
}}
placeholder={placeholder}
disabled={disabled}
rows={rows}
@@ -254,6 +270,7 @@ function McpTextareaWithTags({
setShowTags(false)
setActiveSourceBlockId(null)
}}
inputRef={textareaRef as RefObject<HTMLTextAreaElement>}
/>
</div>
)
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import { ChevronDown, ChevronsUpDown, ChevronUp, Plus } from 'lucide-react'
import { Button, Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn'
import { Trash } from '@/components/emcn/icons/trash'
@@ -8,12 +8,30 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand'
import type { SubBlockConfig } from '@/blocks/types'
const MIN_TEXTAREA_HEIGHT_PX = 80
const MAX_TEXTAREA_HEIGHT_PX = 320
/** Pattern to match complete message objects in JSON */
const COMPLETE_MESSAGE_PATTERN =
/"role"\s*:\s*"(system|user|assistant)"[^}]*"content"\s*:\s*"((?:[^"\\]|\\.)*)"/g
/** Pattern to match incomplete content at end of buffer */
const INCOMPLETE_CONTENT_PATTERN = /"content"\s*:\s*"((?:[^"\\]|\\.)*)$/
/** Pattern to match role before content */
const ROLE_BEFORE_CONTENT_PATTERN = /"role"\s*:\s*"(system|user|assistant)"[^{]*$/
/**
* Unescapes JSON string content
*/
const unescapeContent = (str: string): string =>
str.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\')
/**
* Interface for individual message in the messages array
*/
@@ -38,6 +56,8 @@ interface MessagesInputProps {
previewValue?: Message[] | null
/** Whether the input is disabled */
disabled?: boolean
/** Ref to expose wand control handlers to parent */
wandControlRef?: React.MutableRefObject<WandControlHandlers | null>
}
/**
@@ -55,6 +75,7 @@ export function MessagesInput({
isPreview = false,
previewValue,
disabled = false,
wandControlRef,
}: MessagesInputProps) {
const [messages, setMessages] = useSubBlockValue<Message[]>(blockId, subBlockId, false)
const [localMessages, setLocalMessages] = useState<Message[]>([{ role: 'user', content: '' }])
@@ -68,6 +89,142 @@ export function MessagesInput({
disabled,
})
/**
* Gets the current messages as JSON string for wand context
*/
const getMessagesJson = useCallback((): string => {
if (localMessages.length === 0) return ''
// Filter out empty messages for cleaner context
const nonEmptyMessages = localMessages.filter((m) => m.content.trim() !== '')
if (nonEmptyMessages.length === 0) return ''
return JSON.stringify(nonEmptyMessages, null, 2)
}, [localMessages])
/**
* Streaming buffer for accumulating JSON content
*/
const streamBufferRef = useRef<string>('')
/**
* Parses and validates messages from JSON content
*/
const parseMessages = useCallback((content: string): Message[] | null => {
try {
const parsed = JSON.parse(content)
if (Array.isArray(parsed)) {
const validMessages: Message[] = parsed
.filter(
(m): m is { role: string; content: string } =>
typeof m === 'object' &&
m !== null &&
typeof m.role === 'string' &&
typeof m.content === 'string'
)
.map((m) => ({
role: (['system', 'user', 'assistant'].includes(m.role)
? m.role
: 'user') as Message['role'],
content: m.content,
}))
return validMessages.length > 0 ? validMessages : null
}
} catch {
// Parsing failed
}
return null
}, [])
/**
* Extracts messages from streaming JSON buffer
* Uses simple pattern matching for efficiency
*/
const extractStreamingMessages = useCallback(
(buffer: string): Message[] => {
// Try complete JSON parse first
const complete = parseMessages(buffer)
if (complete) return complete
const result: Message[] = []
// Reset regex lastIndex for global pattern
COMPLETE_MESSAGE_PATTERN.lastIndex = 0
let match
while ((match = COMPLETE_MESSAGE_PATTERN.exec(buffer)) !== null) {
result.push({ role: match[1] as Message['role'], content: unescapeContent(match[2]) })
}
// Check for incomplete message at end (content still streaming)
const lastContentIdx = buffer.lastIndexOf('"content"')
if (lastContentIdx !== -1) {
const tail = buffer.slice(lastContentIdx)
const incomplete = tail.match(INCOMPLETE_CONTENT_PATTERN)
if (incomplete) {
const head = buffer.slice(0, lastContentIdx)
const roleMatch = head.match(ROLE_BEFORE_CONTENT_PATTERN)
if (roleMatch) {
const content = unescapeContent(incomplete[1])
// Only add if not duplicate of last complete message
if (result.length === 0 || result[result.length - 1].content !== content) {
result.push({ role: roleMatch[1] as Message['role'], content })
}
}
}
}
return result
},
[parseMessages]
)
/**
* Wand hook for AI-assisted content generation
*/
const wandHook = useWand({
wandConfig: config.wandConfig,
currentValue: getMessagesJson(),
onStreamStart: () => {
streamBufferRef.current = ''
setLocalMessages([{ role: 'system', content: '' }])
},
onStreamChunk: (chunk) => {
streamBufferRef.current += chunk
const extracted = extractStreamingMessages(streamBufferRef.current)
if (extracted.length > 0) {
setLocalMessages(extracted)
}
},
onGeneratedContent: (content) => {
const validMessages = parseMessages(content)
if (validMessages) {
setLocalMessages(validMessages)
setMessages(validMessages)
} else {
// Fallback: treat as raw system prompt
const trimmed = content.trim()
if (trimmed) {
const fallback: Message[] = [{ role: 'system', content: trimmed }]
setLocalMessages(fallback)
setMessages(fallback)
}
}
},
})
/**
* Expose wand control handlers to parent via ref
*/
useImperativeHandle(
wandControlRef,
() => ({
onWandTrigger: (prompt: string) => {
wandHook.generateStream({ prompt })
},
isWandActive: wandHook.isPromptVisible,
isWandStreaming: wandHook.isStreaming,
}),
[wandHook]
)
/**
* Initialize local state from stored or preview value
*/
@@ -308,7 +465,7 @@ export function MessagesInput({
}, [currentMessages, autoResizeTextarea])
return (
<div className='flex w-full flex-col gap-3'>
<div className='flex w-full flex-col gap-[10px]'>
{currentMessages.map((message, index) => (
<div
key={`message-${index}`}
@@ -364,7 +521,7 @@ export function MessagesInput({
type='button'
disabled={isPreview || disabled}
className={cn(
'-ml-1.5 -my-1 rounded px-1.5 py-1 font-medium text-[13px] text-[var(--text-primary)] leading-none transition-colors hover:bg-[var(--surface-5)] hover:text-[var(--text-secondary)]',
'group -ml-1.5 -my-1 flex items-center gap-1 rounded px-1.5 py-1 font-medium text-[13px] text-[var(--text-primary)] leading-none transition-colors hover:bg-[var(--surface-5)] hover:text-[var(--text-secondary)]',
(isPreview || disabled) &&
'cursor-default hover:bg-transparent hover:text-[var(--text-primary)]'
)}
@@ -372,6 +529,14 @@ export function MessagesInput({
aria-label='Select message role'
>
{formatRole(message.role)}
{!isPreview && !disabled && (
<ChevronDown
className={cn(
'h-3 w-3 flex-shrink-0 transition-transform duration-100',
openPopoverIndex === index && 'rotate-180'
)}
/>
)}
</button>
</PopoverTrigger>
<PopoverContent minWidth={140} align='start'>
@@ -486,6 +651,7 @@ export function MessagesInput({
}}
onDrop={fieldHandlers.onDrop}
onDragOver={fieldHandlers.onDragOver}
onFocus={fieldHandlers.onFocus}
onScroll={(e) => {
const overlay = overlayRefs.current[fieldId]
if (overlay) {
@@ -192,6 +192,24 @@ export function ShortInput({
[isApiKeyField, isPreview, disabled, readOnly]
)
const shouldForceTagDropdown = useCallback(
({
value,
}: {
value: string
cursor: number
event: 'focus'
}): { show: boolean } | undefined => {
if (isPreview || disabled || readOnly) return { show: false }
// Show tag dropdown on focus when input is empty (unless it's an API key field)
if (!isApiKeyField && value.trim() === '') {
return { show: true }
}
return { show: false }
},
[isPreview, disabled, readOnly, isApiKeyField]
)
const baseValue = isPreview ? previewValue : propValue !== undefined ? propValue : undefined
const effectiveValue =
@@ -316,6 +334,7 @@ export function ShortInput({
isStreaming={wandHook.isStreaming}
previewValue={previewValue}
shouldForceEnvDropdown={shouldForceEnvDropdown}
shouldForceTagDropdown={shouldForceTagDropdown}
>
{({
ref,
@@ -356,9 +375,9 @@ export function ShortInput({
type='text'
value={displayValue}
onChange={handleChange as (e: React.ChangeEvent<HTMLInputElement>) => void}
onFocus={() => {
onFocus={(e) => {
setIsFocused(true)
onFocus()
onFocus(e)
}}
onBlur={handleBlur}
onDrop={onDrop as (e: React.DragEvent<HTMLInputElement>) => void}
@@ -211,6 +211,7 @@ export function FieldFormat({
onKeyDown={handlers.onKeyDown}
onDrop={handlers.onDrop}
onDragOver={handlers.onDragOver}
onFocus={handlers.onFocus}
onScroll={(e) => syncNameOverlayScroll(field.id, e.currentTarget.scrollLeft)}
onPaste={() =>
setTimeout(() => {
@@ -464,6 +465,7 @@ export function FieldFormat({
onKeyDown={handlers.onKeyDown}
onDrop={handlers.onDrop}
onDragOver={handlers.onDragOver}
onFocus={handlers.onFocus}
onScroll={(e) => syncOverlayScroll(field.id, e.currentTarget.scrollLeft)}
onPaste={() =>
setTimeout(() => {
@@ -45,6 +45,16 @@ export interface SubBlockInputControllerProps {
cursor: number
event: 'change' | 'focus' | 'deleteAll'
}) => { show: boolean; searchTerm?: string } | undefined
/**
* Optional callback to force/show the tag dropdown (e.g., empty inputs).
* Return { show: true } to override defaults.
* Called on 'focus' event.
*/
shouldForceTagDropdown?: (args: {
value: string
cursor: number
event: 'focus'
}) => { show: boolean } | undefined
/** Render prop for the actual input element. */
children: (args: {
ref: React.RefObject<HTMLTextAreaElement | HTMLInputElement>
@@ -54,7 +64,7 @@ export interface SubBlockInputControllerProps {
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) => void
onDrop: (e: React.DragEvent<HTMLTextAreaElement | HTMLInputElement>) => void
onDragOver: (e: React.DragEvent<HTMLTextAreaElement | HTMLInputElement>) => void
onFocus: () => void
onFocus: (e: React.FocusEvent<HTMLTextAreaElement | HTMLInputElement>) => void
onScroll?: (e: React.UIEvent<HTMLTextAreaElement>) => void
}) => React.ReactNode
}
@@ -76,6 +86,7 @@ export function SubBlockInputController(props: SubBlockInputControllerProps): Re
previewValue,
workspaceId,
shouldForceEnvDropdown,
shouldForceTagDropdown,
children,
} = props
@@ -92,6 +103,7 @@ export function SubBlockInputController(props: SubBlockInputControllerProps): Re
previewValue,
workspaceId,
shouldForceEnvDropdown,
shouldForceTagDropdown,
})
const emitTagSelection = useTagSelection(blockId, subBlockId)
@@ -238,6 +238,7 @@ export function Table({
onScroll={handleScroll}
onDrop={handlers.onDrop}
onDragOver={handlers.onDragOver}
onFocus={handlers.onFocus}
disabled={isPreview || disabled}
autoComplete='off'
className='w-full border-0 bg-transparent px-[10px] py-[8px] font-medium text-sm text-transparent leading-[21px] caret-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus-visible:ring-0 focus-visible:ring-offset-0'
@@ -1210,7 +1210,6 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
const textAfterCursor = liveValue.slice(liveCursor)
const lastOpenBracket = textBeforeCursor.lastIndexOf('<')
if (lastOpenBracket === -1) return
let processedTag = tag
@@ -1256,17 +1255,25 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
}
}
const nextCloseBracket = textAfterCursor.indexOf('>')
let remainingTextAfterCursor = textAfterCursor
let newValue: string
if (nextCloseBracket !== -1) {
const textBetween = textAfterCursor.slice(0, nextCloseBracket)
if (/^[a-zA-Z0-9._]*$/.test(textBetween)) {
remainingTextAfterCursor = textAfterCursor.slice(nextCloseBracket + 1)
if (lastOpenBracket === -1) {
// No '<' found - insert the full tag at cursor position
newValue = `${textBeforeCursor}<${processedTag}>${textAfterCursor}`
} else {
// '<' found - replace from '<' to cursor (and consume trailing '>' if present)
const nextCloseBracket = textAfterCursor.indexOf('>')
let remainingTextAfterCursor = textAfterCursor
if (nextCloseBracket !== -1) {
const textBetween = textAfterCursor.slice(0, nextCloseBracket)
if (/^[a-zA-Z0-9._]*$/.test(textBetween)) {
remainingTextAfterCursor = textAfterCursor.slice(nextCloseBracket + 1)
}
}
}
const newValue = `${textBeforeCursor.slice(0, lastOpenBracket)}<${processedTag}>${remainingTextAfterCursor}`
newValue = `${textBeforeCursor.slice(0, lastOpenBracket)}<${processedTag}>${remainingTextAfterCursor}`
}
onSelect(newValue)
onClose?.()
@@ -381,6 +381,13 @@ export function VariablesInput({
e.target.selectionStart ?? undefined
)
}
onFocus={() => {
if (!isPreview && !disabled && !assignment.value?.trim()) {
setActiveFieldId(assignment.id)
setCursorPosition(0)
setShowTags(true)
}
}}
placeholder={
assignment.type === 'object'
? '{\n "key": "value"\n}'
@@ -434,6 +441,13 @@ export function VariablesInput({
e.target.selectionStart ?? undefined
)
}
onFocus={() => {
if (!isPreview && !disabled && !assignment.value?.trim()) {
setActiveFieldId(assignment.id)
setCursorPosition(0)
setShowTags(true)
}
}}
placeholder={`${assignment.type} value`}
disabled={isPreview || disabled}
autoComplete='off'
@@ -475,6 +489,11 @@ export function VariablesInput({
inputValue={assignment.value || ''}
cursorPosition={cursorPosition}
onClose={() => setShowTags(false)}
inputRef={
{
current: valueInputRefs.current[assignment.id] || null,
} as React.RefObject<HTMLTextAreaElement | HTMLInputElement>
}
/>
)}
</div>
@@ -50,6 +50,16 @@ export interface UseSubBlockInputOptions {
cursor: number
event: 'change' | 'focus' | 'deleteAll'
}) => { show: boolean; searchTerm?: string } | undefined
/**
* Optional callback to force/show the tag dropdown (e.g., empty inputs).
* Return { show: true } to override defaults.
* Called on 'focus' event.
*/
shouldForceTagDropdown?: (args: {
value: string
cursor: number
event: 'focus'
}) => { show: boolean } | undefined
}
/**
@@ -89,7 +99,7 @@ export interface UseSubBlockInputResult {
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) => void
onDrop: (e: React.DragEvent<HTMLTextAreaElement | HTMLInputElement>) => void
onDragOver: (e: React.DragEvent<HTMLTextAreaElement | HTMLInputElement>) => void
onFocus: () => void
onFocus: (e: React.FocusEvent<HTMLTextAreaElement | HTMLInputElement>) => void
onScroll?: (e: React.UIEvent<HTMLTextAreaElement>) => void
}
/** Workspace id for env var dropdown scoping. */
@@ -114,6 +124,7 @@ export interface UseSubBlockInputResult {
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) => void
onDrop: (e: React.DragEvent<HTMLTextAreaElement | HTMLInputElement>) => void
onDragOver: (e: React.DragEvent<HTMLTextAreaElement | HTMLInputElement>) => void
onFocus: (e: React.FocusEvent<HTMLTextAreaElement | HTMLInputElement>) => void
}
/** Hide dropdowns for a specific field */
hideFieldDropdowns: (fieldId: string) => void
@@ -153,6 +164,7 @@ export function useSubBlockInput(options: UseSubBlockInputOptions): UseSubBlockI
previewValue,
workspaceId: workspaceIdProp,
shouldForceEnvDropdown,
shouldForceTagDropdown,
} = options
const params = useParams()
@@ -338,22 +350,42 @@ export function useSubBlockInput(options: UseSubBlockInputOptions): UseSubBlockI
[config?.connectionDroppable, valueString, onChange, isPreview, setStoreValue]
)
const handleFocus = useCallback(() => {
if (shouldForceEnvDropdown) {
// Use a slight delay to ensure the input ref is populated
setTimeout(() => {
const forced = shouldForceEnvDropdown({
value: (inputRef.current as any)?.value ?? valueString,
cursor: (inputRef.current as any)?.selectionStart ?? valueString.length,
const handleFocus = useCallback(
(e: React.FocusEvent<HTMLTextAreaElement | HTMLInputElement>) => {
// Get values directly from the event target - no setTimeout needed
const target = e.target as HTMLInputElement | HTMLTextAreaElement
const currentValue = target.value ?? valueString
const currentCursor = target.selectionStart ?? currentValue.length
// Check if tag dropdown should be forced (takes priority as it's more commonly used)
if (shouldForceTagDropdown) {
const forcedTag = shouldForceTagDropdown({
value: currentValue,
cursor: currentCursor,
event: 'focus',
})
if (forced?.show) {
setShowEnvVars(true)
setSearchTerm(forced.searchTerm ?? '')
if (forcedTag?.show) {
setCursorPosition(currentCursor)
setShowTags(true)
return // Exit early if tag dropdown is shown
}
}, 0)
}
}, [shouldForceEnvDropdown, valueString])
}
// Check if env var dropdown should be forced
if (shouldForceEnvDropdown) {
const forcedEnv = shouldForceEnvDropdown({
value: currentValue,
cursor: currentCursor,
event: 'focus',
})
if (forcedEnv?.show) {
setShowEnvVars(true)
setSearchTerm(forcedEnv.searchTerm ?? '')
}
}
},
[shouldForceEnvDropdown, shouldForceTagDropdown, valueString]
)
const onScroll = useCallback((_: React.UIEvent<HTMLTextAreaElement>) => {
// Intentionally empty; consumers may mirror scroll to overlays if needed
@@ -469,6 +501,17 @@ export function useSubBlockInput(options: UseSubBlockInputOptions): UseSubBlockI
if (config?.connectionDroppable === false) return
e.preventDefault()
},
onFocus: (e: React.FocusEvent<HTMLTextAreaElement | HTMLInputElement>) => {
// Show tag dropdown on focus when field value is empty
const target = e.target as HTMLInputElement | HTMLTextAreaElement
const currentValue = target.value ?? fieldValue
if (!isDisabled && !isStreaming && currentValue.trim() === '') {
updateFieldState(fieldId, {
showTags: true,
cursorPosition: target.selectionStart ?? 0,
})
}
},
}
},
[isDisabled, isStreaming, config?.connectionDroppable, updateFieldState]
@@ -1,7 +1,6 @@
import { type JSX, type MouseEvent, memo, useRef, useState } from 'react'
import { AlertTriangle, Wand2 } from 'lucide-react'
import { Label, Tooltip } from '@/components/emcn/components'
import { Button } from '@/components/ui/button'
import { AlertTriangle, ArrowUp } from 'lucide-react'
import { Button, Input, Label, Tooltip } from '@/components/emcn/components'
import { cn } from '@/lib/core/utils/cn'
import type { FieldDiffStatus } from '@/lib/workflows/diff/types'
import {
@@ -154,22 +153,23 @@ const getPreviewValue = (
}
/**
* Renders the label with optional validation, description tooltips, and inline wand control.
* Renders the label with optional validation and description tooltips.
*
* @remarks
* Handles JSON validation indicators for code blocks, required field markers,
* and AI generation (wand) input interface.
* Handles JSON validation indicators for code blocks and required field markers.
* Includes inline AI generate button when wand is enabled.
*
* @param config - The sub-block configuration defining the label content
* @param isValidJson - Whether the JSON content is valid (for code blocks)
* @param wandState - State and handlers for the AI wand feature
* @param subBlockValues - Current values of all subblocks for evaluating conditional requirements
* @param wandState - Optional state and handlers for the AI wand feature
* @returns The label JSX element, or `null` for switch types or when no title is defined
*/
const renderLabel = (
config: SubBlockConfig,
isValidJson: boolean,
wandState: {
subBlockValues?: Record<string, any>,
wandState?: {
isSearchActive: boolean
searchQuery: string
isWandEnabled: boolean
@@ -182,31 +182,19 @@ const renderLabel = (
onSearchSubmit: () => void
onSearchCancel: () => void
searchInputRef: React.RefObject<HTMLInputElement | null>
},
subBlockValues?: Record<string, any>
}
): JSX.Element | null => {
if (config.type === 'switch') return null
if (!config.title) return null
const {
isSearchActive,
searchQuery,
isWandEnabled,
isPreview,
isStreaming,
disabled,
onSearchClick,
onSearchBlur,
onSearchChange,
onSearchSubmit,
onSearchCancel,
searchInputRef,
} = wandState
const required = isFieldRequired(config, subBlockValues)
const showWand = wandState?.isWandEnabled && !wandState.isPreview && !wandState.disabled
return (
<Label className='flex items-center justify-between gap-[6px] pl-[2px]'>
<Label
className='flex items-center justify-between gap-[6px] pl-[2px]'
onClick={(e) => e.preventDefault()}
>
<div className='flex items-center gap-[6px] whitespace-nowrap'>
{config.title}
{required && <span className='ml-0.5'>*</span>}
@@ -226,42 +214,55 @@ const renderLabel = (
</Tooltip.Root>
)}
</div>
{/* Wand inline prompt */}
{isWandEnabled && !isPreview && !disabled && (
<div className='flex min-w-0 flex-1 items-center justify-end pr-[4px]'>
{!isSearchActive ? (
{showWand && (
<>
{!wandState.isSearchActive ? (
<Button
variant='ghost'
className='h-[12px] w-[12px] flex-shrink-0 p-0 hover:bg-transparent'
aria-label='Generate with AI'
onClick={onSearchClick}
variant='active'
className='-my-1 h-5 px-2 py-0 text-[11px]'
onClick={wandState.onSearchClick}
>
<Wand2 className='!h-[12px] !w-[12px] bg-transparent text-[var(--text-secondary)]' />
Generate
</Button>
) : (
<input
ref={searchInputRef}
type='text'
value={isStreaming ? 'Generating...' : searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
onBlur={onSearchBlur}
onKeyDown={(e) => {
if (e.key === 'Enter' && searchQuery.trim() && !isStreaming) {
onSearchSubmit()
} else if (e.key === 'Escape') {
onSearchCancel()
}
}}
disabled={isStreaming}
className={cn(
'h-[12px] w-full min-w-[100px] border-none bg-transparent py-0 pr-[2px] text-right font-medium text-[12px] text-[var(--text-primary)] leading-[14px] placeholder:text-[var(--text-muted)] focus:outline-none',
isStreaming && 'text-muted-foreground'
)}
placeholder='Describe...'
/>
<div className='-my-1 flex items-center gap-[4px]'>
<Input
ref={wandState.searchInputRef}
value={wandState.isStreaming ? 'Generating...' : wandState.searchQuery}
onChange={(e) => wandState.onSearchChange(e.target.value)}
onBlur={wandState.onSearchBlur}
onKeyDown={(e) => {
if (e.key === 'Enter' && wandState.searchQuery.trim() && !wandState.isStreaming) {
wandState.onSearchSubmit()
} else if (e.key === 'Escape') {
wandState.onSearchCancel()
}
}}
disabled={wandState.isStreaming}
className={cn(
'h-5 max-w-[200px] flex-1 text-[11px]',
wandState.isStreaming && 'text-muted-foreground'
)}
placeholder='Generate...'
/>
<Button
variant='tertiary'
disabled={!wandState.searchQuery.trim() || wandState.isStreaming}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={(e) => {
e.stopPropagation()
wandState.onSearchSubmit()
}}
className='h-[20px] w-[20px] flex-shrink-0 p-0'
>
<ArrowUp className='h-[12px] w-[12px]' />
</Button>
</div>
)}
</div>
</>
)}
</Label>
)
@@ -875,6 +876,7 @@ function SubBlockComponent({
isPreview={isPreview}
previewValue={previewValue as any}
disabled={isDisabled}
wandControlRef={wandControlRef}
/>
)
@@ -885,25 +887,20 @@ function SubBlockComponent({
return (
<div onMouseDown={handleMouseDown} className='subblock-content flex flex-col gap-[10px]'>
{renderLabel(
config,
isValidJson,
{
isSearchActive,
searchQuery,
isWandEnabled,
isPreview,
isStreaming: wandControlRef.current?.isWandStreaming ?? false,
disabled: isDisabled,
onSearchClick: handleSearchClick,
onSearchBlur: handleSearchBlur,
onSearchChange: handleSearchChange,
onSearchSubmit: handleSearchSubmit,
onSearchCancel: handleSearchCancel,
searchInputRef,
},
subBlockValues
)}
{renderLabel(config, isValidJson, subBlockValues, {
isSearchActive,
searchQuery,
isWandEnabled,
isPreview,
isStreaming: wandControlRef.current?.isWandStreaming ?? false,
disabled: isDisabled,
onSearchClick: handleSearchClick,
onSearchBlur: handleSearchBlur,
onSearchChange: handleSearchChange,
onSearchSubmit: handleSearchSubmit,
onSearchCancel: handleSearchCancel,
searchInputRef,
})}
{renderInput()}
</div>
)
@@ -342,7 +342,7 @@ export function Editor() {
ref={subBlocksRef}
className='subblocks-section flex flex-1 flex-col overflow-hidden'
>
<div className='flex-1 overflow-y-auto overflow-x-hidden px-[8px] pt-[8px] pb-[8px]'>
<div className='flex-1 overflow-y-auto overflow-x-hidden px-[8px] pt-[12px] pb-[8px]'>
{subBlocks.length === 0 ? (
<div className='flex h-full items-center justify-center text-center text-[#8D8D8D] text-[13px]'>
This block has no subblocks
@@ -4,7 +4,7 @@ import type { WorkflowBlockProps } from '@/app/workspace/[workspaceId]/w/[workfl
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
import { getBlockRingStyles } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils'
import { useExecutionStore } from '@/stores/execution'
import { usePanelEditorStore } from '@/stores/panel'
import { usePanelEditorStore, usePanelStore } from '@/stores/panel'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
/**
@@ -21,7 +21,8 @@ interface UseBlockVisualProps {
/**
* Provides visual state and interaction handlers for workflow blocks.
* Computes ring styling based on execution, diff, deletion, and run path states.
* Computes ring styling based on editor open state, execution, diff, deletion, and run path states.
* Ring is shown only when the editor panel is open for this block, not during selection/dragging.
* In preview mode, uses isPreviewSelected for selection highlighting.
*
* @param props - The hook properties
@@ -36,13 +37,15 @@ export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVis
const {
isEnabled,
isActive: blockIsActive,
isActive: isExecuting,
diffStatus,
isDeletedBlock,
} = useBlockState(blockId, currentWorkflow, data)
// In preview mode, use isPreviewSelected for selection state
const isActive = isPreview ? isPreviewSelected : blockIsActive
// Check if the editor panel is open for this block
const currentBlockId = usePanelEditorStore((state) => state.currentBlockId)
const activeTab = usePanelStore((state) => state.activeTab)
const isEditorOpen = !isPreview && currentBlockId === blockId && activeTab === 'editor'
const lastRunPath = useExecutionStore((state) => state.lastRunPath)
const runPathStatus = isPreview ? undefined : lastRunPath.get(blockId)
@@ -58,14 +61,24 @@ export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVis
const { hasRing, ringClassName: ringStyles } = useMemo(
() =>
getBlockRingStyles({
isActive,
isExecuting: isPreview ? false : isExecuting,
isEditorOpen: isPreview ? isPreviewSelected : isEditorOpen,
isPending: isPreview ? false : isPending,
isDeletedBlock: isPreview ? false : isDeletedBlock,
diffStatus: isPreview ? undefined : diffStatus,
runPathStatus,
isPreviewSelection: isPreview && isPreviewSelected,
}),
[isActive, isPending, isDeletedBlock, diffStatus, runPathStatus, isPreview, isPreviewSelected]
[
isExecuting,
isEditorOpen,
isPending,
isDeletedBlock,
diffStatus,
runPathStatus,
isPreview,
isPreviewSelected,
]
)
return {
@@ -5,7 +5,10 @@ export type BlockDiffStatus = 'new' | 'edited' | null | undefined
export type BlockRunPathStatus = 'success' | 'error' | undefined
export interface BlockRingOptions {
isActive: boolean
/** Whether the block is executing (shows green pulsing ring) */
isExecuting: boolean
/** Whether the editor panel is open for this block (shows blue ring) */
isEditorOpen: boolean
isPending: boolean
isDeletedBlock: boolean
diffStatus: BlockDiffStatus
@@ -15,17 +18,25 @@ export interface BlockRingOptions {
/**
* Derives visual ring visibility and class names for workflow blocks
* based on execution, diff, deletion, and run-path states.
* based on editor open state, execution, diff, deletion, and run-path states.
*/
export function getBlockRingStyles(options: BlockRingOptions): {
hasRing: boolean
ringClassName: string
} {
const { isActive, isPending, isDeletedBlock, diffStatus, runPathStatus, isPreviewSelection } =
options
const {
isExecuting,
isEditorOpen,
isPending,
isDeletedBlock,
diffStatus,
runPathStatus,
isPreviewSelection,
} = options
const hasRing =
isActive ||
isExecuting ||
isEditorOpen ||
isPending ||
diffStatus === 'new' ||
diffStatus === 'edited' ||
@@ -33,37 +44,41 @@ export function getBlockRingStyles(options: BlockRingOptions): {
!!runPathStatus
const ringClassName = cn(
// Preview selection: static blue ring (standard thickness, no animation)
isActive && isPreviewSelection && 'ring-[1.75px] ring-[var(--brand-secondary)]',
// Executing block: pulsing success ring with prominent thickness
isActive &&
!isPreviewSelection &&
'ring-[3.5px] ring-[var(--border-success)] animate-ring-pulse',
// Executing block: pulsing success ring with prominent thickness (highest priority)
isExecuting && 'ring-[3.5px] ring-[var(--border-success)] animate-ring-pulse',
// Editor open or preview selection: static blue ring
!isExecuting &&
(isEditorOpen || isPreviewSelection) &&
'ring-[1.75px] ring-[var(--brand-secondary)]',
// Non-active states use standard ring utilities
!isActive && hasRing && 'ring-[1.75px]',
!isExecuting && !isEditorOpen && !isPreviewSelection && hasRing && 'ring-[1.75px]',
// Pending state: warning ring
!isActive && isPending && 'ring-[var(--warning)]',
!isExecuting && !isEditorOpen && isPending && 'ring-[var(--warning)]',
// Deleted state (highest priority after active/pending)
!isActive && !isPending && isDeletedBlock && 'ring-[var(--text-error)]',
!isExecuting && !isEditorOpen && !isPending && isDeletedBlock && 'ring-[var(--text-error)]',
// Diff states
!isActive &&
!isExecuting &&
!isEditorOpen &&
!isPending &&
!isDeletedBlock &&
diffStatus === 'new' &&
'ring-[var(--brand-tertiary-2)]',
!isActive &&
!isExecuting &&
!isEditorOpen &&
!isPending &&
!isDeletedBlock &&
diffStatus === 'edited' &&
'ring-[var(--warning)]',
// Run path states (lowest priority - only show if no other states active)
!isActive &&
!isExecuting &&
!isEditorOpen &&
!isPending &&
!isDeletedBlock &&
!diffStatus &&
runPathStatus === 'success' &&
'ring-[var(--border-success)]',
!isActive &&
!isExecuting &&
!isEditorOpen &&
!isPending &&
!isDeletedBlock &&
!diffStatus &&
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Check } from 'lucide-react'
import {
Button,
Popover,
PopoverAnchor,
PopoverBackButton,
@@ -368,17 +369,17 @@ export function ContextMenu({
onClick={(e) => e.stopPropagation()}
className='h-[20px] min-w-0 flex-1 rounded-[4px] bg-[#363636] px-[6px] text-[11px] text-white uppercase caret-white focus:outline-none'
/>
<button
type='button'
<Button
variant='tertiary'
disabled={!canSubmitHex}
onClick={(e) => {
e.stopPropagation()
handleHexSubmit()
}}
className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-[4px] bg-[var(--brand-tertiary-2)] text-white disabled:opacity-40'
className='h-[20px] w-[20px] flex-shrink-0 p-0'
>
<Check className='h-[12px] w-[12px]' />
</button>
</Button>
</div>
</div>
</PopoverFolder>
+36 -1
View File
@@ -83,9 +83,44 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
subBlocks: [
{
id: 'messages',
// title: 'Messages',
title: 'Messages',
type: 'messages-input',
placeholder: 'Enter messages...',
wandConfig: {
enabled: true,
maintainHistory: true,
prompt: `You are an expert at creating professional, comprehensive LLM agent configurations. Generate or modify a JSON array of messages based on the user's request.
Current messages: {context}
RULES:
1. Generate ONLY a valid JSON array - no markdown, no explanations
2. Each message object must have "role" (system/user/assistant) and "content" (string)
3. You can generate any number of messages as needed
4. Content can be as long as necessary - don't truncate
5. If editing existing messages, preserve structure unless asked to change it
6. For new agents, create DETAILED, PROFESSIONAL system prompts that include:
- A clear role definition starting with "You are..."
- Specific methodology or approach guidelines
- Structured output format requirements
- Critical thinking or quality guidelines
- How to handle edge cases and uncertainty
EXAMPLES:
Research agent:
[{"role": "system", "content": "You are a Research Specialist who synthesizes information into actionable insights.\\n\\n## Approach\\n- Identify key concepts, historical context, and future implications\\n- Distinguish between: established facts, emerging research, contested positions, and your inferences\\n- Attribute claims clearly (e.g., \\"Research suggests...\\", \\"Industry consensus holds...\\")\\n\\n## Response Structure\\n1. **Executive Summary**: 2-3 sentences on key findings\\n2. **Key Findings**: Numbered insights with supporting context\\n3. **Analysis**: Organized by themes—background, current state, perspectives, implications\\n4. **Limitations**: What's uncertain or incomplete\\n5. **Recommendations**: Actionable next steps\\n\\n## Standards\\n- Present multiple viewpoints; avoid single narratives as definitive\\n- Make assumptions explicit; question conventional wisdom\\n- Assess evidence strength; distinguish correlation from causation\\n- Use hedging language (\\"likely\\", \\"suggests\\") over false certainty\\n- Acknowledge knowledge limits directly"}, {"role": "user", "content": ""}]
Code reviewer:
[{"role": "system", "content": "You are a Senior Code Reviewer with expertise in software architecture, security, and best practices. Your role is to provide thorough, constructive code reviews that improve code quality and help developers grow.\\n\\n## Review Methodology\\n\\n1. **Security First**: Check for vulnerabilities including injection attacks, authentication flaws, data exposure, and insecure dependencies.\\n\\n2. **Code Quality**: Evaluate readability, maintainability, adherence to DRY/SOLID principles, and appropriate abstraction levels.\\n\\n3. **Performance**: Identify potential bottlenecks, unnecessary computations, memory leaks, and optimization opportunities.\\n\\n4. **Testing**: Assess test coverage, edge case handling, and testability of the code structure.\\n\\n## Output Format\\n\\n### Summary\\nBrief overview of the code's purpose and overall assessment.\\n\\n### Critical Issues\\nSecurity vulnerabilities or bugs that must be fixed before merging.\\n\\n### Improvements\\nSuggested enhancements with clear explanations of why and how.\\n\\n### Positive Aspects\\nHighlight well-written code to reinforce good practices.\\n\\nBe specific with line references. Provide code examples for suggested changes. Balance critique with encouragement."}, {"role": "user", "content": "<start.input>"}]
Writing assistant:
[{"role": "system", "content": "You are a skilled Writing Editor and Coach. Your role is to help users improve their writing through constructive feedback, editing suggestions, and guidance on style, clarity, and structure.\\n\\n## Editing Approach\\n\\n1. **Clarity**: Ensure ideas are expressed clearly and concisely. Eliminate jargon unless appropriate for the audience.\\n\\n2. **Structure**: Evaluate logical flow, paragraph organization, and transitions between ideas.\\n\\n3. **Voice & Tone**: Maintain consistency and appropriateness for the intended audience and purpose.\\n\\n4. **Grammar & Style**: Correct errors while respecting the author's voice.\\n\\n## Output Format\\n\\n### Overall Impression\\nBrief assessment of the piece's strengths and areas for improvement.\\n\\n### Structural Feedback\\nComments on organization, flow, and logical progression.\\n\\n### Line-Level Edits\\nSpecific suggestions with explanations, not just corrections.\\n\\n### Revised Version\\nWhen appropriate, provide an edited version demonstrating improvements.\\n\\nBe encouraging while honest. Explain the reasoning behind suggestions to help the writer improve."}, {"role": "user", "content": "<start.input>"}]
Return ONLY the JSON array.`,
placeholder: 'Describe what you want to create or change...',
generationType: 'json-object',
},
},
{
id: 'model',
+1 -1
View File
@@ -42,7 +42,7 @@ export const ResponseBlock: BlockConfig<ResponseBlockOutput> = {
id: 'data',
title: 'Response Data',
type: 'code',
placeholder: '{\n "message": "Hello world",\n "userId": "<variable.userId>"\n}',
placeholder: '{\n "message": "Hello world"\n}',
language: 'json',
condition: { field: 'dataMode', value: 'json' },
description:
@@ -56,8 +56,6 @@ interface CodeContainerProps {
className?: string
/** Inline styles for the container */
style?: React.CSSProperties
/** Whether editor is in streaming/AI generation state */
isStreaming?: boolean
/** Drag and drop handler */
onDragOver?: (e: React.DragEvent) => void
/** Drop handler */
@@ -77,14 +75,7 @@ interface CodeContainerProps {
* </Code.Container>
* ```
*/
function Container({
children,
className,
style,
isStreaming = false,
onDragOver,
onDrop,
}: CodeContainerProps) {
function Container({ children, className, style, onDragOver, onDrop }: CodeContainerProps) {
return (
<div
className={cn(
@@ -94,8 +85,6 @@ function Container({
'dark:bg-[#1F1F1F]',
// Overflow handling for long content
'overflow-x-auto overflow-y-auto',
// Streaming state
isStreaming && 'streaming-effect',
className
)}
style={style}
+1 -1
View File
@@ -28,7 +28,7 @@ export const SIDEBAR_WIDTH = {
/** Right panel width constraints */
export const PANEL_WIDTH = {
DEFAULT: 290,
DEFAULT: 320,
MIN: 290,
/** Maximum is 40% of viewport, enforced dynamically */
MAX_PERCENTAGE: 0.4,
-9
View File
@@ -126,14 +126,6 @@ export default {
strokeDashoffset: '-24',
},
},
'code-shimmer': {
'0%': {
transform: 'translateX(-100%)',
},
'100%': {
transform: 'translateX(100%)',
},
},
'placeholder-pulse': {
'0%, 100%': {
opacity: '0.5',
@@ -156,7 +148,6 @@ export default {
'slide-left': 'slide-left 80s linear infinite',
'slide-right': 'slide-right 80s linear infinite',
'dash-animation': 'dash-animation 1.5s linear infinite',
'code-shimmer': 'code-shimmer 1.5s infinite',
'placeholder-pulse': 'placeholder-pulse 1.5s ease-in-out infinite',
'ring-pulse': 'ring-pulse 1.5s ease-in-out infinite',
},