mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -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 { SkeletonLoading } from './skeleton-loading/skeleton-loading'
|
||||||
export { SubflowNodeComponent } from './subflows/subflow-node'
|
export { SubflowNodeComponent } from './subflows/subflow-node'
|
||||||
export { Terminal } from './terminal/terminal'
|
export { Terminal } from './terminal/terminal'
|
||||||
export { TrainingControls } from './training-controls/training-controls'
|
|
||||||
export { WandPromptBar } from './wand-prompt-bar/wand-prompt-bar'
|
export { WandPromptBar } from './wand-prompt-bar/wand-prompt-bar'
|
||||||
export { WorkflowBlock } from './workflow-block/workflow-block'
|
export { WorkflowBlock } from './workflow-block/workflow-block'
|
||||||
export { WorkflowEdge } from './workflow-edge/workflow-edge'
|
export { WorkflowEdge } from './workflow-edge/workflow-edge'
|
||||||
|
|||||||
@@ -9,15 +9,19 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Clipboard,
|
Clipboard,
|
||||||
|
Database,
|
||||||
Filter,
|
Filter,
|
||||||
FilterX,
|
FilterX,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
|
Palette,
|
||||||
|
Pause,
|
||||||
RepeatIcon,
|
RepeatIcon,
|
||||||
Search,
|
Search,
|
||||||
SplitIcon,
|
SplitIcon,
|
||||||
Trash2,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -30,6 +34,7 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@/components/emcn'
|
} from '@/components/emcn'
|
||||||
|
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||||
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
|
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
|
||||||
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
|
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
|
||||||
import {
|
import {
|
||||||
@@ -38,6 +43,8 @@ import {
|
|||||||
useTerminalResize,
|
useTerminalResize,
|
||||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks'
|
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks'
|
||||||
import { getBlock } from '@/blocks'
|
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 type { ConsoleEntry } from '@/stores/terminal'
|
||||||
import { useTerminalConsoleStore, useTerminalStore } from '@/stores/terminal'
|
import { useTerminalConsoleStore, useTerminalStore } from '@/stores/terminal'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
@@ -331,6 +338,14 @@ export function Terminal() {
|
|||||||
const outputSearchInputRef = useRef<HTMLInputElement>(null)
|
const outputSearchInputRef = useRef<HTMLInputElement>(null)
|
||||||
const outputContentRef = useRef<HTMLDivElement>(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
|
// Terminal resize hooks
|
||||||
const { handleMouseDown } = useTerminalResize()
|
const { handleMouseDown } = useTerminalResize()
|
||||||
const { handleMouseDown: handleOutputPanelResizeMouseDown } = useOutputPanelResize()
|
const { handleMouseDown: handleOutputPanelResizeMouseDown } = useOutputPanelResize()
|
||||||
@@ -612,6 +627,26 @@ export function Terminal() {
|
|||||||
[activeWorkflowId, exportConsoleCSV]
|
[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:
|
* Register global keyboard shortcuts for the terminal:
|
||||||
* - Mod+D: Clear terminal console for the active workflow
|
* - Mod+D: Clear terminal console for the active workflow
|
||||||
@@ -640,6 +675,14 @@ export function Terminal() {
|
|||||||
setHasHydrated(true)
|
setHasHydrated(true)
|
||||||
}, [setHasHydrated])
|
}, [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
|
* Adjust showInput when selected entry changes
|
||||||
* Stay on input view if the new entry has input data
|
* Stay on input view if the new entry has input data
|
||||||
@@ -1104,6 +1147,48 @@ export function Terminal() {
|
|||||||
)}
|
)}
|
||||||
{!selectedEntry && (
|
{!selectedEntry && (
|
||||||
<div className='ml-auto flex items-center gap-[8px]'>
|
<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 && (
|
{hasActiveFilters && (
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger asChild>
|
<Tooltip.Trigger asChild>
|
||||||
@@ -1426,6 +1511,50 @@ export function Terminal() {
|
|||||||
</Tooltip.Root>
|
</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.Root>
|
||||||
<Tooltip.Trigger asChild>
|
<Tooltip.Trigger asChild>
|
||||||
<Button
|
<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,
|
X,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Button,
|
||||||
DialogContent,
|
Input,
|
||||||
DialogDescription,
|
Label,
|
||||||
DialogHeader,
|
Modal,
|
||||||
DialogTitle,
|
ModalBody,
|
||||||
} from '@/components/ui/dialog'
|
ModalContent,
|
||||||
import { Input } from '@/components/ui/input'
|
ModalHeader,
|
||||||
import { Label } from '@/components/ui/label'
|
ModalTabs,
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
ModalTabsContent,
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
ModalTabsList,
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
ModalTabsTrigger,
|
||||||
|
Textarea,
|
||||||
|
} from '@/components/emcn'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
||||||
@@ -74,6 +75,7 @@ export function TrainingModal() {
|
|||||||
const [liveWorkflowFailed, setLiveWorkflowFailed] = useState(false)
|
const [liveWorkflowFailed, setLiveWorkflowFailed] = useState(false)
|
||||||
const [liveWorkflowTitle, setLiveWorkflowTitle] = useState('')
|
const [liveWorkflowTitle, setLiveWorkflowTitle] = useState('')
|
||||||
const [liveWorkflowDescription, setLiveWorkflowDescription] = useState('')
|
const [liveWorkflowDescription, setLiveWorkflowDescription] = useState('')
|
||||||
|
const [activeTab, setActiveTab] = useState(isTraining ? 'datasets' : 'new')
|
||||||
|
|
||||||
const handleStart = () => {
|
const handleStart = () => {
|
||||||
if (localTitle.trim() && localPrompt.trim()) {
|
if (localTitle.trim() && localPrompt.trim()) {
|
||||||
@@ -340,438 +342,470 @@ export function TrainingModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={showModal} onOpenChange={toggleModal}>
|
<Modal open={showModal} onOpenChange={toggleModal}>
|
||||||
<DialogContent className='max-w-3xl'>
|
<ModalContent size='lg'>
|
||||||
<DialogHeader>
|
<ModalHeader>Copilot Training Dataset Builder</ModalHeader>
|
||||||
<DialogTitle>Copilot Training Dataset Builder</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Record workflow editing sessions to create training datasets for the copilot
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{isTraining && (
|
<ModalTabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
<>
|
<ModalTabsList>
|
||||||
<div className='mt-4 rounded-lg border bg-orange-50 p-4 dark:bg-orange-950/30'>
|
<ModalTabsTrigger value='new' disabled={isTraining}>
|
||||||
<p className='mb-2 font-medium text-orange-700 dark:text-orange-300'>
|
New Session
|
||||||
Recording: {currentTitle}
|
</ModalTabsTrigger>
|
||||||
</p>
|
<ModalTabsTrigger value='datasets'>Datasets ({datasets.length})</ModalTabsTrigger>
|
||||||
<p className='mb-3 text-sm'>{currentPrompt}</p>
|
<ModalTabsTrigger value='live'>Send Live State</ModalTabsTrigger>
|
||||||
<div className='flex gap-2'>
|
</ModalTabsList>
|
||||||
<Button variant='outline' size='sm' onClick={cancelTraining} className='flex-1'>
|
|
||||||
<X className='mr-2 h-4 w-4' />
|
<ModalBody className='flex min-h-[400px] flex-col overflow-hidden'>
|
||||||
Cancel
|
{/* Recording Banner */}
|
||||||
</Button>
|
{isTraining && (
|
||||||
<Button
|
<div className='mb-[16px] rounded-[8px] border bg-orange-50 p-[12px] dark:bg-orange-950/30'>
|
||||||
variant='default'
|
<p className='mb-[8px] font-medium text-[13px] text-orange-700 dark:text-orange-300'>
|
||||||
size='sm'
|
Recording: {currentTitle}
|
||||||
onClick={() => {
|
</p>
|
||||||
useCopilotTrainingStore.getState().stopTraining()
|
<p className='mb-[12px] text-[12px] text-[var(--text-secondary)]'>
|
||||||
setLocalPrompt('')
|
{currentPrompt}
|
||||||
}}
|
</p>
|
||||||
className='flex-1'
|
<div className='flex gap-[8px]'>
|
||||||
>
|
<Button variant='default' onClick={cancelTraining} className='flex-1'>
|
||||||
<Check className='mr-2 h-4 w-4' />
|
<X className='mr-[6px] h-[14px] w-[14px]' />
|
||||||
Save Dataset
|
Cancel
|
||||||
</Button>
|
</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>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{startSnapshot && (
|
{/* New Training Session Tab */}
|
||||||
<div className='mt-3 rounded-lg border p-3'>
|
<ModalTabsContent value='new' className='flex flex-col gap-[16px]'>
|
||||||
<p className='mb-2 font-medium text-sm'>Starting State</p>
|
<div className='flex items-center gap-[16px] text-[13px]'>
|
||||||
<p className='text-muted-foreground text-xs'>
|
<span className='text-[var(--text-muted)]'>Current workflow:</span>
|
||||||
{Object.keys(startSnapshot.blocks).length} blocks, {startSnapshot.edges.length}{' '}
|
<span className='text-[var(--text-primary)]'>
|
||||||
edges
|
{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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Tabs defaultValue={isTraining ? 'datasets' : 'new'} className='mt-4'>
|
<Button
|
||||||
<TabsList className='grid w-full grid-cols-3'>
|
onClick={handleStart}
|
||||||
<TabsTrigger value='new' disabled={isTraining}>
|
disabled={!localTitle.trim() || !localPrompt.trim()}
|
||||||
New Session
|
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90 w-full'
|
||||||
</TabsTrigger>
|
>
|
||||||
<TabsTrigger value='datasets'>Datasets ({datasets.length})</TabsTrigger>
|
Start Training Session
|
||||||
<TabsTrigger value='live'>Send Live State</TabsTrigger>
|
</Button>
|
||||||
</TabsList>
|
</ModalTabsContent>
|
||||||
|
|
||||||
{/* New Training Session Tab */}
|
{/* Datasets Tab */}
|
||||||
<TabsContent value='new' className='space-y-4'>
|
<ModalTabsContent value='datasets' className='flex flex-col gap-[16px]'>
|
||||||
<div className='rounded-lg border bg-muted/50 p-3'>
|
{datasets.length === 0 ? (
|
||||||
<p className='mb-2 font-medium text-muted-foreground text-sm'>
|
<div className='py-[32px] text-center text-[13px] text-[var(--text-muted)]'>
|
||||||
Current Workflow State
|
No training datasets yet. Start a new session to create one.
|
||||||
</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>
|
|
||||||
</div>
|
</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' />
|
<div className='flex items-center justify-between'>
|
||||||
Send Live Workflow State
|
<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 && (
|
{/* Send Live State Tab */}
|
||||||
<div className='rounded-lg border bg-green-50 p-3 dark:bg-green-950/30'>
|
<ModalTabsContent value='live' className='flex flex-col gap-[16px]'>
|
||||||
<p className='text-green-700 text-sm dark:text-green-300'>
|
<div className='flex items-center gap-[16px] text-[13px]'>
|
||||||
Workflow state sent successfully!
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{liveWorkflowFailed && (
|
<div className='flex flex-col gap-[8px]'>
|
||||||
<div className='rounded-lg border bg-red-50 p-3 dark:bg-red-950/30'>
|
<Label htmlFor='live-description'>Description</Label>
|
||||||
<p className='text-red-700 text-sm dark:text-red-300'>
|
<Textarea
|
||||||
Failed to send workflow state. Please try again.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</TabsContent>
|
<Button
|
||||||
</Tabs>
|
onClick={handleSendLiveWorkflow}
|
||||||
</DialogContent>
|
disabled={
|
||||||
</Dialog>
|
!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,
|
Panel,
|
||||||
SubflowNodeComponent,
|
SubflowNodeComponent,
|
||||||
Terminal,
|
Terminal,
|
||||||
TrainingControls,
|
|
||||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components'
|
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components'
|
||||||
import { Chat } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat'
|
import { Chat } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat'
|
||||||
import { Cursors } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/cursors/cursors'
|
import { Cursors } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/cursors/cursors'
|
||||||
import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/error/index'
|
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 { 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 { 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 { 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 { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge'
|
||||||
import {
|
import {
|
||||||
@@ -45,6 +45,7 @@ import { useWorkspaceEnvironment } from '@/hooks/queries/environment'
|
|||||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||||
import { useStreamCleanup } from '@/hooks/use-stream-cleanup'
|
import { useStreamCleanup } from '@/hooks/use-stream-cleanup'
|
||||||
import { useWorkspacePermissions } from '@/hooks/use-workspace-permissions'
|
import { useWorkspacePermissions } from '@/hooks/use-workspace-permissions'
|
||||||
|
import { useCopilotTrainingStore } from '@/stores/copilot-training/store'
|
||||||
import { useExecutionStore } from '@/stores/execution/store'
|
import { useExecutionStore } from '@/stores/execution/store'
|
||||||
import { useNotificationStore } from '@/stores/notifications/store'
|
import { useNotificationStore } from '@/stores/notifications/store'
|
||||||
import { useCopilotStore } from '@/stores/panel/copilot/store'
|
import { useCopilotStore } from '@/stores/panel/copilot/store'
|
||||||
@@ -134,6 +135,9 @@ const WorkflowContent = React.memo(() => {
|
|||||||
// Get copilot cleanup function
|
// Get copilot cleanup function
|
||||||
const copilotCleanup = useCopilotStore((state) => state.cleanup)
|
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
|
// Handle copilot stream cleanup on page unload and component unmount
|
||||||
useStreamCleanup(copilotCleanup)
|
useStreamCleanup(copilotCleanup)
|
||||||
|
|
||||||
@@ -2244,8 +2248,8 @@ const WorkflowContent = React.memo(() => {
|
|||||||
return (
|
return (
|
||||||
<div className='flex h-full w-full flex-col overflow-hidden'>
|
<div className='flex h-full w-full flex-col overflow-hidden'>
|
||||||
<div className='relative h-full w-full flex-1 transition-all duration-200'>
|
<div className='relative h-full w-full flex-1 transition-all duration-200'>
|
||||||
{/* Training Controls - for recording workflow edits */}
|
{/* Training Modal - for recording workflow edits */}
|
||||||
<TrainingControls />
|
{showTrainingModal && <TrainingModal />}
|
||||||
|
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
|
|||||||
@@ -303,7 +303,8 @@ export const env = createEnv({
|
|||||||
NEXT_PUBLIC_SUPPORT_EMAIL: z.string().email().optional(), // Custom support email
|
NEXT_PUBLIC_SUPPORT_EMAIL: z.string().email().optional(), // Custom support email
|
||||||
|
|
||||||
NEXT_PUBLIC_E2B_ENABLED: z.string().optional(),
|
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_DOCUMENTATION_URL: z.string().url().optional(), // Custom documentation URL
|
||||||
NEXT_PUBLIC_TERMS_URL: z.string().url().optional(), // Custom terms of service 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
|
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_EMAIL_PASSWORD_SIGNUP_ENABLED: process.env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED,
|
||||||
NEXT_PUBLIC_E2B_ENABLED: process.env.NEXT_PUBLIC_E2B_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_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_ENABLED: process.env.NEXT_PUBLIC_POSTHOG_ENABLED,
|
||||||
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
|
|||||||
Reference in New Issue
Block a user