diff --git a/assets/css/app.css b/assets/css/app.css index 87eece11dd3..40dd1c98109 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -95,6 +95,20 @@ --color-warning-800: var(--color-yellow-800); --color-warning-900: var(--color-yellow-900); --color-warning-950: var(--color-yellow-950); + + /* NEW DESIGN SYSTEM - TODO-AI-FIRST extend? */ + --gray-light-200: #eae9e8; + --gray-light-300: #d6d5d4; + --gray-light-400: #cccbca; + + --color-border-strong: var(--gray-light-400); + --color-border-subtle: var(--gray-light-200); + + /* Surface tokens */ + --color-surface-subtle: #EBF5F2; + + /* Semantic tokens */ + --color-semantic-success: #006840; } @layer base { diff --git a/assets/js/collaborative-editor/CollaborativeEditor.tsx b/assets/js/collaborative-editor/CollaborativeEditor.tsx index a86a7030972..6c4a9d95ca0 100644 --- a/assets/js/collaborative-editor/CollaborativeEditor.tsx +++ b/assets/js/collaborative-editor/CollaborativeEditor.tsx @@ -2,29 +2,36 @@ import { useMemo, useRef } from 'react'; import { useURLState } from '#/react/lib/use-url-state'; +import { PickerButton } from '../picker/PickerButton'; import { SocketProvider } from '../react/contexts/SocketProvider'; import type { WithActionProps } from '../react/lib/with-props'; import { AIAssistantPanelWrapper } from './components/AIAssistantPanelWrapper'; import { BreadcrumbLink, BreadcrumbText } from './components/Breadcrumbs'; -import { PickerButton } from '../picker/PickerButton'; import type { MonacoHandle } from './components/CollaborativeMonaco'; import { Header } from './components/Header'; +import { LandingScreen } from './components/LandingScreen'; import { LoadingBoundary } from './components/LoadingBoundary'; import { Toaster } from './components/ui/Toaster'; import { VersionDebugLogger } from './components/VersionDebugLogger'; import { VersionDropdown } from './components/VersionDropdown'; import { WorkflowEditor } from './components/WorkflowEditor'; +import { YAMLImportModal } from './components/YAMLImportModal'; import { CredentialModalProvider } from './contexts/CredentialModalContext'; import { LiveViewActionsProvider } from './contexts/LiveViewActionsContext'; import { MonacoRefProvider } from './contexts/MonacoRefContext'; import { SessionProvider } from './contexts/SessionProvider'; import { StoreProvider } from './contexts/StoreProvider'; import { + useIsNewWorkflow, useLatestSnapshotLockVersion, useProject, } from './hooks/useSessionContext'; -import { useIsRunPanelOpen } from './hooks/useUI'; +import { + useIsRunPanelOpen, + useShowLandingScreen, + useUICommands, +} from './hooks/useUI'; import { useVersionSelect } from './hooks/useVersionSelect'; import { useWorkflowState } from './hooks/useWorkflow'; import { KeyboardProvider } from './keyboard'; @@ -67,7 +74,6 @@ interface BreadcrumbContentProps { projectIsSandboxFallback?: string; projectColorFallback?: string | null; projectEnvFallback?: string; - isNewWorkflow?: boolean; aiAssistantEnabled: boolean; } @@ -80,18 +86,16 @@ function BreadcrumbContent({ projectIsSandboxFallback, projectColorFallback, projectEnvFallback, - isNewWorkflow = false, aiAssistantEnabled, }: BreadcrumbContentProps) { + const isNewWorkflow = useIsNewWorkflow(); const projectFromStore = useProject(); - const workflowFromStore = useWorkflowState(state => state.workflow); const latestSnapshotLockVersion = useLatestSnapshotLockVersion(); - const isRunPanelOpen = useIsRunPanelOpen(); - const { params } = useURLState(); const isIDEOpen = params['panel'] === 'editor'; + const handleVersionSelect = useVersionSelect(); const projectId = projectFromStore?.id ?? projectIdFallback; const projectName = projectFromStore?.name ?? projectNameFallback; @@ -101,8 +105,6 @@ function BreadcrumbContent({ const isSandbox = projectIsSandboxFallback === 'true'; const currentWorkflowName = workflowFromStore?.name ?? workflowName; - const handleVersionSelect = useVersionSelect(); - const breadcrumbElements = useMemo(() => { return [ // Project name as picker trigger @@ -152,12 +154,15 @@ function BreadcrumbContent({ projectColor, projectEnv, currentWorkflowName, - workflowId, workflowFromStore?.lock_version, latestSnapshotLockVersion, handleVersionSelect, + isNewWorkflow, ]); + // Hide header until the first save clears isNewWorkflow in the store. + if (isNewWorkflow) return null; + return (
+ { + dismissLandingScreen(); + openAIAssistantPanel(prompt); + }} + onBuildFromScratch={() => {}} + onBrowseTemplates={() => {}} + onImportYAML={openYAMLImportModal} + /> + + + ); +} + export const CollaborativeEditor: WithActionProps< CollaborativeEditorDataProps > = props => { @@ -222,7 +255,6 @@ export const CollaborativeEditor: WithActionProps< + void; + onClose?: () => void; onNewConversation?: () => void; onSessionSelect?: (sessionId: string) => void; onShowSessions?: () => void; @@ -233,7 +233,7 @@ export function AIAssistantPanel({ if (onShowSessions) { onShowSessions(); } - } else { + } else if (onClose) { onClose(); } }; @@ -390,27 +390,31 @@ export function AIAssistantPanel({ )} - - - + + + )} diff --git a/assets/js/collaborative-editor/components/AIAssistantPanelWrapper.tsx b/assets/js/collaborative-editor/components/AIAssistantPanelWrapper.tsx index b2c158545aa..83dc10fe9b7 100644 --- a/assets/js/collaborative-editor/components/AIAssistantPanelWrapper.tsx +++ b/assets/js/collaborative-editor/components/AIAssistantPanelWrapper.tsx @@ -61,7 +61,7 @@ import { useWorkflowState, } from '../hooks/useWorkflow'; import { useKeyboardShortcut } from '../keyboard'; -import type { JobCodeContext } from '../types/ai-assistant'; +import type { JobCodeContext, Message } from '../types/ai-assistant'; import { Z_INDEX } from '../utils/constants'; import { prepareWorkflowForSerialization, @@ -103,6 +103,9 @@ export function AIAssistantPanelWrapper({ const isPinnedVersion = currentVersion !== undefined && currentVersion !== null; + const { isReadOnly } = useWorkflowReadOnly(); + const isNewWorkflow = useIsNewWorkflow(); + // Track IDE state changes to re-focus chat input when IDE closes const isIDEOpen = params.panel === 'editor'; const [focusTrigger, setFocusTrigger] = useState(0); @@ -128,7 +131,7 @@ export function AIAssistantPanelWrapper({ toggleAIAssistantPanel(); }, 0, - { enabled: !isPinnedVersion && aiAssistantEnabled } + { enabled: !isPinnedVersion && aiAssistantEnabled && !isNewWorkflow } ); const aiStore = useAIStore(); @@ -157,10 +160,7 @@ export function AIAssistantPanelWrapper({ const workflow = useWorkflowState(state => state.workflow); const limits = useLimits(); - // Check readonly state and new workflow status // AI can apply changes if: not readonly OR is a new workflow (being created) - const { isReadOnly } = useWorkflowReadOnly(); - const isNewWorkflow = useIsNewWorkflow(); const canApplyChanges = !isReadOnly || isNewWorkflow; const isWriteDisabled = !canApplyChanges; @@ -548,6 +548,7 @@ export function AIAssistantPanelWrapper({ startApplyingJobCode, doneApplyingJobCode, updateJob, + saveWorkflow, } = useWorkflowActions(); // Get applying state from workflow store for disabling Apply button across all users @@ -559,6 +560,20 @@ export function AIAssistantPanelWrapper({ state => state.applyingJobCodeMessageId ); + const onValidationError = useCallback( + (errorMessage: string) => { + const message: Message = { + id: crypto.randomUUID(), + role: 'assistant', + content: errorMessage, + status: 'error', + inserted_at: new Date().toISOString(), + }; + aiStore._addMessage(message); + }, + [aiStore] + ); + // Hook to handle workflow/job code application logic const { handleApplyWorkflow, handlePreviewJobCode, handleApplyJobCode } = useAIWorkflowApplications({ @@ -573,6 +588,8 @@ export function AIAssistantPanelWrapper({ : null, currentUserId: user?.id, aiMode, + isNewWorkflow, + onValidationError, workflowActions: { importWorkflow, startApplyingWorkflow, @@ -580,6 +597,7 @@ export function AIAssistantPanelWrapper({ startApplyingJobCode, doneApplyingJobCode, updateJob, + saveWorkflow, }, monacoRef, jobs, @@ -683,7 +701,7 @@ export function AIAssistantPanelWrapper({
void; + testId: string; +} + +interface LandingScreenProps { + aiAssistantEnabled: boolean; + onBuildWithAI: (prompt: string) => void; + onBuildFromScratch: () => void; + onBrowseTemplates: () => void; + onImportYAML: () => void; +} + +export function LandingScreen({ + aiAssistantEnabled, + onBuildWithAI, + onBuildFromScratch, + onBrowseTemplates, + onImportYAML, +}: LandingScreenProps) { + const [prompt, setPrompt] = useState(''); + const isValid = prompt.trim().length > 0; + + const handleSubmit = () => { + if (!isValid) return; + onBuildWithAI(prompt); + }; + + return ( +
+
+

+ Where would you like to start today? +

+ + {aiAssistantEnabled && ( +
+
+ + + {/* Custom sparkle — no heroicons equivalent */} + + Recommended + +
+
+