Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/sim/app/api/copilot/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,27 @@ export async function POST(req: NextRequest) {
}
}

// Detect migration requests and add specialized system prompt
const isMigrationRequest = message.toLowerCase().includes('convert this n8n workflow')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Detection string is hardcoded and brittle. If the user message format varies (e.g., "Convert this N8N workflow", "convert the n8n workflow"), detection will fail. Consider a more flexible pattern match or regex.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/sim/app/api/copilot/chat/route.ts
Line: 175:175

Comment:
[P3] Detection string is hardcoded and brittle. If the user message format varies (e.g., "Convert this N8N workflow", "convert the n8n workflow"), detection will fail. Consider a more flexible pattern match or regex.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

if (isMigrationRequest) {
try {
const { getMigrationSystemPrompt } = await import('@/lib/migration')
const migrationPrompt = getMigrationSystemPrompt()

// Add migration instructions as a high-priority context
agentContexts.unshift({
type: 'migration_instructions',
content: migrationPrompt,
})

logger.info(`[${tracker.requestId}] Migration request detected - added specialized prompt`, {
promptLength: migrationPrompt.length,
})
} catch (e) {
logger.error(`[${tracker.requestId}] Failed to add migration prompt`, e)
}
}

// Handle chat context
let currentChat: any = null
let conversationHistory: any[] = []
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './copilot-message/copilot-message'
export * from './migration-dialog'
export * from './plan-mode-section/plan-mode-section'
export * from './todo-list/todo-list'
export * from './tool-call/tool-call'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
'use client'

import { useState } from 'react'
import { AlertCircle, FileUp, Upload } from 'lucide-react'
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'

interface MigrationDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onSubmitToChat: (jsonContent: string) => void
}

export function MigrationDialog({ open, onOpenChange, onSubmitToChat }: MigrationDialogProps) {
const [workflowJson, setWorkflowJson] = useState('')
const [validationError, setValidationError] = useState<string>('')

const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return

const reader = new FileReader()
reader.onload = (event) => {
const content = event.target?.result as string
setWorkflowJson(content)
setValidationError('')

// Validate it's valid JSON
try {
JSON.parse(content)
} catch (error) {
setValidationError('Invalid JSON format')
}
}
reader.onerror = () => {
setValidationError('Failed to read file. Please try again.')
}
reader.readAsText(file)
}

const handleSubmit = async () => {
if (!workflowJson.trim()) {
setValidationError('Please upload or paste a workflow JSON')
return
}

// Validate JSON
try {
const parsed = JSON.parse(workflowJson)

// Basic n8n workflow validation
if (!parsed.nodes || !Array.isArray(parsed.nodes)) {
setValidationError('Invalid n8n workflow: missing "nodes" array')
return
}

if (parsed.nodes.length === 0) {
setValidationError('Invalid n8n workflow: must contain at least one node')
return
}

// Use the simplified formatter
const { formatMigrationRequest } = await import('@/lib/migration')
const message = await formatMigrationRequest(workflowJson)

onSubmitToChat(message)

// Reset and close
setWorkflowJson('')
setValidationError('')
onOpenChange(false)
} catch (error) {
setValidationError(error instanceof Error ? error.message : 'Invalid JSON format')
}
}

const handleClose = () => {
setWorkflowJson('')
setValidationError('')
onOpenChange(false)
}

