Skip to content

Commit 242710f

Browse files
committed
add wand to generate diff
1 parent 43f7f3b commit 242710f

File tree

3 files changed

+466
-13
lines changed

3 files changed

+466
-13
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import {
1010
ModalHeader,
1111
Textarea,
1212
} from '@/components/emcn'
13-
import { useUpdateDeploymentVersion } from '@/hooks/queries/deployments'
13+
import {
14+
useGenerateVersionDescription,
15+
useUpdateDeploymentVersion,
16+
} from '@/hooks/queries/deployments'
1417

1518
interface VersionDescriptionModalProps {
1619
open: boolean
@@ -29,14 +32,15 @@ export function VersionDescriptionModal({
2932
versionName,
3033
currentDescription,
3134
}: VersionDescriptionModalProps) {
32-
// Initialize state from props - component remounts via key prop when version changes
3335
const initialDescription = currentDescription || ''
3436
const [description, setDescription] = useState(initialDescription)
3537
const [showUnsavedChangesAlert, setShowUnsavedChangesAlert] = useState(false)
3638

3739
const updateMutation = useUpdateDeploymentVersion()
40+
const generateMutation = useGenerateVersionDescription()
3841

3942
const hasChanges = description.trim() !== initialDescription.trim()
43+
const isGenerating = generateMutation.isPending
4044

4145
const handleCloseAttempt = useCallback(() => {
4246
if (hasChanges && !updateMutation.isPending) {
@@ -52,6 +56,16 @@ export function VersionDescriptionModal({
5256
onOpenChange(false)
5357
}, [initialDescription, onOpenChange])
5458

59+
const handleGenerateDescription = useCallback(() => {
60+
generateMutation.mutate({
61+
workflowId,
62+
version,
63+
onStreamChunk: (accumulated) => {
64+
setDescription(accumulated)
65+
},
66+
})
67+
}, [workflowId, version, generateMutation])
68+
5569
const handleSave = useCallback(async () => {
5670
if (!workflowId) return
5771

@@ -76,41 +90,51 @@ export function VersionDescriptionModal({
7690
<ModalHeader>
7791
<span>Version Description</span>
7892
</ModalHeader>
79-
<ModalBody className='space-y-[12px]'>
80-
<p className='text-[12px] text-[var(--text-secondary)]'>
81-
{currentDescription ? 'Edit the' : 'Add a'} description for{' '}
82-
<span className='font-medium text-[var(--text-primary)]'>{versionName}</span>
83-
</p>
93+
<ModalBody className='space-y-[10px]'>
94+
<div className='flex items-center justify-between'>
95+
<p className='text-[12px] text-[var(--text-secondary)]'>
96+
{currentDescription ? 'Edit the' : 'Add a'} description for{' '}
97+
<span className='font-medium text-[var(--text-primary)]'>{versionName}</span>
98+
</p>
99+
<Button
100+
variant='active'
101+
className='-my-1 h-5 px-2 py-0 text-[11px]'
102+
onClick={handleGenerateDescription}
103+
disabled={isGenerating || updateMutation.isPending}
104+
>
105+
{isGenerating ? 'Generating...' : 'Generate'}
106+
</Button>
107+
</div>
84108
<Textarea
85109
placeholder='Describe the changes in this deployment version...'
86110
className='min-h-[120px] resize-none'
87111
value={description}
88112
onChange={(e) => setDescription(e.target.value)}
89113
maxLength={500}
114+
disabled={isGenerating}
90115
/>
91116
<div className='flex items-center justify-between'>
92-
{updateMutation.error ? (
117+
{(updateMutation.error || generateMutation.error) && (
93118
<p className='text-[12px] text-[var(--text-error)]'>
94-
{updateMutation.error.message}
119+
{updateMutation.error?.message || generateMutation.error?.message}
95120
</p>
96-
) : (
97-
<div />
98121
)}
122+
{!updateMutation.error && !generateMutation.error && <div />}
99123
<p className='text-[11px] text-[var(--text-tertiary)]'>{description.length}/500</p>
100124
</div>
101125
</ModalBody>
102126
<ModalFooter>
103127
<Button
104128
variant='default'
105129
onClick={handleCloseAttempt}
106-
disabled={updateMutation.isPending}
130+
disabled={updateMutation.isPending || isGenerating}
107131
>
108132
Cancel
109133
</Button>
110134
<Button
111135
variant='tertiary'
112136
onClick={handleSave}
113-
disabled={updateMutation.isPending || !hasChanges}
137+
disabled={updateMutation.isPending || isGenerating || !hasChanges}
114138
>
115139
{updateMutation.isPending ? 'Saving...' : 'Save'}
116140
</Button>

apps/sim/hooks/queries/deployments.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,147 @@ export function useUpdateDeploymentVersion() {
411411
})
412412
}
413413

414+
/**
415+
* Variables for generating a version description
416+
*/
417+
interface GenerateVersionDescriptionVariables {
418+
workflowId: string
419+
version: number
420+
onStreamChunk?: (accumulated: string) => void
421+
}
422+
423+
const VERSION_DESCRIPTION_SYSTEM_PROMPT = `You are a technical writer generating concise deployment version descriptions.
424+
425+
Given a diff of changes between two workflow versions, write a brief, factual description (1-2 sentences, under 300 characters) that states ONLY what changed.
426+
427+
RULES:
428+
- State specific values when provided (e.g. "model changed from X to Y")
429+
- Do NOT wrap your response in quotes
430+
- Do NOT add filler phrases like "streamlining the workflow", "for improved efficiency"
431+
- Do NOT use markdown formatting
432+
- Do NOT include version numbers
433+
- Do NOT start with "This version" or similar phrases
434+
435+
Good examples:
436+
- Changes model in Agent 1 from gpt-4o to claude-sonnet-4-20250514.
437+
- Adds Slack notification block. Updates webhook URL to production endpoint.
438+
- Removes Function block and its connection to Router.
439+
440+
Bad examples:
441+
- "Changes model..." (NO - don't wrap in quotes)
442+
- Changes model, streamlining the workflow. (NO - don't add filler)
443+
444+
Respond with ONLY the plain text description.`
445+
446+
/**
447+
* Hook for generating a version description using AI based on workflow diff
448+
*/
449+
export function useGenerateVersionDescription() {
450+
return useMutation({
451+
mutationFn: async ({
452+
workflowId,
453+
version,
454+
onStreamChunk,
455+
}: GenerateVersionDescriptionVariables): Promise<string> => {
456+
const { generateWorkflowDiffSummary, formatDiffSummaryForDescription } = await import(
457+
'@/lib/workflows/comparison/compare'
458+
)
459+
460+
const currentResponse = await fetch(`/api/workflows/${workflowId}/deployments/${version}`)
461+
if (!currentResponse.ok) {
462+
throw new Error('Failed to fetch current version state')
463+
}
464+
const currentData = await currentResponse.json()
465+
const currentState = currentData.deployedState
466+
467+
let previousState = null
468+
if (version > 1) {
469+
const previousResponse = await fetch(
470+
`/api/workflows/${workflowId}/deployments/${version - 1}`
471+
)
472+
if (previousResponse.ok) {
473+
const previousData = await previousResponse.json()
474+
previousState = previousData.deployedState
475+
}
476+
}
477+
478+
const diffSummary = generateWorkflowDiffSummary(currentState, previousState)
479+
const diffText = formatDiffSummaryForDescription(diffSummary)
480+
481+
const wandResponse = await fetch('/api/wand', {
482+
method: 'POST',
483+
headers: {
484+
'Content-Type': 'application/json',
485+
'Cache-Control': 'no-cache, no-transform',
486+
},
487+
body: JSON.stringify({
488+
prompt: `Generate a deployment version description based on these changes:\n\n${diffText}`,
489+
systemPrompt: VERSION_DESCRIPTION_SYSTEM_PROMPT,
490+
stream: true,
491+
workflowId,
492+
}),
493+
cache: 'no-store',
494+
})
495+
496+
if (!wandResponse.ok) {
497+
const errorText = await wandResponse.text()
498+
throw new Error(errorText || 'Failed to generate description')
499+
}
500+
501+
if (!wandResponse.body) {
502+
throw new Error('Response body is null')
503+
}
504+
505+
const reader = wandResponse.body.getReader()
506+
const decoder = new TextDecoder()
507+
let accumulatedContent = ''
508+
509+
try {
510+
while (true) {
511+
const { done, value } = await reader.read()
512+
if (done) break
513+
514+
const chunk = decoder.decode(value)
515+
const lines = chunk.split('\n\n')
516+
517+
for (const line of lines) {
518+
if (line.startsWith('data: ')) {
519+
const lineData = line.substring(6)
520+
if (lineData === '[DONE]') continue
521+
522+
try {
523+
const data = JSON.parse(lineData)
524+
if (data.error) throw new Error(data.error)
525+
if (data.chunk) {
526+
accumulatedContent += data.chunk
527+
onStreamChunk?.(accumulatedContent)
528+
}
529+
if (data.done) break
530+
} catch {
531+
// Skip unparseable lines
532+
}
533+
}
534+
}
535+
}
536+
} finally {
537+
reader.releaseLock()
538+
}
539+
540+
if (!accumulatedContent) {
541+
throw new Error('Failed to generate description')
542+
}
543+
544+
return accumulatedContent.trim()
545+
},
546+
onSuccess: (content) => {
547+
logger.info('Generated version description', { length: content.length })
548+
},
549+
onError: (error) => {
550+
logger.error('Failed to generate version description', { error })
551+
},
552+
})
553+
}
554+
414555
/**
415556
* Variables for activate version mutation
416557
*/

0 commit comments

Comments
 (0)