diff --git a/apps/sim/app/playground/page.tsx b/apps/sim/app/playground/page.tsx
new file mode 100644
index 000000000..7a3804211
--- /dev/null
+++ b/apps/sim/app/playground/page.tsx
@@ -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 (
+
+
+ {title}
+
+ {children}
+
+ )
+}
+
+function VariantRow({ label, children }: { label: string; children: React.ReactNode }) {
+ return (
+
+ )
+}
+
+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 (
+
+
+
+
+
+
+ Go back
+
+
+
+
+ EMCN Component Playground
+
+
+ All emcn UI components and their variants
+
+
+
+ {/* Button */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Badge */}
+
+
+ Default
+
+
+ Outline
+
+
+
+ {/* Input */}
+
+
+ {/* Textarea */}
+
+
+ {/* Label */}
+
+
+ {/* Switch */}
+
+
+
+
+ {switchValue ? 'On' : 'Off'}
+
+
+
+
+ {/* Combobox */}
+
+
+
+
+
+
+
+
+ {}}
+ placeholder='Small size'
+ size='sm'
+ />
+
+
+
+
+ {}}
+ placeholder='With search'
+ searchable
+ />
+
+
+
+
+ {}}
+ placeholder='Type or select...'
+ editable
+ />
+
+
+
+
+ {}}
+ placeholder='Select multiple...'
+ multiSelect
+ searchable
+ />
+
+
+
+
+ {/* Breadcrumb */}
+
+
+ {/* Tooltip */}
+
+
+
+
+
+
+ Tooltip content
+
+
+
+
+ {/* Popover */}
+
+
+
+
+
+
+
+ Section Title
+ Item 1
+ Item 2
+ Active Item
+
+
+
+
+
+
+
+
+
+ Item 1
+ Active Item
+
+
+
+
+
+
+
+
+
+
+
+ Apple
+ Banana
+ Cherry
+ Date
+ Elderberry
+
+
+
+
+
+
+
+
+
+
+
+ Root Item
+ }
+ >
+ Nested Item 1
+ Nested Item 2
+
+ }
+ >
+ Another Nested Item
+
+
+
+
+
+
+ {/* Modal */}
+
+
+ {(['sm', 'md', 'lg', 'xl', 'full'] as const).map((size) => (
+
+
+
+
+
+ Modal {size.toUpperCase()}
+
+ This is a {size} sized modal.
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ Settings
+
+
+ General
+ Advanced
+
+
+
+ General settings content
+
+
+ Advanced settings content
+
+
+
+
+
+
+
+
+
+
+
+ {/* SModal (Sidebar Modal) */}
+
+
+
+
+
+
+
+ Settings
+
+ Account
+ }
+ active={activeTab === 'profile'}
+ onClick={() => setActiveTab('profile')}
+ >
+ Profile
+
+ }
+ active={activeTab === 'security'}
+ onClick={() => setActiveTab('security')}
+ >
+ Security
+
+
+
+ Preferences
+ }
+ active={activeTab === 'notifications'}
+ onClick={() => setActiveTab('notifications')}
+ >
+ Notifications
+
+ }
+ active={activeTab === 'general'}
+ onClick={() => setActiveTab('general')}
+ >
+ General
+
+
+
+
+
+ {activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}
+
+
+ Content for {activeTab} tab
+
+
+
+
+
+
+ {/* Code */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Icons */}
+
+
+ {[
+ { 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 }) => (
+
+
+
+
+
+
+ {name}
+
+ ))}
+
+
+
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts
index 9662e63c9..948fc180c 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts
@@ -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'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx
index d50b65953..c51ed91c5 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx
@@ -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(null)
const outputContentRef = useRef(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 && (
+ {isPlaygroundEnabled && (
+
+
+
+
+
+
+
+ Component Playground
+
+
+ )}
+ {shouldShowTrainingButton && (
+
+
+
+
+
+ {isTraining ? 'Stop Training' : 'Train Copilot'}
+
+
+ )}
{hasActiveFilters && (
@@ -1426,6 +1511,50 @@ export function Terminal() {
)}
+ {isPlaygroundEnabled && (
+
+
+
+
+
+
+
+ Component Playground
+
+
+ )}
+
+ {shouldShowTrainingButton && (
+
+
+
+
+
+ {isTraining ? 'Stop Training' : 'Train Copilot'}
+
+
+ )}
+