return (
<Modal open={open} onOpenChange={onOpenChange}>
<ModalContent className='max-w-3xl max-h-[90vh] overflow-hidden flex flex-col'>
<ModalHeader>
<div className='flex items-center gap-2'>
<span>Migrate from n8n</span>
<span className='text-xs bg-[var(--surface-3)] text-[var(--text-secondary)] px-2 py-0.5 rounded-full font-medium'>
BETA
</span>
</div>
</ModalHeader>

<ModalBody className='flex-1 overflow-y-auto'>
<div className='space-y-4'>
{/* Info Banner */}
<div className='rounded-md bg-[var(--surface-2)] border border-[var(--border)] p-3'>
<div className='flex items-start gap-2'>
<Upload className='h-4 w-4 text-[var(--text-secondary)] mt-0.5 flex-shrink-0' />
<div className='flex-1 text-sm text-[var(--text-primary)]'>
<p className='font-medium mb-1'>Upload n8n Workflow</p>
<p className='text-xs text-[var(--text-secondary)]'>
Upload or paste your n8n workflow JSON. The workflow will be submitted to Copilot for intelligent conversion to Sim blocks.
</p>
</div>
</div>
</div>

{/* Upload Section */}
<div>
<label className='block text-sm font-medium text-[var(--text-primary)] mb-2'>
Upload n8n Workflow JSON
</label>
<div className='flex gap-2'>
<label className='flex-1 flex items-center justify-center h-32 px-4 border-2 border-[var(--border)] border-dashed rounded-lg cursor-pointer hover:border-[var(--border-hover)] transition-colors bg-[var(--surface-1)]'>
<div className='flex flex-col items-center'>
<FileUp className='w-8 h-8 text-[var(--text-tertiary)] mb-2' />
<span className='text-sm text-[var(--text-secondary)]'>
Click to upload or drag & drop
</span>
<span className='text-xs text-[var(--text-tertiary)] mt-1'>JSON files only</span>
</div>
<input
type='file'
className='hidden'
accept='.json,application/json'
onChange={handleFileUpload}
/>
</label>
</div>
</div>

{/* JSON Input */}
<div>
<label className='block text-sm font-medium text-[var(--text-primary)] mb-2'>
Or Paste n8n Workflow JSON
</label>
<textarea
className='w-full h-64 px-3 py-2 text-sm font-mono border border-[var(--border)] rounded-md focus:outline-none focus:ring-2 focus:ring-[var(--accent)] bg-[var(--surface-1)] text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)]'
placeholder='Paste your n8n workflow JSON here...'
value={workflowJson}
onChange={(e) => {
setWorkflowJson(e.target.value)
setValidationError('')
}}
/>
</div>

{/* Validation Error */}
{validationError && (
<div className='rounded-md bg-red-50 border border-red-200 p-3'>
<div className='flex items-start gap-2'>
<AlertCircle className='h-4 w-4 text-red-600 mt-0.5 flex-shrink-0' />
<div className='flex-1'>
<p className='text-sm font-medium text-red-900'>Validation Error</p>
<p className='text-sm text-red-700 mt-1'>{validationError}</p>
</div>
</div>
</div>
)}
</div>
</ModalBody>

<ModalFooter>
<Button variant='ghost' onClick={handleClose}>
Cancel
</Button>

<Button
onClick={handleSubmit}
disabled={!workflowJson.trim()}
variant='tertiary'
>
<Upload className='mr-2 h-4 w-4' />
Submit to Copilot
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ interface WelcomeProps {
onQuestionClick?: (question: string) => void
/** Current copilot mode ('ask' for Q&A, 'plan' for planning, 'build' for workflow building) */
mode?: 'ask' | 'build' | 'plan'
/** Callback when migrate button is clicked */
onMigrateClick?: () => void
}

/**
Expand All @@ -19,7 +21,7 @@ interface WelcomeProps {
* @param props - Component props
* @returns Welcome screen UI
*/
export function Welcome({ onQuestionClick, mode = 'ask' }: WelcomeProps) {
export function Welcome({ onQuestionClick, mode = 'ask', onMigrateClick }: WelcomeProps) {
const capabilities =
mode === 'build'
? [
Expand Down Expand Up @@ -68,6 +70,25 @@ export function Welcome({ onQuestionClick, mode = 'ask' }: WelcomeProps) {
</div>
</Button>
))}

{/* Migrate from n8n button - only show in build mode */}
{mode === 'build' && (
<Button
variant='active'
onClick={onMigrateClick}
className='w-full justify-start'
>
<div className='flex flex-col items-start'>
<p className='font-medium flex items-center gap-2'>
Migrate From N8n
<span className='text-[10px] bg-[var(--surface-3)] text-[var(--text-secondary)] px-1.5 py-0.5 rounded-full font-medium'>
BETA
</span>
</p>
<p className='text-[var(--text-secondary)]'>Convert n8n workflows using AI</p>
</div>
</Button>
)}
</div>

{/* Tips */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { Trash } from '@/components/emcn/icons/trash'
import {
CopilotMessage,
MigrationDialog,
PlanModeSection,
TodoList,
UserInput,
Expand Down Expand Up @@ -75,6 +76,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
const [isEditingMessage, setIsEditingMessage] = useState(false)
const [revertingMessageId, setRevertingMessageId] = useState<string | null>(null)
const [isHistoryDropdownOpen, setIsHistoryDropdownOpen] = useState(false)
const [isMigrationDialogOpen, setIsMigrationDialogOpen] = useState(false)

const { activeWorkflowId } = useWorkflowRegistry()

Expand Down Expand Up @@ -499,7 +501,11 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
/>
</div>
<div className='flex-shrink-0 pt-[8px]'>
<Welcome onQuestionClick={handleSubmit} mode={mode} />
<Welcome
onQuestionClick={handleSubmit}
mode={mode}
onMigrateClick={() => setIsMigrationDialogOpen(true)}
/>
</div>
</div>
) : (
Expand Down Expand Up @@ -610,6 +616,14 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
</>
)}
</div>

<MigrationDialog
open={isMigrationDialogOpen}
onOpenChange={setIsMigrationDialogOpen}
onSubmitToChat={(jsonContent) => {
handleSubmit(jsonContent)
}}
/>
</>
)
})
Expand Down
35 changes: 35 additions & 0 deletions apps/sim/lib/migration/format-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use server'

