Comment instead of ff

This commit is contained in:
Siddharth Ganesan
2025-07-09 11:13:28 -07:00
parent 8176b37d89
commit eade867d98
6 changed files with 921 additions and 11 deletions

View File

@@ -1291,6 +1291,7 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
{renderDebugModeToggle()}
<ImportControls disabled={!userPermissions.canEdit} />
<ExportControls disabled={!userPermissions.canRead} />
{/* <WorkflowTextEditorModal disabled={!userPermissions.canEdit} /> */}
{/* {renderPublishButton()} */}
{renderDeployButton()}
{renderRunButton()}

View File

@@ -10,7 +10,6 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { Chat } from './components/chat/chat'
import { ChatModal } from './components/chat/components/chat-modal/chat-modal'
import { Console } from './components/console/console'
import { Copilot } from './components/copilot/copilot'
import { Variables } from './components/variables/variables'
export function Panel() {
@@ -120,7 +119,7 @@ export function Panel() {
>
Variables
</button>
<button
{/* <button
onClick={() => setActiveTab('copilot')}
className={`rounded-md px-3 py-1 text-sm transition-colors ${
activeTab === 'copilot'
@@ -129,19 +128,20 @@ export function Panel() {
}`}
>
Copilot
</button>
</button> */}
</div>
{(activeTab === 'console' || activeTab === 'chat' || activeTab === 'copilot') && (
{(activeTab === 'console' || activeTab === 'chat' /* || activeTab === 'copilot' */) && (
<button
onClick={() => {
if (activeTab === 'console') {
clearConsole(activeWorkflowId)
} else if (activeTab === 'chat') {
clearChat(activeWorkflowId)
} else if (activeTab === 'copilot') {
copilotRef.current?.clearMessages()
}
// else if (activeTab === 'copilot') {
// copilotRef.current?.clearMessages()
// }
}}
className='rounded-md px-3 py-1 text-muted-foreground text-sm transition-colors hover:bg-accent/50 hover:text-foreground'
>
@@ -156,7 +156,8 @@ export function Panel() {
<Chat panelWidth={width} chatMessage={chatMessage} setChatMessage={setChatMessage} />
) : activeTab === 'console' ? (
<Console panelWidth={width} />
) : activeTab === 'copilot' ? (
) : (
/* activeTab === 'copilot' ? (
<Copilot
ref={copilotRef}
panelWidth={width}
@@ -165,8 +166,7 @@ export function Panel() {
fullscreenInput={copilotMessage}
onFullscreenInputChange={setCopilotMessage}
/>
) : (
<Variables panelWidth={width} />
) : */ <Variables panelWidth={width} />
)}
</div>
@@ -200,7 +200,7 @@ export function Panel() {
</Tooltip>
)}
{activeTab === 'copilot' && (
{/* activeTab === 'copilot' && (
<Tooltip>
<TooltipTrigger asChild>
<button
@@ -213,7 +213,7 @@ export function Panel() {
</TooltipTrigger>
<TooltipContent side='left'>Expand Copilot</TooltipContent>
</Tooltip>
)}
) */}
</div>
</div>

View File

@@ -0,0 +1,256 @@
import { createLogger } from '@/lib/logs/console-logger'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { importWorkflowFromYaml } from '@/stores/workflows/yaml/importer'
import type { EditorFormat } from './workflow-text-editor'
const logger = createLogger('WorkflowApplier')
export interface ApplyResult {
success: boolean
errors: string[]
warnings: string[]
appliedOperations: number
}
/**
* Apply workflow changes by using the existing importer for YAML
* or direct state replacement for JSON
*/
export async function applyWorkflowDiff(
content: string,
format: EditorFormat
): Promise<ApplyResult> {
try {
const { activeWorkflowId } = useWorkflowRegistry.getState()
if (!activeWorkflowId) {
return {
success: false,
errors: ['No active workflow found'],
warnings: [],
appliedOperations: 0,
}
}
if (format === 'yaml') {
// Use the existing YAML importer which handles ID mapping and complete state replacement
const workflowActions = {
addBlock: () => {}, // Not used in this path
addEdge: () => {}, // Not used in this path
applyAutoLayout: () => {
// Trigger auto layout after import
setTimeout(() => {
window.dispatchEvent(new CustomEvent('trigger-auto-layout'))
}, 100)
},
setSubBlockValue: () => {}, // Not used in this path
getExistingBlocks: () => useWorkflowStore.getState().blocks,
}
const result = await importWorkflowFromYaml(content, workflowActions)
return {
success: result.success,
errors: result.errors,
warnings: result.warnings,
appliedOperations: result.success ? 1 : 0, // One complete import operation
}
}
// Handle JSON format - complete state replacement
let parsedData: any
try {
parsedData = JSON.parse(content)
} catch (error) {
return {
success: false,
errors: [`Invalid JSON: ${error instanceof Error ? error.message : 'Parse error'}`],
warnings: [],
appliedOperations: 0,
}
}
// Validate JSON structure
if (!parsedData.state || !parsedData.state.blocks) {
return {
success: false,
errors: ['Invalid JSON structure: missing state.blocks'],
warnings: [],
appliedOperations: 0,
}
}
// Extract workflow state and subblock values
const newWorkflowState = {
blocks: parsedData.state.blocks,
edges: parsedData.state.edges || [],
loops: parsedData.state.loops || {},
parallels: parsedData.state.parallels || {},
lastSaved: Date.now(),
isDeployed: parsedData.state.isDeployed || false,
deployedAt: parsedData.state.deployedAt,
deploymentStatuses: parsedData.state.deploymentStatuses || {},
hasActiveSchedule: parsedData.state.hasActiveSchedule || false,
hasActiveWebhook: parsedData.state.hasActiveWebhook || false,
}
// Update local workflow state
useWorkflowStore.setState(newWorkflowState)
// Update subblock values if provided
if (parsedData.subBlockValues) {
useSubBlockStore.setState((state: any) => ({
workflowValues: {
...state.workflowValues,
[activeWorkflowId]: parsedData.subBlockValues,
},
}))
}
// Update workflow metadata if provided
if (parsedData.workflow) {
const { updateWorkflow } = useWorkflowRegistry.getState()
const metadata = parsedData.workflow
updateWorkflow(activeWorkflowId, {
name: metadata.name,
description: metadata.description,
color: metadata.color,
})
}
// Save to database
try {
const response = await fetch(`/api/workflows/${activeWorkflowId}/state`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newWorkflowState),
})
if (!response.ok) {
const errorData = await response.json()
logger.error('Failed to save workflow state:', errorData.error)
return {
success: false,
errors: [`Database save failed: ${errorData.error || 'Unknown error'}`],
warnings: [],
appliedOperations: 0,
}
}
} catch (error) {
logger.error('Failed to save workflow state:', error)
return {
success: false,
errors: [
`Failed to save workflow state: ${error instanceof Error ? error.message : 'Unknown error'}`,
],
warnings: [],
appliedOperations: 0,
}
}
// Trigger auto layout
setTimeout(() => {
window.dispatchEvent(new CustomEvent('trigger-auto-layout'))
}, 100)
return {
success: true,
errors: [],
warnings: [],
appliedOperations: 1, // One complete state replacement
}
} catch (error) {
logger.error('Failed to apply workflow changes:', error)
return {
success: false,
errors: [`Apply failed: ${error instanceof Error ? error.message : 'Unknown error'}`],
warnings: [],
appliedOperations: 0,
}
}
}
/**
* Preview what changes would be applied (simplified for the new approach)
*/
export function previewWorkflowDiff(
content: string,
format: EditorFormat
): {
summary: string
operations: Array<{
type: string
description: string
}>
} {
try {
if (format === 'yaml') {
// For YAML, we would do a complete import
return {
summary: 'Complete workflow replacement from YAML',
operations: [
{
type: 'complete_replacement',
description: 'Replace entire workflow with YAML content',
},
],
}
}
// For JSON, we would do a complete state replacement
let parsedData: any
try {
parsedData = JSON.parse(content)
} catch (error) {
return {
summary: 'Invalid JSON format',
operations: [],
}
}
const operations = []
if (parsedData.state?.blocks) {
const blockCount = Object.keys(parsedData.state.blocks).length
operations.push({
type: 'replace_blocks',
description: `Replace workflow with ${blockCount} blocks`,
})
}
if (parsedData.state?.edges) {
const edgeCount = parsedData.state.edges.length
operations.push({
type: 'replace_edges',
description: `Replace connections with ${edgeCount} edges`,
})
}
if (parsedData.subBlockValues) {
operations.push({
type: 'replace_values',
description: 'Replace all input values',
})
}
if (parsedData.workflow) {
operations.push({
type: 'update_metadata',
description: 'Update workflow metadata',
})
}
return {
summary: 'Complete workflow state replacement from JSON',
operations,
}
} catch (error) {
return {
summary: 'Error analyzing changes',
operations: [],
}
}
}

View File

@@ -0,0 +1,126 @@
import { dump as yamlDump } from 'js-yaml'
import { createLogger } from '@/lib/logs/console-logger'
import { generateWorkflowYaml } from '@/lib/workflows/yaml-generator'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { EditorFormat } from './workflow-text-editor'
const logger = createLogger('WorkflowExporter')
/**
* Get subblock values organized by block for the exporter
*/
function getSubBlockValues() {
const workflowState = useWorkflowStore.getState()
const subBlockStore = useSubBlockStore.getState()
const subBlockValues: Record<string, Record<string, any>> = {}
Object.entries(workflowState.blocks).forEach(([blockId]) => {
subBlockValues[blockId] = {}
// Get all subblock values for this block
Object.keys(workflowState.blocks[blockId].subBlocks || {}).forEach((subBlockId) => {
const value = subBlockStore.getValue(blockId, subBlockId)
if (value !== undefined) {
subBlockValues[blockId][subBlockId] = value
}
})
})
return subBlockValues
}
/**
* Generate full workflow data including metadata and state
*/
export function generateFullWorkflowData() {
const workflowState = useWorkflowStore.getState()
const { workflows, activeWorkflowId } = useWorkflowRegistry.getState()
const currentWorkflow = activeWorkflowId ? workflows[activeWorkflowId] : null
if (!currentWorkflow || !activeWorkflowId) {
throw new Error('No active workflow found')
}
const subBlockValues = getSubBlockValues()
return {
workflow: {
id: activeWorkflowId,
name: currentWorkflow.name,
description: currentWorkflow.description,
color: currentWorkflow.color,
workspaceId: currentWorkflow.workspaceId,
folderId: currentWorkflow.folderId,
},
state: {
blocks: workflowState.blocks,
edges: workflowState.edges,
loops: workflowState.loops,
parallels: workflowState.parallels,
},
subBlockValues,
exportedAt: new Date().toISOString(),
version: '1.0',
}
}
/**
* Export workflow in the specified format
*/
export function exportWorkflow(format: EditorFormat): string {
try {
if (format === 'yaml') {
// Use the existing YAML generator for condensed format
const workflowState = useWorkflowStore.getState()
const subBlockValues = getSubBlockValues()
return generateWorkflowYaml(workflowState, subBlockValues)
}
// Generate full JSON format
const fullData = generateFullWorkflowData()
return JSON.stringify(fullData, null, 2)
} catch (error) {
logger.error(`Failed to export workflow as ${format}:`, error)
throw error
}
}
/**
* Parse workflow content based on format
*/
export function parseWorkflowContent(content: string, format: EditorFormat): any {
if (format === 'yaml') {
const { load: yamlParse } = require('js-yaml')
return yamlParse(content)
}
return JSON.parse(content)
}
/**
* Convert between YAML and JSON formats
*/
export function convertBetweenFormats(
content: string,
fromFormat: EditorFormat,
toFormat: EditorFormat
): string {
if (fromFormat === toFormat) return content
try {
const parsed = parseWorkflowContent(content, fromFormat)
if (toFormat === 'yaml') {
return yamlDump(parsed, {
indent: 2,
lineWidth: -1,
noRefs: true,
sortKeys: false,
})
}
return JSON.stringify(parsed, null, 2)
} catch (error) {
logger.error(`Failed to convert from ${fromFormat} to ${toFormat}:`, error)
throw error
}
}

View File

@@ -0,0 +1,179 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { FileCode } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { applyWorkflowDiff } from './workflow-applier'
import { exportWorkflow } from './workflow-exporter'
import { type EditorFormat, WorkflowTextEditor } from './workflow-text-editor'
const logger = createLogger('WorkflowTextEditorModal')
interface WorkflowTextEditorModalProps {
disabled?: boolean
className?: string
}
export function WorkflowTextEditorModal({
disabled = false,
className,
}: WorkflowTextEditorModalProps) {
const [isOpen, setIsOpen] = useState(false)
const [format, setFormat] = useState<EditorFormat>('yaml')
const [initialContent, setInitialContent] = useState('')
const [isLoading, setIsLoading] = useState(false)
const { activeWorkflowId } = useWorkflowRegistry()
const workflowState = useWorkflowStore()
// Load initial content when modal opens
useEffect(() => {
if (isOpen && activeWorkflowId) {
setIsLoading(true)
try {
const content = exportWorkflow(format)
setInitialContent(content)
} catch (error) {
logger.error('Failed to export workflow:', error)
setInitialContent('# Error loading workflow content')
} finally {
setIsLoading(false)
}
}
}, [isOpen, format, activeWorkflowId])
// Handle format changes
const handleFormatChange = useCallback((newFormat: EditorFormat) => {
setFormat(newFormat)
}, [])
// Handle save operation
const handleSave = useCallback(
async (content: string, contentFormat: EditorFormat) => {
if (!activeWorkflowId) {
return { success: false, errors: ['No active workflow'] }
}
try {
logger.info('Applying workflow changes from text editor', { format: contentFormat })
// Apply changes using the simplified approach
const applyResult = await applyWorkflowDiff(content, contentFormat)
if (applyResult.success) {
logger.info('Successfully applied workflow changes', {
appliedOperations: applyResult.appliedOperations,
})
// Update initial content to reflect current state
setTimeout(() => {
try {
const updatedContent = exportWorkflow(contentFormat)
setInitialContent(updatedContent)
} catch (error) {
logger.error('Failed to refresh content after save:', error)
}
}, 500) // Give more time for the workflow state to update
}
return {
success: applyResult.success,
errors: applyResult.errors,
warnings: applyResult.warnings,
}
} catch (error) {
logger.error('Failed to save workflow changes:', error)
return {
success: false,
errors: [error instanceof Error ? error.message : 'Unknown error'],
}
}
},
[activeWorkflowId]
)
const handleOpenChange = useCallback((open: boolean) => {
setIsOpen(open)
if (!open) {
// Reset state when closing
setInitialContent('')
}
}, [])
const isDisabled = disabled || !activeWorkflowId
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
{isDisabled ? (
<div className='inline-flex h-10 w-10 cursor-not-allowed items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm opacity-50 ring-offset-background transition-colors [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0'>
<FileCode className='h-5 w-5' />
</div>
) : (
<Button
variant='ghost'
size='icon'
className={cn('hover:text-foreground', className)}
>
<FileCode className='h-5 w-5' />
<span className='sr-only'>Edit as Text</span>
</Button>
)}
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>
{isDisabled
? disabled
? 'Text editor not available'
: 'No active workflow'
: 'Edit as Text'}
</TooltipContent>
</Tooltip>
<DialogContent className='flex h-[85vh] w-[90vw] max-w-6xl flex-col p-0'>
<DialogHeader className='flex-shrink-0 border-b px-6 py-4'>
<DialogTitle>Workflow Text Editor</DialogTitle>
<DialogDescription>
Edit your workflow as YAML or JSON. Changes will completely replace the current workflow
when you save.
</DialogDescription>
</DialogHeader>
<div className='flex-1 overflow-hidden'>
{isLoading ? (
<div className='flex h-full items-center justify-center'>
<div className='text-center'>
<div className='mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-primary border-b-2' />
<p className='text-muted-foreground'>Loading workflow content...</p>
</div>
</div>
) : (
<WorkflowTextEditor
initialValue={initialContent}
format={format}
onSave={handleSave}
onFormatChange={handleFormatChange}
disabled={isDisabled}
className='h-full rounded-none border-0'
/>
)}
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,348 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { dump as yamlDump, load as yamlParse } from 'js-yaml'
import { AlertCircle, Check, FileCode, Save } from 'lucide-react'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
import { CodeEditor } from '../workflow-block/components/sub-block/components/tool-input/components/code-editor/code-editor'
const logger = createLogger('WorkflowTextEditor')
export type EditorFormat = 'yaml' | 'json'
interface ValidationError {
line?: number
column?: number
message: string
}
interface WorkflowTextEditorProps {
initialValue: string
format: EditorFormat
onSave: (
content: string,
format: EditorFormat
) => Promise<{ success: boolean; errors?: string[]; warnings?: string[] }>
onFormatChange?: (format: EditorFormat) => void
className?: string
disabled?: boolean
}
export function WorkflowTextEditor({
initialValue,
format,
onSave,
onFormatChange,
className,
disabled = false,
}: WorkflowTextEditorProps) {
const [content, setContent] = useState(initialValue)
const [currentFormat, setCurrentFormat] = useState<EditorFormat>(format)
const [validationErrors, setValidationErrors] = useState<ValidationError[]>([])
const [isSaving, setIsSaving] = useState(false)
const [saveResult, setSaveResult] = useState<{
success: boolean
errors?: string[]
warnings?: string[]
} | null>(null)
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
// Validate content based on format
const validateContent = useCallback((text: string, fmt: EditorFormat): ValidationError[] => {
const errors: ValidationError[] = []
if (!text.trim()) {
return errors // Empty content is valid
}
try {
if (fmt === 'yaml') {
yamlParse(text)
} else if (fmt === 'json') {
JSON.parse(text)
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Parse error'
// Extract line/column info if available
const lineMatch = errorMessage.match(/line (\d+)/i)
const columnMatch = errorMessage.match(/column (\d+)/i)
errors.push({
line: lineMatch ? Number.parseInt(lineMatch[1], 10) : undefined,
column: columnMatch ? Number.parseInt(columnMatch[1], 10) : undefined,
message: errorMessage,
})
}
return errors
}, [])
// Convert between formats
const convertFormat = useCallback(
(text: string, fromFormat: EditorFormat, toFormat: EditorFormat): string => {
if (fromFormat === toFormat || !text.trim()) {
return text
}
try {
let parsed: any
if (fromFormat === 'yaml') {
parsed = yamlParse(text)
} else {
parsed = JSON.parse(text)
}
if (toFormat === 'yaml') {
return yamlDump(parsed, {
indent: 2,
lineWidth: -1,
noRefs: true,
})
}
return JSON.stringify(parsed, null, 2)
} catch (error) {
logger.warn(`Failed to convert from ${fromFormat} to ${toFormat}:`, error)
return text // Return original if conversion fails
}
},
[]
)
// Handle content changes
const handleContentChange = useCallback(
(newContent: string) => {
setContent(newContent)
setHasUnsavedChanges(newContent !== initialValue)
// Validate on change
const errors = validateContent(newContent, currentFormat)
setValidationErrors(errors)
// Clear save result when editing
setSaveResult(null)
},
[initialValue, currentFormat, validateContent]
)
// Handle format changes
const handleFormatChange = useCallback(
(newFormat: EditorFormat) => {
if (newFormat === currentFormat) return
// Convert content to new format
const convertedContent = convertFormat(content, currentFormat, newFormat)
setCurrentFormat(newFormat)
setContent(convertedContent)
// Validate converted content
const errors = validateContent(convertedContent, newFormat)
setValidationErrors(errors)
// Notify parent
onFormatChange?.(newFormat)
},
[content, currentFormat, convertFormat, validateContent, onFormatChange]
)
// Handle save
const handleSave = useCallback(async () => {
if (validationErrors.length > 0) {
logger.warn('Cannot save with validation errors')
return
}
setIsSaving(true)
setSaveResult(null)
try {
const result = await onSave(content, currentFormat)
setSaveResult(result)
if (result.success) {
setHasUnsavedChanges(false)
logger.info('Workflow successfully updated from text editor')
} else {
logger.error('Failed to save workflow:', result.errors)
}
} catch (error) {
logger.error('Save failed with exception:', error)
setSaveResult({
success: false,
errors: [error instanceof Error ? error.message : 'Unknown error'],
})
} finally {
setIsSaving(false)
}
}, [content, currentFormat, validationErrors, onSave])
// Update content when initialValue changes
useEffect(() => {
setContent(initialValue)
setHasUnsavedChanges(false)
setSaveResult(null)
}, [initialValue])
// Validation status
const isValid = validationErrors.length === 0
const canSave = isValid && hasUnsavedChanges && !disabled
// Get editor language for syntax highlighting
const editorLanguage = currentFormat === 'yaml' ? 'javascript' : 'json' // yaml highlighting not available, use js
return (
<div className={cn('flex h-full flex-col bg-background', className)}>
{/* Header with controls */}
<div className='flex-shrink-0 border-b bg-background px-6 py-4'>
<div className='mb-3 flex items-center justify-between'>
<div className='flex items-center gap-2'>
<FileCode className='h-5 w-5' />
<span className='font-semibold'>Workflow Text Editor</span>
</div>
<div className='flex items-center gap-2'>
<Tabs
value={currentFormat}
onValueChange={(value) => handleFormatChange(value as EditorFormat)}
>
<TabsList className='grid w-fit grid-cols-2'>
<TabsTrigger value='yaml' disabled={disabled}>
YAML
</TabsTrigger>
<TabsTrigger value='json' disabled={disabled}>
JSON
</TabsTrigger>
</TabsList>
</Tabs>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={handleSave}
disabled={!canSave || isSaving}
size='sm'
className='flex items-center gap-2'
>
<Save className='h-4 w-4' />
{isSaving ? 'Saving...' : 'Save'}
</Button>
</TooltipTrigger>
<TooltipContent>
{!isValid
? 'Fix validation errors to save'
: !hasUnsavedChanges
? 'No changes to save'
: disabled
? 'Editor is disabled'
: 'Save changes to workflow'}
</TooltipContent>
</Tooltip>
</div>
</div>
{/* Status indicators */}
<div className='flex items-center gap-2 text-sm'>
{isValid ? (
<div className='flex items-center gap-1 text-green-600'>
<Check className='h-4 w-4' />
Valid {currentFormat.toUpperCase()}
</div>
) : (
<div className='flex items-center gap-1 text-red-600'>
<AlertCircle className='h-4 w-4' />
{validationErrors.length} validation error{validationErrors.length !== 1 ? 's' : ''}
</div>
)}
{hasUnsavedChanges && <div className='text-orange-600'> Unsaved changes</div>}
</div>
</div>
{/* Alerts section - fixed height, scrollable if needed */}
{(validationErrors.length > 0 || saveResult) && (
<div className='scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent max-h-32 flex-shrink-0 overflow-y-auto border-b bg-muted/20'>
<div className='space-y-2 p-4'>
{/* Validation errors */}
{validationErrors.length > 0 && (
<>
{validationErrors.map((error, index) => (
<Alert key={index} variant='destructive' className='py-2'>
<AlertCircle className='h-4 w-4' />
<AlertDescription className='text-sm'>
{error.line && error.column
? `Line ${error.line}, Column ${error.column}: ${error.message}`
: error.message}
</AlertDescription>
</Alert>
))}
</>
)}
{/* Save result */}
{saveResult && (
<Alert variant={saveResult.success ? 'default' : 'destructive'} className='py-2'>
{saveResult.success ? (
<Check className='h-4 w-4' />
) : (
<AlertCircle className='h-4 w-4' />
)}
<AlertDescription className='text-sm'>
{saveResult.success ? (
<>
Workflow updated successfully!
{saveResult.warnings && saveResult.warnings.length > 0 && (
<div className='mt-2'>
<strong>Warnings:</strong>
<ul className='mt-1 list-inside list-disc text-xs'>
{saveResult.warnings.map((warning, index) => (
<li key={index}>{warning}</li>
))}
</ul>
</div>
)}
</>
) : (
<>
Failed to update workflow:
{saveResult.errors && (
<ul className='mt-1 list-inside list-disc text-xs'>
{saveResult.errors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
)}
</>
)}
</AlertDescription>
</Alert>
)}
</div>
</div>
)}
{/* Code editor - takes remaining space */}
<div className='min-h-0 flex-1 overflow-hidden'>
<div className='h-full p-4'>
<CodeEditor
value={content}
onChange={handleContentChange}
language={editorLanguage}
placeholder={`Enter ${currentFormat.toUpperCase()} workflow definition...`}
className={cn(
'h-full w-full overflow-auto rounded-md border',
!isValid && 'border-red-500',
hasUnsavedChanges && 'border-orange-500'
)}
minHeight='calc(100vh - 300px)'
disabled={disabled}
/>
</div>
</div>
</div>
)
}