Skip to content
Merged
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
103 changes: 103 additions & 0 deletions apps/sim/app/api/copilot/training/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'

const logger = createLogger('CopilotTrainingAPI')

// Schema for the request body
const TrainingDataSchema = z.object({
title: z.string().min(1),
prompt: z.string().min(1),
input: z.any(), // Workflow state (start)
output: z.any(), // Workflow state (end)
operations: z.any(),
})

export async function POST(request: NextRequest) {
try {
// Check for required environment variables
const baseUrl = env.AGENT_INDEXER_URL
if (!baseUrl) {
logger.error('Missing AGENT_INDEXER_URL environment variable')
return NextResponse.json({ error: 'Agent indexer not configured' }, { status: 500 })
}

const apiKey = env.AGENT_INDEXER_API_KEY
if (!apiKey) {
logger.error('Missing AGENT_INDEXER_API_KEY environment variable')
return NextResponse.json(
{ error: 'Agent indexer authentication not configured' },
{ status: 500 }
)
}

// Parse and validate request body
const body = await request.json()
const validationResult = TrainingDataSchema.safeParse(body)

if (!validationResult.success) {
logger.warn('Invalid training data format', { errors: validationResult.error.errors })
return NextResponse.json(
{
error: 'Invalid training data format',
details: validationResult.error.errors,
},
{ status: 400 }
)
}

const { title, prompt, input, output, operations } = validationResult.data

logger.info('Sending training data to agent indexer', {
title,
operationsCount: operations.length,
})

const wrappedOperations = {
operations: operations,
}

// Forward to agent indexer
const upstreamUrl = `${baseUrl}/operations/add`
const upstreamResponse = await fetch(upstreamUrl, {
method: 'POST',
headers: {
'x-api-key': apiKey,
'content-type': 'application/json',
},
body: JSON.stringify({
title,
prompt,
input,
output,
operations: wrappedOperations,
}),
})

const responseData = await upstreamResponse.json()

if (!upstreamResponse.ok) {
logger.error('Agent indexer rejected the data', {
status: upstreamResponse.status,
response: responseData,
})
return NextResponse.json(responseData, { status: upstreamResponse.status })
}

logger.info('Successfully sent training data to agent indexer', {
title,
response: responseData,
})

return NextResponse.json(responseData)
} catch (error) {
logger.error('Failed to send training data to agent indexer', { error })
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Failed to send training data',
},
{ status: 502 }
)
}
}
4 changes: 4 additions & 0 deletions apps/sim/app/api/users/me/settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const SettingsSchema = z.object({
})
.optional(),
billingUsageNotificationsEnabled: z.boolean().optional(),
showFloatingControls: z.boolean().optional(),
showTrainingControls: z.boolean().optional(),
})

// Default settings values
Expand All @@ -38,6 +40,8 @@ const defaultSettings = {
telemetryEnabled: true,
emailPreferences: {},
billingUsageNotificationsEnabled: true,
showFloatingControls: true,
showTrainingControls: false,
}

export async function GET() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client'

import { useEffect, useState } from 'react'
import { getEnv, isTruthy } from '@/lib/env'
import { useCopilotTrainingStore } from '@/stores/copilot-training/store'
import { useGeneralStore } from '@/stores/settings/general/store'
import { TrainingFloatingButton } from './training-floating-button'
import { TrainingModal } from './training-modal'

/**
* Main training controls component that manages the training UI
* Only renders if COPILOT_TRAINING_ENABLED env var is set AND user has enabled it in settings
*/
export function TrainingControls() {
const [isEnvEnabled, setIsEnvEnabled] = useState(false)
const showTrainingControls = useGeneralStore((state) => state.showTrainingControls)
const { isTraining, showModal, toggleModal } = useCopilotTrainingStore()

// Check environment variable on mount
useEffect(() => {
// Use getEnv to check if training is enabled
const trainingEnabled = isTruthy(getEnv('NEXT_PUBLIC_COPILOT_TRAINING_ENABLED'))
setIsEnvEnabled(trainingEnabled)
}, [])

// Don't render if not enabled by env var OR user settings
if (!isEnvEnabled || !showTrainingControls) {
return null
}

return (
<>
{/* Floating button to start/stop training */}
<TrainingFloatingButton isTraining={isTraining} onToggleModal={toggleModal} />

{/* Modal for entering prompt and viewing dataset */}
{showModal && <TrainingModal />}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client'

import { Database, Pause } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { useCopilotTrainingStore } from '@/stores/copilot-training/store'

interface TrainingFloatingButtonProps {
isTraining: boolean
onToggleModal: () => void
}

/**
* Floating button positioned above the diff controls
* Shows training state and allows starting/stopping training
*/
export function TrainingFloatingButton({ isTraining, onToggleModal }: TrainingFloatingButtonProps) {
const { stopTraining } = useCopilotTrainingStore()

const handleClick = () => {
if (isTraining) {
// Stop and save the training session
const dataset = stopTraining()
if (dataset) {
// Show a brief success indicator
const button = document.getElementById('training-button')
if (button) {
button.classList.add('animate-pulse')
setTimeout(() => button.classList.remove('animate-pulse'), 1000)
}
}
} else {
// Open modal to start new training
onToggleModal()
}
}

return (
<div className='-translate-x-1/2 fixed bottom-32 left-1/2 z-30'>
<Tooltip>
<TooltipTrigger asChild>
<Button
id='training-button'
variant='outline'
size='sm'
onClick={handleClick}
className={cn(
'flex items-center gap-2 rounded-[14px] border bg-card/95 px-3 py-2 shadow-lg backdrop-blur-sm transition-all',
'hover:bg-muted/80',
isTraining &&
'border-orange-500 bg-orange-50 dark:border-orange-400 dark:bg-orange-950/30'
)}
>
{isTraining ? (
<>
<Pause className='h-4 w-4 text-orange-600 dark:text-orange-400' />
<span className='font-medium text-orange-700 text-sm dark:text-orange-300'>
Stop Training
</span>
</>
) : (
<>
<Database className='h-4 w-4' />
<span className='font-medium text-sm'>Train Copilot</span>
</>
)}
</Button>
</TooltipTrigger>
<TooltipContent>
{isTraining
? 'Stop recording and save training dataset'
: 'Start recording workflow changes for training'}
</TooltipContent>
</Tooltip>
</div>
)
}
Loading