/**
* Format n8n workflow for Copilot migration
* Provides workflow summary and clear instructions
*/
export async function formatMigrationRequest(n8nWorkflowJson: string): Promise<string> {
try {
// Validate JSON
const parsed = JSON.parse(n8nWorkflowJson)

// Extract workflow info for better context
const nodeCount = parsed.nodes?.length || 0
const nodeTypes = parsed.nodes?.map((n: any) => n.type).filter(Boolean).slice(0, 5).join(', ') || 'unknown'
const workflowName = parsed.name || 'Unnamed workflow'

// Concise, action-focused message
return `Convert this n8n workflow (${nodeCount} nodes) to Sim blocks.

Workflow: "${workflowName}"
Nodes: ${nodeTypes}${nodeCount > 5 ? ', ...' : ''}

Create ALL ${nodeCount} blocks in ONE edit_workflow call, then autolayout.

N8n JSON:
\`\`\`json
${n8nWorkflowJson}
\`\`\`

Create all blocks using edit_workflow with 'add' operations.`

} catch (error) {
throw new Error(`Invalid n8n workflow JSON: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
6 changes: 6 additions & 0 deletions apps/sim/lib/migration/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Migration utilities for converting n8n workflows to Sim blocks
*/

export { formatMigrationRequest } from './format-request'
export { getMigrationSystemPrompt } from './prompts'
28 changes: 28 additions & 0 deletions apps/sim/lib/migration/prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Generate optimized migration system prompt for Copilot
* Minimal, context-aware prompt that leverages Copilot's existing knowledge
*/
export function getMigrationSystemPrompt(): string {
return `Convert n8n workflow to Sim efficiently: analyze the workflow, create ALL blocks at once, then autolayout.

**Block Mappings**:
- HTTP/API → "api"
- Webhook → "webhook_trigger"
- Code → "function"
- Schedule → "schedule"
- Manual → "manual_trigger"
- Variables → "variables"
- Conditions → "condition" or "router"

**Efficient Process**:
1. Analyze all n8n nodes and map to Sim block types
2. Query get_block_config for any unclear block types
3. Create ALL blocks in ONE edit_workflow call with multiple 'add' operations:
- Set triggerMode: true for first/trigger nodes
- Use rough positions (x: 100, y: 100 + index*200)
- Map all parameters from n8n to Sim subBlocks
- Heights: triggers=172, functions=143, others=127
4. After all blocks created, call the autolayout tool to organize positions

**Important**: Create ALL blocks in a SINGLE edit_workflow call, not incrementally. Then autolayout for clean positioning.`
}
Loading