mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 22:48:14 -05:00
feat(ui): added component playground & fixed training modal (#2354)
This commit is contained in:
566
apps/sim/app/playground/page.tsx
Normal file
566
apps/sim/app/playground/page.tsx
Normal file
@@ -0,0 +1,566 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ArrowLeft, Bell, Folder, Key, Settings, User } from 'lucide-react'
|
||||
import { notFound, useRouter } from 'next/navigation'
|
||||
import {
|
||||
Badge,
|
||||
Breadcrumb,
|
||||
BubbleChatPreview,
|
||||
Button,
|
||||
Card as CardIcon,
|
||||
ChevronDown,
|
||||
Code,
|
||||
Combobox,
|
||||
Connections,
|
||||
Copy,
|
||||
DocumentAttachment,
|
||||
Duplicate,
|
||||
Eye,
|
||||
FolderCode,
|
||||
FolderPlus,
|
||||
HexSimple,
|
||||
Input,
|
||||
Key as KeyIcon,
|
||||
Label,
|
||||
Layout,
|
||||
Library,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTabs,
|
||||
ModalTabsContent,
|
||||
ModalTabsList,
|
||||
ModalTabsTrigger,
|
||||
ModalTrigger,
|
||||
MoreHorizontal,
|
||||
NoWrap,
|
||||
PanelLeft,
|
||||
Play,
|
||||
Popover,
|
||||
PopoverBackButton,
|
||||
PopoverContent,
|
||||
PopoverFolder,
|
||||
PopoverItem,
|
||||
PopoverScrollArea,
|
||||
PopoverSearch,
|
||||
PopoverSection,
|
||||
PopoverTrigger,
|
||||
Redo,
|
||||
Rocket,
|
||||
SModal,
|
||||
SModalContent,
|
||||
SModalMain,
|
||||
SModalMainBody,
|
||||
SModalMainHeader,
|
||||
SModalSidebar,
|
||||
SModalSidebarHeader,
|
||||
SModalSidebarItem,
|
||||
SModalSidebarSection,
|
||||
SModalSidebarSectionTitle,
|
||||
SModalTrigger,
|
||||
Switch,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
Trash,
|
||||
Trash2,
|
||||
Undo,
|
||||
Wrap,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
} from '@/components/emcn'
|
||||
import { env, isTruthy } from '@/lib/core/config/env'
|
||||
|
||||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<section className='space-y-4'>
|
||||
<h2 className='border-[var(--border)] border-b pb-2 font-medium text-[var(--text-primary)] text-lg'>
|
||||
{title}
|
||||
</h2>
|
||||
<div className='space-y-4'>{children}</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function VariantRow({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className='flex items-center gap-4'>
|
||||
<span className='w-32 shrink-0 text-[var(--text-secondary)] text-sm'>{label}</span>
|
||||
<div className='flex flex-wrap items-center gap-2'>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SAMPLE_CODE = `function greet(name) {
|
||||
console.log("Hello, " + name);
|
||||
return { success: true };
|
||||
}`
|
||||
|
||||
const SAMPLE_PYTHON = `def greet(name):
|
||||
print(f"Hello, {name}")
|
||||
return {"success": True}`
|
||||
|
||||
const COMBOBOX_OPTIONS = [
|
||||
{ label: 'Option 1', value: 'opt1' },
|
||||
{ label: 'Option 2', value: 'opt2' },
|
||||
{ label: 'Option 3', value: 'opt3' },
|
||||
]
|
||||
|
||||
export default function PlaygroundPage() {
|
||||
const router = useRouter()
|
||||
const [comboboxValue, setComboboxValue] = useState('')
|
||||
const [switchValue, setSwitchValue] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState('profile')
|
||||
|
||||
if (!isTruthy(env.NEXT_PUBLIC_ENABLE_PLAYGROUND)) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip.Provider>
|
||||
<div className='relative min-h-screen bg-[var(--bg)] p-8'>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => router.back()}
|
||||
className='absolute top-8 left-8 h-8 w-8 p-0'
|
||||
>
|
||||
<ArrowLeft className='h-4 w-4' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>Go back</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<div className='mx-auto max-w-4xl space-y-12'>
|
||||
<div>
|
||||
<h1 className='font-semibold text-2xl text-[var(--text-primary)]'>
|
||||
EMCN Component Playground
|
||||
</h1>
|
||||
<p className='mt-2 text-[var(--text-secondary)]'>
|
||||
All emcn UI components and their variants
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Button */}
|
||||
<Section title='Button'>
|
||||
<VariantRow label='default'>
|
||||
<Button variant='default'>Default</Button>
|
||||
</VariantRow>
|
||||
<VariantRow label='active'>
|
||||
<Button variant='active'>Active</Button>
|
||||
</VariantRow>
|
||||
<VariantRow label='3d'>
|
||||
<Button variant='3d'>3D</Button>
|
||||
</VariantRow>
|
||||
<VariantRow label='outline'>
|
||||
<Button variant='outline'>Outline</Button>
|
||||
</VariantRow>
|
||||
<VariantRow label='primary'>
|
||||
<Button variant='primary'>Primary</Button>
|
||||
</VariantRow>
|
||||
<VariantRow label='secondary'>
|
||||
<Button variant='secondary'>Secondary</Button>
|
||||
</VariantRow>
|
||||
<VariantRow label='tertiary'>
|
||||
<Button variant='tertiary'>Tertiary</Button>
|
||||
</VariantRow>
|
||||
<VariantRow label='ghost'>
|
||||
<Button variant='ghost'>Ghost</Button>
|
||||
</VariantRow>
|
||||
<VariantRow label='ghost-secondary'>
|
||||
<Button variant='ghost-secondary'>Ghost Secondary</Button>
|
||||
</VariantRow>
|
||||
<VariantRow label='disabled'>
|
||||
<Button disabled>Disabled</Button>
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Badge */}
|
||||
<Section title='Badge'>
|
||||
<VariantRow label='default'>
|
||||
<Badge variant='default'>Default</Badge>
|
||||
</VariantRow>
|
||||
<VariantRow label='outline'>
|
||||
<Badge variant='outline'>Outline</Badge>
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Input */}
|
||||
<Section title='Input'>
|
||||
<VariantRow label='default'>
|
||||
<Input placeholder='Enter text...' className='max-w-xs' />
|
||||
</VariantRow>
|
||||
<VariantRow label='disabled'>
|
||||
<Input placeholder='Disabled' disabled className='max-w-xs' />
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Textarea */}
|
||||
<Section title='Textarea'>
|
||||
<Textarea placeholder='Enter your message...' className='max-w-md' rows={4} />
|
||||
</Section>
|
||||
|
||||
{/* Label */}
|
||||
<Section title='Label'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Label htmlFor='demo-input'>Label Text</Label>
|
||||
<Input id='demo-input' placeholder='Input with label' className='max-w-xs' />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Switch */}
|
||||
<Section title='Switch'>
|
||||
<VariantRow label='default'>
|
||||
<Switch checked={switchValue} onCheckedChange={setSwitchValue} />
|
||||
<span className='text-[var(--text-secondary)] text-sm'>
|
||||
{switchValue ? 'On' : 'Off'}
|
||||
</span>
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Combobox */}
|
||||
<Section title='Combobox'>
|
||||
<VariantRow label='default'>
|
||||
<div className='w-48'>
|
||||
<Combobox
|
||||
options={COMBOBOX_OPTIONS}
|
||||
value={comboboxValue}
|
||||
onChange={setComboboxValue}
|
||||
placeholder='Select option...'
|
||||
/>
|
||||
</div>
|
||||
</VariantRow>
|
||||
<VariantRow label='size sm'>
|
||||
<div className='w-48'>
|
||||
<Combobox
|
||||
options={COMBOBOX_OPTIONS}
|
||||
value=''
|
||||
onChange={() => {}}
|
||||
placeholder='Small size'
|
||||
size='sm'
|
||||
/>
|
||||
</div>
|
||||
</VariantRow>
|
||||
<VariantRow label='searchable'>
|
||||
<div className='w-48'>
|
||||
<Combobox
|
||||
options={COMBOBOX_OPTIONS}
|
||||
value=''
|
||||
onChange={() => {}}
|
||||
placeholder='With search'
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
</VariantRow>
|
||||
<VariantRow label='editable'>
|
||||
<div className='w-48'>
|
||||
<Combobox
|
||||
options={COMBOBOX_OPTIONS}
|
||||
value=''
|
||||
onChange={() => {}}
|
||||
placeholder='Type or select...'
|
||||
editable
|
||||
/>
|
||||
</div>
|
||||
</VariantRow>
|
||||
<VariantRow label='multiSelect'>
|
||||
<div className='w-48'>
|
||||
<Combobox
|
||||
options={COMBOBOX_OPTIONS}
|
||||
multiSelectValues={[]}
|
||||
onMultiSelectChange={() => {}}
|
||||
placeholder='Select multiple...'
|
||||
multiSelect
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<Section title='Breadcrumb'>
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{ label: 'Home', href: '#' },
|
||||
{ label: 'Settings', href: '#' },
|
||||
{ label: 'Profile' },
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* Tooltip */}
|
||||
<Section title='Tooltip'>
|
||||
<VariantRow label='default'>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button variant='default'>Hover me</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>Tooltip content</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Popover */}
|
||||
<Section title='Popover'>
|
||||
<VariantRow label='default'>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant='default'>Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverSection>Section Title</PopoverSection>
|
||||
<PopoverItem>Item 1</PopoverItem>
|
||||
<PopoverItem>Item 2</PopoverItem>
|
||||
<PopoverItem active>Active Item</PopoverItem>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</VariantRow>
|
||||
<VariantRow label='primary variant'>
|
||||
<Popover variant='primary'>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant='primary'>Primary Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverItem>Item 1</PopoverItem>
|
||||
<PopoverItem active>Active Item</PopoverItem>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</VariantRow>
|
||||
<VariantRow label='with search'>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant='default'>Searchable Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverSearch placeholder='Search items...' />
|
||||
<PopoverScrollArea className='max-h-40'>
|
||||
<PopoverItem>Apple</PopoverItem>
|
||||
<PopoverItem>Banana</PopoverItem>
|
||||
<PopoverItem>Cherry</PopoverItem>
|
||||
<PopoverItem>Date</PopoverItem>
|
||||
<PopoverItem>Elderberry</PopoverItem>
|
||||
</PopoverScrollArea>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</VariantRow>
|
||||
<VariantRow label='with folders'>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant='default'>Folder Navigation</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverBackButton />
|
||||
<PopoverItem rootOnly>Root Item</PopoverItem>
|
||||
<PopoverFolder
|
||||
id='folder1'
|
||||
title='Folder 1'
|
||||
icon={<Folder className='h-3 w-3' />}
|
||||
>
|
||||
<PopoverItem>Nested Item 1</PopoverItem>
|
||||
<PopoverItem>Nested Item 2</PopoverItem>
|
||||
</PopoverFolder>
|
||||
<PopoverFolder
|
||||
id='folder2'
|
||||
title='Folder 2'
|
||||
icon={<Folder className='h-3 w-3' />}
|
||||
>
|
||||
<PopoverItem>Another Nested Item</PopoverItem>
|
||||
</PopoverFolder>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Modal */}
|
||||
<Section title='Modal'>
|
||||
<VariantRow label='sizes'>
|
||||
{(['sm', 'md', 'lg', 'xl', 'full'] as const).map((size) => (
|
||||
<Modal key={size}>
|
||||
<ModalTrigger asChild>
|
||||
<Button variant='default'>{size}</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent size={size}>
|
||||
<ModalHeader>Modal {size.toUpperCase()}</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[var(--text-secondary)]'>This is a {size} sized modal.</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant='ghost'>Cancel</Button>
|
||||
<Button variant='primary'>Save</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
))}
|
||||
</VariantRow>
|
||||
<VariantRow label='with tabs'>
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<Button variant='default'>Modal with Tabs</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent>
|
||||
<ModalHeader>Settings</ModalHeader>
|
||||
<ModalTabs defaultValue='tab1'>
|
||||
<ModalTabsList>
|
||||
<ModalTabsTrigger value='tab1'>General</ModalTabsTrigger>
|
||||
<ModalTabsTrigger value='tab2'>Advanced</ModalTabsTrigger>
|
||||
</ModalTabsList>
|
||||
<ModalBody>
|
||||
<ModalTabsContent value='tab1'>
|
||||
<p className='text-[var(--text-secondary)]'>General settings content</p>
|
||||
</ModalTabsContent>
|
||||
<ModalTabsContent value='tab2'>
|
||||
<p className='text-[var(--text-secondary)]'>Advanced settings content</p>
|
||||
</ModalTabsContent>
|
||||
</ModalBody>
|
||||
</ModalTabs>
|
||||
<ModalFooter>
|
||||
<Button variant='primary'>Save</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* SModal (Sidebar Modal) */}
|
||||
<Section title='SModal (Sidebar Modal)'>
|
||||
<SModal>
|
||||
<SModalTrigger asChild>
|
||||
<Button variant='default'>Open Sidebar Modal</Button>
|
||||
</SModalTrigger>
|
||||
<SModalContent>
|
||||
<SModalSidebar>
|
||||
<SModalSidebarHeader>Settings</SModalSidebarHeader>
|
||||
<SModalSidebarSection>
|
||||
<SModalSidebarSectionTitle>Account</SModalSidebarSectionTitle>
|
||||
<SModalSidebarItem
|
||||
icon={<User />}
|
||||
active={activeTab === 'profile'}
|
||||
onClick={() => setActiveTab('profile')}
|
||||
>
|
||||
Profile
|
||||
</SModalSidebarItem>
|
||||
<SModalSidebarItem
|
||||
icon={<Key />}
|
||||
active={activeTab === 'security'}
|
||||
onClick={() => setActiveTab('security')}
|
||||
>
|
||||
Security
|
||||
</SModalSidebarItem>
|
||||
</SModalSidebarSection>
|
||||
<SModalSidebarSection>
|
||||
<SModalSidebarSectionTitle>Preferences</SModalSidebarSectionTitle>
|
||||
<SModalSidebarItem
|
||||
icon={<Bell />}
|
||||
active={activeTab === 'notifications'}
|
||||
onClick={() => setActiveTab('notifications')}
|
||||
>
|
||||
Notifications
|
||||
</SModalSidebarItem>
|
||||
<SModalSidebarItem
|
||||
icon={<Settings />}
|
||||
active={activeTab === 'general'}
|
||||
onClick={() => setActiveTab('general')}
|
||||
>
|
||||
General
|
||||
</SModalSidebarItem>
|
||||
</SModalSidebarSection>
|
||||
</SModalSidebar>
|
||||
<SModalMain>
|
||||
<SModalMainHeader>
|
||||
{activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}
|
||||
</SModalMainHeader>
|
||||
<SModalMainBody>
|
||||
<p className='text-[var(--text-secondary)]'>Content for {activeTab} tab</p>
|
||||
</SModalMainBody>
|
||||
</SModalMain>
|
||||
</SModalContent>
|
||||
</SModal>
|
||||
</Section>
|
||||
|
||||
{/* Code */}
|
||||
<Section title='Code'>
|
||||
<VariantRow label='javascript'>
|
||||
<div className='w-full max-w-lg'>
|
||||
<Code.Viewer code={SAMPLE_CODE} language='javascript' showGutter />
|
||||
</div>
|
||||
</VariantRow>
|
||||
<VariantRow label='json'>
|
||||
<div className='w-full max-w-lg'>
|
||||
<Code.Viewer
|
||||
code={JSON.stringify({ name: 'Sim', version: '1.0' }, null, 2)}
|
||||
language='json'
|
||||
showGutter
|
||||
/>
|
||||
</div>
|
||||
</VariantRow>
|
||||
<VariantRow label='python'>
|
||||
<div className='w-full max-w-lg'>
|
||||
<Code.Viewer code={SAMPLE_PYTHON} language='python' showGutter />
|
||||
</div>
|
||||
</VariantRow>
|
||||
<VariantRow label='no gutter'>
|
||||
<div className='w-full max-w-lg'>
|
||||
<Code.Viewer code={SAMPLE_CODE} language='javascript' />
|
||||
</div>
|
||||
</VariantRow>
|
||||
<VariantRow label='wrap text'>
|
||||
<div className='w-full max-w-lg'>
|
||||
<Code.Viewer
|
||||
code="const longLine = 'This is a very long line that should wrap when wrapText is enabled to demonstrate the text wrapping functionality';"
|
||||
language='javascript'
|
||||
showGutter
|
||||
wrapText
|
||||
/>
|
||||
</div>
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Icons */}
|
||||
<Section title='Icons'>
|
||||
<div className='grid grid-cols-6 gap-4 sm:grid-cols-8 md:grid-cols-10'>
|
||||
{[
|
||||
{ Icon: BubbleChatPreview, name: 'BubbleChatPreview' },
|
||||
{ Icon: CardIcon, name: 'Card' },
|
||||
{ Icon: ChevronDown, name: 'ChevronDown' },
|
||||
{ Icon: Connections, name: 'Connections' },
|
||||
{ Icon: Copy, name: 'Copy' },
|
||||
{ Icon: DocumentAttachment, name: 'DocumentAttachment' },
|
||||
{ Icon: Duplicate, name: 'Duplicate' },
|
||||
{ Icon: Eye, name: 'Eye' },
|
||||
{ Icon: FolderCode, name: 'FolderCode' },
|
||||
{ Icon: FolderPlus, name: 'FolderPlus' },
|
||||
{ Icon: HexSimple, name: 'HexSimple' },
|
||||
{ Icon: KeyIcon, name: 'Key' },
|
||||
{ Icon: Layout, name: 'Layout' },
|
||||
{ Icon: Library, name: 'Library' },
|
||||
{ Icon: MoreHorizontal, name: 'MoreHorizontal' },
|
||||
{ Icon: NoWrap, name: 'NoWrap' },
|
||||
{ Icon: PanelLeft, name: 'PanelLeft' },
|
||||
{ Icon: Play, name: 'Play' },
|
||||
{ Icon: Redo, name: 'Redo' },
|
||||
{ Icon: Rocket, name: 'Rocket' },
|
||||
{ Icon: Trash, name: 'Trash' },
|
||||
{ Icon: Trash2, name: 'Trash2' },
|
||||
{ Icon: Undo, name: 'Undo' },
|
||||
{ Icon: Wrap, name: 'Wrap' },
|
||||
{ Icon: ZoomIn, name: 'ZoomIn' },
|
||||
{ Icon: ZoomOut, name: 'ZoomOut' },
|
||||
].map(({ Icon, name }) => (
|
||||
<Tooltip.Root key={name}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className='flex h-10 w-10 cursor-pointer items-center justify-center rounded-md border border-[var(--border)] bg-[var(--surface-2)] transition-colors hover:bg-[var(--surface-5)]'>
|
||||
<Icon className='h-5 w-5 text-[var(--text-secondary)]' />
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>{name}</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
)
|
||||
}
|
||||
@@ -7,7 +7,6 @@ export { Panel } from './panel/panel'
|
||||
export { SkeletonLoading } from './skeleton-loading/skeleton-loading'
|
||||
export { SubflowNodeComponent } from './subflows/subflow-node'
|
||||
export { Terminal } from './terminal/terminal'
|
||||
export { TrainingControls } from './training-controls/training-controls'
|
||||
export { WandPromptBar } from './wand-prompt-bar/wand-prompt-bar'
|
||||
export { WorkflowBlock } from './workflow-block/workflow-block'
|
||||
export { WorkflowEdge } from './workflow-edge/workflow-edge'
|
||||
|
||||
@@ -9,15 +9,19 @@ import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
Clipboard,
|
||||
Database,
|
||||
Filter,
|
||||
FilterX,
|
||||
MoreHorizontal,
|
||||
Palette,
|
||||
Pause,
|
||||
RepeatIcon,
|
||||
Search,
|
||||
SplitIcon,
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import {
|
||||
Button,
|
||||
@@ -30,6 +34,7 @@ import {
|
||||
PopoverTrigger,
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
|
||||
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
|
||||
import {
|
||||
@@ -38,6 +43,8 @@ import {
|
||||
useTerminalResize,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { useCopilotTrainingStore } from '@/stores/copilot-training/store'
|
||||
import { useGeneralStore } from '@/stores/settings/general/store'
|
||||
import type { ConsoleEntry } from '@/stores/terminal'
|
||||
import { useTerminalConsoleStore, useTerminalStore } from '@/stores/terminal'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
@@ -331,6 +338,14 @@ export function Terminal() {
|
||||
const outputSearchInputRef = useRef<HTMLInputElement>(null)
|
||||
const outputContentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Training controls state
|
||||
const [isTrainingEnvEnabled, setIsTrainingEnvEnabled] = useState(false)
|
||||
const showTrainingControls = useGeneralStore((state) => state.showTrainingControls)
|
||||
const { isTraining, toggleModal: toggleTrainingModal, stopTraining } = useCopilotTrainingStore()
|
||||
|
||||
// Playground state
|
||||
const [isPlaygroundEnabled, setIsPlaygroundEnabled] = useState(false)
|
||||
|
||||
// Terminal resize hooks
|
||||
const { handleMouseDown } = useTerminalResize()
|
||||
const { handleMouseDown: handleOutputPanelResizeMouseDown } = useOutputPanelResize()
|
||||
@@ -612,6 +627,26 @@ export function Terminal() {
|
||||
[activeWorkflowId, exportConsoleCSV]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle training button click - toggle training state or open modal
|
||||
*/
|
||||
const handleTrainingClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (isTraining) {
|
||||
stopTraining()
|
||||
} else {
|
||||
toggleTrainingModal()
|
||||
}
|
||||
},
|
||||
[isTraining, stopTraining, toggleTrainingModal]
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether training controls should be visible
|
||||
*/
|
||||
const shouldShowTrainingButton = isTrainingEnvEnabled && showTrainingControls
|
||||
|
||||
/**
|
||||
* Register global keyboard shortcuts for the terminal:
|
||||
* - Mod+D: Clear terminal console for the active workflow
|
||||
@@ -640,6 +675,14 @@ export function Terminal() {
|
||||
setHasHydrated(true)
|
||||
}, [setHasHydrated])
|
||||
|
||||
/**
|
||||
* Check environment variables on mount
|
||||
*/
|
||||
useEffect(() => {
|
||||
setIsTrainingEnvEnabled(isTruthy(getEnv('NEXT_PUBLIC_COPILOT_TRAINING_ENABLED')))
|
||||
setIsPlaygroundEnabled(isTruthy(getEnv('NEXT_PUBLIC_ENABLE_PLAYGROUND')))
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Adjust showInput when selected entry changes
|
||||
* Stay on input view if the new entry has input data
|
||||
@@ -1104,6 +1147,48 @@ export function Terminal() {
|
||||
)}
|
||||
{!selectedEntry && (
|
||||
<div className='ml-auto flex items-center gap-[8px]'>
|
||||
{isPlaygroundEnabled && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Link href='/playground'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
aria-label='Component Playground'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<Palette className='h-3 w-3' />
|
||||
</Button>
|
||||
</Link>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>Component Playground</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
{shouldShowTrainingButton && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleTrainingClick}
|
||||
aria-label={isTraining ? 'Stop training' : 'Train Copilot'}
|
||||
className={clsx(
|
||||
'!p-1.5 -m-1.5',
|
||||
isTraining && 'text-orange-600 dark:text-orange-400'
|
||||
)}
|
||||
>
|
||||
{isTraining ? (
|
||||
<Pause className='h-3 w-3' />
|
||||
) : (
|
||||
<Database className='h-3 w-3' />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>{isTraining ? 'Stop Training' : 'Train Copilot'}</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
{hasActiveFilters && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
@@ -1426,6 +1511,50 @@ export function Terminal() {
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
|
||||
{isPlaygroundEnabled && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Link href='/playground'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
aria-label='Component Playground'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<Palette className='h-[12px] w-[12px]' />
|
||||
</Button>
|
||||
</Link>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>Component Playground</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
|
||||
{shouldShowTrainingButton && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleTrainingClick}
|
||||
aria-label={isTraining ? 'Stop training' : 'Train Copilot'}
|
||||
className={clsx(
|
||||
'!p-1.5 -m-1.5',
|
||||
isTraining && 'text-orange-600 dark:text-orange-400'
|
||||
)}
|
||||
>
|
||||
{isTraining ? (
|
||||
<Pause className='h-[12px] w-[12px]' />
|
||||
) : (
|
||||
<Database className='h-[12px] w-[12px]' />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>{isTraining ? 'Stop Training' : 'Train Copilot'}</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||
import { TrainingFloatingButton } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/training-controls/training-floating-button'
|
||||
import { TrainingModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/training-controls/training-modal'
|
||||
import { useCopilotTrainingStore } from '@/stores/copilot-training/store'
|
||||
import { useGeneralStore } from '@/stores/settings/general/store'
|
||||
|
||||
/**
|
||||
* 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 />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Database, Pause } from 'lucide-react'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
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.Root>
|
||||
<Tooltip.Trigger 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>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
{isTraining
|
||||
? 'Stop recording and save training dataset'
|
||||
: 'Start recording workflow changes for training'}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,20 +13,21 @@ import {
|
||||
X,
|
||||
XCircle,
|
||||
} from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalTabs,
|
||||
ModalTabsContent,
|
||||
ModalTabsList,
|
||||
ModalTabsTrigger,
|
||||
Textarea,
|
||||
} from '@/components/emcn'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
||||
@@ -74,6 +75,7 @@ export function TrainingModal() {
|
||||
const [liveWorkflowFailed, setLiveWorkflowFailed] = useState(false)
|
||||
const [liveWorkflowTitle, setLiveWorkflowTitle] = useState('')
|
||||
const [liveWorkflowDescription, setLiveWorkflowDescription] = useState('')
|
||||
const [activeTab, setActiveTab] = useState(isTraining ? 'datasets' : 'new')
|
||||
|
||||
const handleStart = () => {
|
||||
if (localTitle.trim() && localPrompt.trim()) {
|
||||
@@ -340,438 +342,470 @@ export function TrainingModal() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={showModal} onOpenChange={toggleModal}>
|
||||
<DialogContent className='max-w-3xl'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Copilot Training Dataset Builder</DialogTitle>
|
||||
<DialogDescription>
|
||||
Record workflow editing sessions to create training datasets for the copilot
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Modal open={showModal} onOpenChange={toggleModal}>
|
||||
<ModalContent size='lg'>
|
||||
<ModalHeader>Copilot Training Dataset Builder</ModalHeader>
|
||||
|
||||
{isTraining && (
|
||||
<>
|
||||
<div className='mt-4 rounded-lg border bg-orange-50 p-4 dark:bg-orange-950/30'>
|
||||
<p className='mb-2 font-medium text-orange-700 dark:text-orange-300'>
|
||||
Recording: {currentTitle}
|
||||
</p>
|
||||
<p className='mb-3 text-sm'>{currentPrompt}</p>
|
||||
<div className='flex gap-2'>
|
||||
<Button variant='outline' size='sm' onClick={cancelTraining} className='flex-1'>
|
||||
<X className='mr-2 h-4 w-4' />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='default'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
useCopilotTrainingStore.getState().stopTraining()
|
||||
setLocalPrompt('')
|
||||
}}
|
||||
className='flex-1'
|
||||
>
|
||||
<Check className='mr-2 h-4 w-4' />
|
||||
Save Dataset
|
||||
</Button>
|
||||
<ModalTabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<ModalTabsList>
|
||||
<ModalTabsTrigger value='new' disabled={isTraining}>
|
||||
New Session
|
||||
</ModalTabsTrigger>
|
||||
<ModalTabsTrigger value='datasets'>Datasets ({datasets.length})</ModalTabsTrigger>
|
||||
<ModalTabsTrigger value='live'>Send Live State</ModalTabsTrigger>
|
||||
</ModalTabsList>
|
||||
|
||||
<ModalBody className='flex min-h-[400px] flex-col overflow-hidden'>
|
||||
{/* Recording Banner */}
|
||||
{isTraining && (
|
||||
<div className='mb-[16px] rounded-[8px] border bg-orange-50 p-[12px] dark:bg-orange-950/30'>
|
||||
<p className='mb-[8px] font-medium text-[13px] text-orange-700 dark:text-orange-300'>
|
||||
Recording: {currentTitle}
|
||||
</p>
|
||||
<p className='mb-[12px] text-[12px] text-[var(--text-secondary)]'>
|
||||
{currentPrompt}
|
||||
</p>
|
||||
<div className='flex gap-[8px]'>
|
||||
<Button variant='default' onClick={cancelTraining} className='flex-1'>
|
||||
<X className='mr-[6px] h-[14px] w-[14px]' />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={() => {
|
||||
useCopilotTrainingStore.getState().stopTraining()
|
||||
setLocalPrompt('')
|
||||
}}
|
||||
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90 flex-1'
|
||||
>
|
||||
<Check className='mr-[6px] h-[14px] w-[14px]' />
|
||||
Save Dataset
|
||||
</Button>
|
||||
</div>
|
||||
{startSnapshot && (
|
||||
<div className='mt-[8px] flex items-center gap-[12px] text-[12px]'>
|
||||
<span className='text-orange-600 dark:text-orange-400'>Starting state:</span>
|
||||
<span className='text-[var(--text-primary)]'>
|
||||
{Object.keys(startSnapshot.blocks).length} blocks
|
||||
</span>
|
||||
<span className='text-[var(--text-tertiary)]'>·</span>
|
||||
<span className='text-[var(--text-primary)]'>
|
||||
{startSnapshot.edges.length} edges
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{startSnapshot && (
|
||||
<div className='mt-3 rounded-lg border p-3'>
|
||||
<p className='mb-2 font-medium text-sm'>Starting State</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{Object.keys(startSnapshot.blocks).length} blocks, {startSnapshot.edges.length}{' '}
|
||||
edges
|
||||
{/* New Training Session Tab */}
|
||||
<ModalTabsContent value='new' className='flex flex-col gap-[16px]'>
|
||||
<div className='flex items-center gap-[16px] text-[13px]'>
|
||||
<span className='text-[var(--text-muted)]'>Current workflow:</span>
|
||||
<span className='text-[var(--text-primary)]'>
|
||||
{currentWorkflow.getBlockCount()} blocks
|
||||
</span>
|
||||
<span className='text-[var(--text-tertiary)]'>·</span>
|
||||
<span className='text-[var(--text-primary)]'>
|
||||
{currentWorkflow.getEdgeCount()} edges
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Label htmlFor='title'>Title</Label>
|
||||
<Input
|
||||
id='title'
|
||||
placeholder='Enter a title for this training dataset...'
|
||||
value={localTitle}
|
||||
onChange={(e) => setLocalTitle(e.target.value)}
|
||||
className='h-9'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Label htmlFor='prompt'>Training Prompt</Label>
|
||||
<Textarea
|
||||
id='prompt'
|
||||
placeholder='Enter the user intent/prompt for this workflow transformation...'
|
||||
value={localPrompt}
|
||||
onChange={(e) => setLocalPrompt(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
<p className='text-[12px] text-[var(--text-muted)]'>
|
||||
Describe what the next sequence of edits aim to achieve
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue={isTraining ? 'datasets' : 'new'} className='mt-4'>
|
||||
<TabsList className='grid w-full grid-cols-3'>
|
||||
<TabsTrigger value='new' disabled={isTraining}>
|
||||
New Session
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value='datasets'>Datasets ({datasets.length})</TabsTrigger>
|
||||
<TabsTrigger value='live'>Send Live State</TabsTrigger>
|
||||
</TabsList>
|
||||
<Button
|
||||
onClick={handleStart}
|
||||
disabled={!localTitle.trim() || !localPrompt.trim()}
|
||||
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90 w-full'
|
||||
>
|
||||
Start Training Session
|
||||
</Button>
|
||||
</ModalTabsContent>
|
||||
|
||||
{/* New Training Session Tab */}
|
||||
<TabsContent value='new' className='space-y-4'>
|
||||
<div className='rounded-lg border bg-muted/50 p-3'>
|
||||
<p className='mb-2 font-medium text-muted-foreground text-sm'>
|
||||
Current Workflow State
|
||||
</p>
|
||||
<p className='text-sm'>
|
||||
{currentWorkflow.getBlockCount()} blocks, {currentWorkflow.getEdgeCount()} edges
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='title'>Title</Label>
|
||||
<Input
|
||||
id='title'
|
||||
placeholder='Enter a title for this training dataset...'
|
||||
value={localTitle}
|
||||
onChange={(e) => setLocalTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='prompt'>Training Prompt</Label>
|
||||
<Textarea
|
||||
id='prompt'
|
||||
placeholder='Enter the user intent/prompt for this workflow transformation...'
|
||||
value={localPrompt}
|
||||
onChange={(e) => setLocalPrompt(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Describe what the next sequence of edits aim to achieve
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleStart}
|
||||
disabled={!localTitle.trim() || !localPrompt.trim()}
|
||||
className='w-full'
|
||||
>
|
||||
Start Training Session
|
||||
</Button>
|
||||
</TabsContent>
|
||||
|
||||
{/* Datasets Tab */}
|
||||
<TabsContent value='datasets' className='space-y-4'>
|
||||
{datasets.length === 0 ? (
|
||||
<div className='py-8 text-center text-muted-foreground'>
|
||||
No training datasets yet. Start a new session to create one.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<Checkbox
|
||||
checked={datasets.length > 0 && selectedDatasets.size === datasets.length}
|
||||
onCheckedChange={toggleSelectAll}
|
||||
disabled={datasets.length === 0}
|
||||
/>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
{selectedDatasets.size > 0
|
||||
? `${selectedDatasets.size} of ${datasets.length} selected`
|
||||
: `${datasets.length} dataset${datasets.length !== 1 ? 's' : ''} recorded`}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex gap-2'>
|
||||
{selectedDatasets.size > 0 && (
|
||||
<Button
|
||||
variant='default'
|
||||
size='sm'
|
||||
onClick={handleSendSelected}
|
||||
disabled={sendingSelected}
|
||||
>
|
||||
<Send className='mr-2 h-4 w-4' />
|
||||
{sendingSelected ? 'Sending...' : `Send ${selectedDatasets.size} Selected`}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleSendAll}
|
||||
disabled={datasets.length === 0 || sendingAll}
|
||||
>
|
||||
<Send className='mr-2 h-4 w-4' />
|
||||
{sendingAll ? 'Sending...' : 'Send All'}
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleExportAll}
|
||||
disabled={datasets.length === 0}
|
||||
>
|
||||
<Download className='mr-2 h-4 w-4' />
|
||||
Export
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={clearDatasets}
|
||||
disabled={datasets.length === 0}
|
||||
>
|
||||
<Trash2 className='mr-2 h-4 w-4' />
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
{/* Datasets Tab */}
|
||||
<ModalTabsContent value='datasets' className='flex flex-col gap-[16px]'>
|
||||
{datasets.length === 0 ? (
|
||||
<div className='py-[32px] text-center text-[13px] text-[var(--text-muted)]'>
|
||||
No training datasets yet. Start a new session to create one.
|
||||
</div>
|
||||
|
||||
<ScrollArea className='h-[400px]'>
|
||||
<div className='space-y-3'>
|
||||
{datasets.map((dataset, index) => (
|
||||
<div
|
||||
key={dataset.id}
|
||||
className='rounded-lg border bg-card transition-colors hover:bg-muted/50'
|
||||
>
|
||||
<div className='flex items-start p-4'>
|
||||
<Checkbox
|
||||
checked={selectedDatasets.has(dataset.id)}
|
||||
onCheckedChange={() => toggleDatasetSelection(dataset.id)}
|
||||
className='mt-0.5 mr-3'
|
||||
/>
|
||||
<button
|
||||
className='flex flex-1 items-center justify-between text-left'
|
||||
onClick={() =>
|
||||
setExpandedDataset(expandedDataset === dataset.id ? null : dataset.id)
|
||||
}
|
||||
>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium text-sm'>{dataset.title}</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{dataset.prompt.substring(0, 50)}
|
||||
{dataset.prompt.length > 50 ? '...' : ''}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex items-center gap-3'>
|
||||
{dataset.sentAt && (
|
||||
<span className='inline-flex items-center rounded-full bg-green-50 px-2 py-0.5 text-green-700 text-xs ring-1 ring-green-600/20 ring-inset dark:bg-green-900/20 dark:text-green-300'>
|
||||
<CheckCircle2 className='mr-1 h-3 w-3' /> Sent
|
||||
</span>
|
||||
)}
|
||||
<span className='text-muted-foreground text-xs'>
|
||||
{dataset.editSequence.length} ops
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-4 w-4 text-muted-foreground transition-transform',
|
||||
expandedDataset === dataset.id && 'rotate-180'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{expandedDataset === dataset.id && (
|
||||
<div className='space-y-3 border-t px-4 pt-3 pb-4'>
|
||||
<div>
|
||||
<p className='mb-1 font-medium text-sm'>Prompt</p>
|
||||
<p className='text-muted-foreground text-sm'>{dataset.prompt}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className='mb-1 font-medium text-sm'>Statistics</p>
|
||||
<div className='grid grid-cols-2 gap-2 text-sm'>
|
||||
<div>
|
||||
<span className='text-muted-foreground'>Duration:</span>{' '}
|
||||
{dataset.metadata?.duration
|
||||
? `${(dataset.metadata.duration / 1000).toFixed(1)}s`
|
||||
: 'N/A'}
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-muted-foreground'>Operations:</span>{' '}
|
||||
{dataset.editSequence.length}
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-muted-foreground'>Final blocks:</span>{' '}
|
||||
{dataset.metadata?.blockCount || 0}
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-muted-foreground'>Final edges:</span>{' '}
|
||||
{dataset.metadata?.edgeCount || 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className='mb-1 font-medium text-sm'>Edit Sequence</p>
|
||||
<div className='max-h-32 overflow-y-auto rounded border bg-muted/50 p-2'>
|
||||
<ul className='space-y-1 font-mono text-xs'>
|
||||
{formatEditSequence(dataset.editSequence).map((desc, i) => (
|
||||
<li key={i}>{desc}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
variant={
|
||||
sentDatasets.has(dataset.id)
|
||||
? 'outline'
|
||||
: failedDatasets.has(dataset.id)
|
||||
? 'destructive'
|
||||
: 'outline'
|
||||
}
|
||||
size='sm'
|
||||
onClick={() => handleSendOne(dataset)}
|
||||
disabled={sendingDatasets.has(dataset.id)}
|
||||
className={
|
||||
sentDatasets.has(dataset.id)
|
||||
? 'border-green-500 text-green-600 hover:bg-green-50 dark:border-green-400 dark:text-green-400 dark:hover:bg-green-950'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{sendingDatasets.has(dataset.id) ? (
|
||||
<>
|
||||
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent' />
|
||||
Sending...
|
||||
</>
|
||||
) : sentDatasets.has(dataset.id) ? (
|
||||
<>
|
||||
<CheckCircle2 className='mr-2 h-4 w-4' />
|
||||
Sent
|
||||
</>
|
||||
) : failedDatasets.has(dataset.id) ? (
|
||||
<>
|
||||
<XCircle className='mr-2 h-4 w-4' />
|
||||
Failed
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className='mr-2 h-4 w-4' />
|
||||
Send
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => setViewingDataset(dataset.id)}
|
||||
>
|
||||
<Eye className='mr-2 h-4 w-4' />
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => handleCopyDataset(dataset)}
|
||||
>
|
||||
{copiedId === dataset.id ? (
|
||||
<>
|
||||
<Check className='mr-2 h-4 w-4' />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Clipboard className='mr-2 h-4 w-4' />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{viewingDataset === dataset.id && (
|
||||
<div className='rounded border bg-muted/50 p-3'>
|
||||
<pre className='max-h-64 overflow-auto text-xs'>
|
||||
{JSON.stringify(
|
||||
{
|
||||
prompt: dataset.prompt,
|
||||
editSequence: dataset.editSequence,
|
||||
metadata: dataset.metadata,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Send Live State Tab */}
|
||||
<TabsContent value='live' className='space-y-4'>
|
||||
<div className='rounded-lg border bg-muted/50 p-3'>
|
||||
<p className='mb-2 font-medium text-muted-foreground text-sm'>
|
||||
Current Workflow State
|
||||
</p>
|
||||
<p className='text-sm'>
|
||||
{currentWorkflow.getBlockCount()} blocks, {currentWorkflow.getEdgeCount()} edges
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='live-title'>Title</Label>
|
||||
<Input
|
||||
id='live-title'
|
||||
placeholder='e.g., Customer Onboarding Workflow'
|
||||
value={liveWorkflowTitle}
|
||||
onChange={(e) => setLiveWorkflowTitle(e.target.value)}
|
||||
/>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
A short title identifying this workflow
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='live-description'>Description</Label>
|
||||
<Textarea
|
||||
id='live-description'
|
||||
placeholder='Describe what this workflow does...'
|
||||
value={liveWorkflowDescription}
|
||||
onChange={(e) => setLiveWorkflowDescription(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Explain the purpose and functionality of this workflow
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleSendLiveWorkflow}
|
||||
disabled={
|
||||
!liveWorkflowTitle.trim() ||
|
||||
!liveWorkflowDescription.trim() ||
|
||||
sendingLiveWorkflow ||
|
||||
currentWorkflow.getBlockCount() === 0
|
||||
}
|
||||
className='w-full'
|
||||
>
|
||||
{sendingLiveWorkflow ? (
|
||||
<>
|
||||
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent' />
|
||||
Sending...
|
||||
</>
|
||||
) : liveWorkflowSent ? (
|
||||
<>
|
||||
<CheckCircle2 className='mr-2 h-4 w-4' />
|
||||
Sent Successfully
|
||||
</>
|
||||
) : liveWorkflowFailed ? (
|
||||
<>
|
||||
<XCircle className='mr-2 h-4 w-4' />
|
||||
Failed - Try Again
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className='mr-2 h-4 w-4' />
|
||||
Send Live Workflow State
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-[12px]'>
|
||||
<Checkbox
|
||||
checked={datasets.length > 0 && selectedDatasets.size === datasets.length}
|
||||
onCheckedChange={toggleSelectAll}
|
||||
disabled={datasets.length === 0}
|
||||
/>
|
||||
<p className='text-[13px] text-[var(--text-muted)]'>
|
||||
{selectedDatasets.size > 0
|
||||
? `${selectedDatasets.size} of ${datasets.length} selected`
|
||||
: `${datasets.length} dataset${datasets.length !== 1 ? 's' : ''} recorded`}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex gap-[8px]'>
|
||||
{selectedDatasets.size > 0 && (
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSendSelected}
|
||||
disabled={sendingSelected}
|
||||
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
|
||||
>
|
||||
<Send className='mr-[6px] h-[12px] w-[12px]' />
|
||||
{sendingSelected
|
||||
? 'Sending...'
|
||||
: `Send ${selectedDatasets.size} Selected`}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={handleSendAll}
|
||||
disabled={datasets.length === 0 || sendingAll}
|
||||
>
|
||||
<Send className='mr-[6px] h-[12px] w-[12px]' />
|
||||
{sendingAll ? 'Sending...' : 'Send All'}
|
||||
</Button>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={handleExportAll}
|
||||
disabled={datasets.length === 0}
|
||||
>
|
||||
<Download className='mr-[6px] h-[12px] w-[12px]' />
|
||||
Export
|
||||
</Button>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={clearDatasets}
|
||||
disabled={datasets.length === 0}
|
||||
>
|
||||
<Trash2 className='mr-[6px] h-[12px] w-[12px]' />
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='max-h-[320px] overflow-y-auto'>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
{datasets.map((dataset, index) => (
|
||||
<div
|
||||
key={dataset.id}
|
||||
className='rounded-[8px] border bg-[var(--surface-3)] transition-colors hover:bg-[var(--surface-5)]'
|
||||
>
|
||||
<div className='flex items-start p-[12px]'>
|
||||
<Checkbox
|
||||
checked={selectedDatasets.has(dataset.id)}
|
||||
onCheckedChange={() => toggleDatasetSelection(dataset.id)}
|
||||
className='mt-[2px] mr-[12px]'
|
||||
/>
|
||||
<button
|
||||
className='flex flex-1 items-center justify-between text-left'
|
||||
onClick={() =>
|
||||
setExpandedDataset(
|
||||
expandedDataset === dataset.id ? null : dataset.id
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium text-[14px] text-[var(--text-primary)]'>
|
||||
{dataset.title}
|
||||
</p>
|
||||
<p className='text-[12px] text-[var(--text-muted)]'>
|
||||
{dataset.prompt.substring(0, 50)}
|
||||
{dataset.prompt.length > 50 ? '...' : ''}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex items-center gap-[12px]'>
|
||||
{dataset.sentAt && (
|
||||
<span className='inline-flex items-center rounded-full bg-green-50 px-[8px] py-[2px] text-[11px] text-green-700 ring-1 ring-green-600/20 ring-inset dark:bg-green-900/20 dark:text-green-300'>
|
||||
<CheckCircle2 className='mr-[4px] h-[10px] w-[10px]' /> Sent
|
||||
</span>
|
||||
)}
|
||||
<span className='text-[12px] text-[var(--text-muted)]'>
|
||||
{dataset.editSequence.length} ops
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-[14px] w-[14px] text-[var(--text-muted)] transition-transform',
|
||||
expandedDataset === dataset.id && 'rotate-180'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{expandedDataset === dataset.id && (
|
||||
<div className='flex flex-col gap-[12px] border-t px-[12px] pt-[12px] pb-[16px]'>
|
||||
<div>
|
||||
<p className='mb-[4px] font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
Prompt
|
||||
</p>
|
||||
<p className='text-[13px] text-[var(--text-secondary)]'>
|
||||
{dataset.prompt}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className='mb-[4px] font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
Statistics
|
||||
</p>
|
||||
<div className='grid grid-cols-2 gap-[8px] text-[13px]'>
|
||||
<div>
|
||||
<span className='text-[var(--text-muted)]'>Duration:</span>{' '}
|
||||
<span className='text-[var(--text-secondary)]'>
|
||||
{dataset.metadata?.duration
|
||||
? `${(dataset.metadata.duration / 1000).toFixed(1)}s`
|
||||
: 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-[var(--text-muted)]'>Operations:</span>{' '}
|
||||
<span className='text-[var(--text-secondary)]'>
|
||||
{dataset.editSequence.length}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-[var(--text-muted)]'>Final blocks:</span>{' '}
|
||||
<span className='text-[var(--text-secondary)]'>
|
||||
{dataset.metadata?.blockCount || 0}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-[var(--text-muted)]'>Final edges:</span>{' '}
|
||||
<span className='text-[var(--text-secondary)]'>
|
||||
{dataset.metadata?.edgeCount || 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className='mb-[4px] font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
Edit Sequence
|
||||
</p>
|
||||
<div className='max-h-[100px] overflow-y-auto rounded-[6px] border bg-[var(--surface-6)] p-[8px]'>
|
||||
<ul className='flex flex-col gap-[4px] font-mono text-[11px]'>
|
||||
{formatEditSequence(dataset.editSequence).map((desc, i) => (
|
||||
<li key={i} className='text-[var(--text-secondary)]'>
|
||||
{desc}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-[8px]'>
|
||||
<Button
|
||||
variant={
|
||||
sentDatasets.has(dataset.id)
|
||||
? 'default'
|
||||
: failedDatasets.has(dataset.id)
|
||||
? 'default'
|
||||
: 'default'
|
||||
}
|
||||
onClick={() => handleSendOne(dataset)}
|
||||
disabled={sendingDatasets.has(dataset.id)}
|
||||
className={
|
||||
sentDatasets.has(dataset.id)
|
||||
? '!border-green-500 !text-green-600 dark:!border-green-400 dark:!text-green-400'
|
||||
: failedDatasets.has(dataset.id)
|
||||
? '!border-red-500 !text-red-600 dark:!border-red-400 dark:!text-red-400'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{sendingDatasets.has(dataset.id) ? (
|
||||
<>
|
||||
<div className='mr-[6px] h-[12px] w-[12px] animate-spin rounded-full border-2 border-current border-t-transparent' />
|
||||
Sending...
|
||||
</>
|
||||
) : sentDatasets.has(dataset.id) ? (
|
||||
<>
|
||||
<CheckCircle2 className='mr-[6px] h-[12px] w-[12px]' />
|
||||
Sent
|
||||
</>
|
||||
) : failedDatasets.has(dataset.id) ? (
|
||||
<>
|
||||
<XCircle className='mr-[6px] h-[12px] w-[12px]' />
|
||||
Failed
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className='mr-[6px] h-[12px] w-[12px]' />
|
||||
Send
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={() => setViewingDataset(dataset.id)}
|
||||
>
|
||||
<Eye className='mr-[6px] h-[12px] w-[12px]' />
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={() => handleCopyDataset(dataset)}
|
||||
>
|
||||
{copiedId === dataset.id ? (
|
||||
<>
|
||||
<Check className='mr-[6px] h-[12px] w-[12px]' />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Clipboard className='mr-[6px] h-[12px] w-[12px]' />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{viewingDataset === dataset.id && (
|
||||
<div className='rounded-[6px] border bg-[var(--surface-6)] p-[12px]'>
|
||||
<pre className='max-h-[200px] overflow-auto text-[11px] text-[var(--text-secondary)]'>
|
||||
{JSON.stringify(
|
||||
{
|
||||
prompt: dataset.prompt,
|
||||
editSequence: dataset.editSequence,
|
||||
metadata: dataset.metadata,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</ModalTabsContent>
|
||||
|
||||
{liveWorkflowSent && (
|
||||
<div className='rounded-lg border bg-green-50 p-3 dark:bg-green-950/30'>
|
||||
<p className='text-green-700 text-sm dark:text-green-300'>
|
||||
Workflow state sent successfully!
|
||||
{/* Send Live State Tab */}
|
||||
<ModalTabsContent value='live' className='flex flex-col gap-[16px]'>
|
||||
<div className='flex items-center gap-[16px] text-[13px]'>
|
||||
<span className='text-[var(--text-muted)]'>Current workflow:</span>
|
||||
<span className='text-[var(--text-primary)]'>
|
||||
{currentWorkflow.getBlockCount()} blocks
|
||||
</span>
|
||||
<span className='text-[var(--text-tertiary)]'>·</span>
|
||||
<span className='text-[var(--text-primary)]'>
|
||||
{currentWorkflow.getEdgeCount()} edges
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Label htmlFor='live-title'>Title</Label>
|
||||
<Input
|
||||
id='live-title'
|
||||
placeholder='e.g., Customer Onboarding Workflow'
|
||||
value={liveWorkflowTitle}
|
||||
onChange={(e) => setLiveWorkflowTitle(e.target.value)}
|
||||
className='h-9'
|
||||
/>
|
||||
<p className='text-[12px] text-[var(--text-muted)]'>
|
||||
A short title identifying this workflow
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{liveWorkflowFailed && (
|
||||
<div className='rounded-lg border bg-red-50 p-3 dark:bg-red-950/30'>
|
||||
<p className='text-red-700 text-sm dark:text-red-300'>
|
||||
Failed to send workflow state. Please try again.
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Label htmlFor='live-description'>Description</Label>
|
||||
<Textarea
|
||||
id='live-description'
|
||||
placeholder='Describe what this workflow does...'
|
||||
value={liveWorkflowDescription}
|
||||
onChange={(e) => setLiveWorkflowDescription(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
<p className='text-[12px] text-[var(--text-muted)]'>
|
||||
Explain the purpose and functionality of this workflow
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Button
|
||||
onClick={handleSendLiveWorkflow}
|
||||
disabled={
|
||||
!liveWorkflowTitle.trim() ||
|
||||
!liveWorkflowDescription.trim() ||
|
||||
sendingLiveWorkflow ||
|
||||
currentWorkflow.getBlockCount() === 0
|
||||
}
|
||||
className={cn(
|
||||
'w-full',
|
||||
liveWorkflowSent
|
||||
? '!bg-green-600 !text-white hover:!bg-green-700'
|
||||
: liveWorkflowFailed
|
||||
? '!bg-red-600 !text-white hover:!bg-red-700'
|
||||
: '!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
|
||||
)}
|
||||
>
|
||||
{sendingLiveWorkflow ? (
|
||||
<>
|
||||
<div className='mr-[6px] h-[14px] w-[14px] animate-spin rounded-full border-2 border-current border-t-transparent' />
|
||||
Sending...
|
||||
</>
|
||||
) : liveWorkflowSent ? (
|
||||
<>
|
||||
<CheckCircle2 className='mr-[6px] h-[14px] w-[14px]' />
|
||||
Sent Successfully
|
||||
</>
|
||||
) : liveWorkflowFailed ? (
|
||||
<>
|
||||
<XCircle className='mr-[6px] h-[14px] w-[14px]' />
|
||||
Failed - Try Again
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className='mr-[6px] h-[14px] w-[14px]' />
|
||||
Send Live Workflow State
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{liveWorkflowSent && (
|
||||
<div className='rounded-[8px] border bg-green-50 p-[12px] dark:bg-green-950/30'>
|
||||
<p className='text-[13px] text-green-700 dark:text-green-300'>
|
||||
Workflow state sent successfully!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{liveWorkflowFailed && (
|
||||
<div className='rounded-[8px] border bg-red-50 p-[12px] dark:bg-red-950/30'>
|
||||
<p className='text-[13px] text-red-700 dark:text-red-300'>
|
||||
Failed to send workflow state. Please try again.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</ModalTabsContent>
|
||||
</ModalBody>
|
||||
</ModalTabs>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,13 +24,13 @@ import {
|
||||
Panel,
|
||||
SubflowNodeComponent,
|
||||
Terminal,
|
||||
TrainingControls,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components'
|
||||
import { Chat } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat'
|
||||
import { Cursors } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/cursors/cursors'
|
||||
import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/error/index'
|
||||
import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block'
|
||||
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
|
||||
import { TrainingModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/training-controls/training-modal'
|
||||
import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
|
||||
import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge'
|
||||
import {
|
||||
@@ -45,6 +45,7 @@ import { useWorkspaceEnvironment } from '@/hooks/queries/environment'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { useStreamCleanup } from '@/hooks/use-stream-cleanup'
|
||||
import { useWorkspacePermissions } from '@/hooks/use-workspace-permissions'
|
||||
import { useCopilotTrainingStore } from '@/stores/copilot-training/store'
|
||||
import { useExecutionStore } from '@/stores/execution/store'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
import { useCopilotStore } from '@/stores/panel/copilot/store'
|
||||
@@ -134,6 +135,9 @@ const WorkflowContent = React.memo(() => {
|
||||
// Get copilot cleanup function
|
||||
const copilotCleanup = useCopilotStore((state) => state.cleanup)
|
||||
|
||||
// Training modal state
|
||||
const showTrainingModal = useCopilotTrainingStore((state) => state.showModal)
|
||||
|
||||
// Handle copilot stream cleanup on page unload and component unmount
|
||||
useStreamCleanup(copilotCleanup)
|
||||
|
||||
@@ -2244,8 +2248,8 @@ const WorkflowContent = React.memo(() => {
|
||||
return (
|
||||
<div className='flex h-full w-full flex-col overflow-hidden'>
|
||||
<div className='relative h-full w-full flex-1 transition-all duration-200'>
|
||||
{/* Training Controls - for recording workflow edits */}
|
||||
<TrainingControls />
|
||||
{/* Training Modal - for recording workflow edits */}
|
||||
{showTrainingModal && <TrainingModal />}
|
||||
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
|
||||
@@ -303,7 +303,8 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_SUPPORT_EMAIL: z.string().email().optional(), // Custom support email
|
||||
|
||||
NEXT_PUBLIC_E2B_ENABLED: z.string().optional(),
|
||||
NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: z.string().optional(),
|
||||
NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: z.string().optional(),
|
||||
NEXT_PUBLIC_ENABLE_PLAYGROUND: z.string().optional(), // Enable component playground at /playground
|
||||
NEXT_PUBLIC_DOCUMENTATION_URL: z.string().url().optional(), // Custom documentation URL
|
||||
NEXT_PUBLIC_TERMS_URL: z.string().url().optional(), // Custom terms of service URL
|
||||
NEXT_PUBLIC_PRIVACY_URL: z.string().url().optional(), // Custom privacy policy URL
|
||||
@@ -352,6 +353,7 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED: process.env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED,
|
||||
NEXT_PUBLIC_E2B_ENABLED: process.env.NEXT_PUBLIC_E2B_ENABLED,
|
||||
NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: process.env.NEXT_PUBLIC_COPILOT_TRAINING_ENABLED,
|
||||
NEXT_PUBLIC_ENABLE_PLAYGROUND: process.env.NEXT_PUBLIC_ENABLE_PLAYGROUND,
|
||||
NEXT_PUBLIC_POSTHOG_ENABLED: process.env.NEXT_PUBLIC_POSTHOG_ENABLED,
|
||||
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
|
||||
Reference in New Issue
Block a user