-
Notifications
You must be signed in to change notification settings - Fork 3.2k
feat(copilot): add training interface #1445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
b2eb692
progress
icecrasher321 9ccb55f
cleanup UI
icecrasher321 32dd871
progress
icecrasher321 ba4580f
fix trigger mode in yaml + copilot side
icecrasher321 ee9682c
persist user settings
icecrasher321 b07e4c2
wrap operations correctly
icecrasher321 23857b6
add trigger mode to add op
icecrasher321 7a28a00
remove misplaced comment
icecrasher321 178ea39
add sent notification
icecrasher321 0393f30
remove unused tab:
icecrasher321 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 } | ||
| ) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
...workspace/[workspaceId]/w/[workflowId]/components/training-controls/training-controls.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 />} | ||
| </> | ||
| ) | ||
| } |
78 changes: 78 additions & 0 deletions
78
...ce/[workspaceId]/w/[workflowId]/components/training-controls/training-floating-button.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.