diff --git a/.cursorrules b/.cursorrules index 583c84e653..a8f1e657b2 100644 --- a/.cursorrules +++ b/.cursorrules @@ -8,7 +8,7 @@ ENSURE that you use the logger.info and logger.warn and logger.error instead of ## Comments -You must use TSDOC for comments. Do not use ==== for comments to separate sections. +You must use TSDOC for comments. Do not use ==== for comments to separate sections. Do not leave any comments that are not TSDOC. ## Globals styles diff --git a/apps/sim/.cursorrules b/apps/sim/.cursorrules index 73ae781234..4a3d2c2acb 100644 --- a/apps/sim/.cursorrules +++ b/apps/sim/.cursorrules @@ -428,20 +428,22 @@ setSidebarWidth: (width) => { ### Tailwind Classes 1. **No Inline Styles**: Use Tailwind utility classes exclusively -2. **Dark Mode**: Always include dark mode variants (`dark:bg-[var(--surface-1)]`) -3. **Exact Values**: Use exact values from design system (`text-[14px]`, `h-[25px]`) -4. **clsx for Conditionals**: Use clsx() for conditional classes -5. **Consistent Spacing**: Use spacing tokens (`gap-[8px]`, `px-[14px]`) -6. **Transitions**: Add transitions for interactive states (`transition-colors`) -7. **Prefer px units**: Use arbitrary px values over scale utilities (e.g., `px-[4px]` instead of `px-1`) +2. **Dark Mode**: Include dark mode variants only when the value differs from light mode +3. **No Duplicate Dark Classes**: Never add a `dark:` class when the value is identical to the light mode class (e.g., `text-[var(--text-primary)] dark:text-[var(--text-primary)]` is redundant - just use `text-[var(--text-primary)]`) +4. **Exact Values**: Use exact values from design system (`text-[14px]`, `h-[25px]`) +5. **cn for Conditionals**: Use `cn()` from `@/lib/utils` for conditional classes (wraps clsx + tailwind-merge for conflict resolution) +6. **Consistent Spacing**: Use spacing tokens (`gap-[8px]`, `px-[14px]`) +7. **Transitions**: Add transitions for interactive states (`transition-colors`) +8. **Prefer px units**: Use arbitrary px values over scale utilities (e.g., `px-[4px]` instead of `px-1`) ```typescript +import { cn } from '@/lib/utils' +
``` @@ -620,9 +622,10 @@ Before considering a component/hook complete, verify: ### Styling - [ ] No styles attributes (use className with Tailwind) -- [ ] Dark mode variants included +- [ ] Dark mode variants only when values differ from light mode +- [ ] No duplicate dark: classes with identical values - [ ] Consistent spacing using design tokens -- [ ] clsx for conditional classes +- [ ] cn() for conditional classes ### Accessibility - [ ] Semantic HTML elements @@ -652,6 +655,11 @@ Before considering a component/hook complete, verify: // ❌ Inline styles
+// ❌ Duplicate dark mode classes (same value as light mode) +
+
+
+ // ❌ console.log console.log('Debug info') @@ -690,6 +698,14 @@ export function Component() { // ✅ Tailwind classes
+// ✅ No duplicate dark classes - CSS variables already handle theming +
+
+
+ +// ✅ Only add dark: when values differ between modes +
+ // ✅ Logger logger.info('Debug info', { context }) diff --git a/apps/sim/app/_shell/providers/theme-provider.tsx b/apps/sim/app/_shell/providers/theme-provider.tsx index c1f847bb07..bd18e559e0 100644 --- a/apps/sim/app/_shell/providers/theme-provider.tsx +++ b/apps/sim/app/_shell/providers/theme-provider.tsx @@ -7,25 +7,23 @@ import { ThemeProvider as NextThemesProvider } from 'next-themes' export function ThemeProvider({ children, ...props }: ThemeProviderProps) { const pathname = usePathname() - // Force dark mode for workspace pages and templates - // Force light mode for certain public pages + // Force light mode for public/marketing pages + // Workspace and templates respect user's theme preference from settings const forcedTheme = - pathname.startsWith('/workspace') || pathname.startsWith('/templates') - ? 'dark' - : pathname === '/' || - pathname.startsWith('/login') || - pathname.startsWith('/signup') || - pathname.startsWith('/sso') || - pathname.startsWith('/terms') || - pathname.startsWith('/privacy') || - pathname.startsWith('/invite') || - pathname.startsWith('/verify') || - pathname.startsWith('/careers') || - pathname.startsWith('/changelog') || - pathname.startsWith('/chat') || - pathname.startsWith('/studio') - ? 'light' - : undefined + pathname === '/' || + pathname.startsWith('/login') || + pathname.startsWith('/signup') || + pathname.startsWith('/sso') || + pathname.startsWith('/terms') || + pathname.startsWith('/privacy') || + pathname.startsWith('/invite') || + pathname.startsWith('/verify') || + pathname.startsWith('/careers') || + pathname.startsWith('/changelog') || + pathname.startsWith('/chat') || + pathname.startsWith('/studio') + ? 'light' + : undefined return ( = ({ className = 'w-6 h-6' }) => ( {hasDuration && ( @@ -467,12 +467,12 @@ export function WorkflowDetails({
{statusLabel} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 0738cd6153..30e441f291 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -483,12 +483,12 @@ export default function Logs() {
{statusLabel} diff --git a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx index 250503898b..ef95769080 100644 --- a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx @@ -25,7 +25,12 @@ interface TemplateCardProps { export function TemplateCardSkeleton({ className }: { className?: string }) { return ( -
+
@@ -193,7 +198,10 @@ function TemplateCardInner({ return (
+{blockTypes.length - 3} @@ -273,7 +281,7 @@ function TemplateCardInner({ {author}
) : ( -
+
)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index 35c9dd7e2a..ee49e27508 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -28,13 +28,13 @@ import { ChatMessage, OutputSelect, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components' -import { - useChatBoundarySync, - useChatDrag, - useChatFileUpload, - useChatResize, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks' +import { useChatFileUpload } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks' import { useScrollManagement } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' +import { + useFloatBoundarySync, + useFloatDrag, + useFloatResize, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-float' import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution' import type { BlockLog, ExecutionResult } from '@/executor/types' import { getChatPosition, useChatStore } from '@/stores/chat/store' @@ -312,7 +312,7 @@ export function Chat() { ) // Drag hook - const { handleMouseDown } = useChatDrag({ + const { handleMouseDown } = useFloatDrag({ position: actualPosition, width: chatWidth, height: chatHeight, @@ -320,7 +320,7 @@ export function Chat() { }) // Boundary sync hook - keeps chat within bounds when layout changes - useChatBoundarySync({ + useFloatBoundarySync({ isOpen: isChatOpen, position: actualPosition, width: chatWidth, @@ -334,7 +334,7 @@ export function Chat() { handleMouseMove: handleResizeMouseMove, handleMouseLeave: handleResizeMouseLeave, handleMouseDown: handleResizeMouseDown, - } = useChatResize({ + } = useFloatResize({ position: actualPosition, width: chatWidth, height: chatHeight, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx index cceeb25145..b0c4ba7d61 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx @@ -62,7 +62,7 @@ export function OutputSelect({ valueMode = 'id', disablePopoverPortal = false, align = 'start', - maxHeight = 300, + maxHeight = 200, }: OutputSelectProps) { const [open, setOpen] = useState(false) const [highlightedIndex, setHighlightedIndex] = useState(-1) @@ -420,6 +420,7 @@ export function OutputSelect({ maxHeight={maxHeight} maxWidth={300} minWidth={160} + border disablePortal={disablePopoverPortal} onKeyDown={handleKeyDown} tabIndex={0} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks/index.ts index dc85c4971b..a1b7370b85 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks/index.ts @@ -1,5 +1 @@ -export { useChatBoundarySync } from './use-chat-boundary-sync' -export { useChatDrag } from './use-chat-drag' -export type { ChatFile } from './use-chat-file-upload' -export { useChatFileUpload } from './use-chat-file-upload' -export { useChatResize } from './use-chat-resize' +export { type ChatFile, useChatFileUpload } from './use-chat-file-upload' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx index 2e7e5c328f..570b2edc04 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx @@ -1,8 +1,10 @@ import { memo, useCallback } from 'react' +import clsx from 'clsx' import { Eye, EyeOff } from 'lucide-react' import { Button } from '@/components/emcn' import { createLogger } from '@/lib/logs/console/logger' import { useCopilotStore } from '@/stores/panel/copilot/store' +import { useTerminalStore } from '@/stores/terminal' import { useWorkflowDiffStore } from '@/stores/workflow-diff' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { mergeSubblockState } from '@/stores/workflows/utils' @@ -11,6 +13,7 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store' const logger = createLogger('DiffControls') export const DiffControls = memo(function DiffControls() { + const isTerminalResizing = useTerminalStore((state) => state.isResizing) // Optimized: Single diff store subscription const { isShowingDiff, @@ -312,7 +315,10 @@ export const DiffControls = memo(function DiffControls() { return (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx index 2f49c7ee97..80314ce731 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx @@ -4,7 +4,7 @@ import type { NodeProps } from 'reactflow' import remarkGfm from 'remark-gfm' import { cn } from '@/lib/utils' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' -import { useBlockCore } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' +import { useBlockVisual } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { BLOCK_DIMENSIONS, useBlockDimensions, @@ -76,7 +76,7 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string } return ( {children} @@ -121,9 +121,10 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string } export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps) { const { type, config, name } = data - const { activeWorkflowId, isEnabled, isFocused, handleClick, hasRing, ringStyles } = useBlockCore( - { blockId: id, data } - ) + const { activeWorkflowId, isEnabled, handleClick, hasRing, ringStyles } = useBlockVisual({ + blockId: id, + data, + }) const storedValues = useSubBlockStore( useCallback( (state) => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx index fe40868ed1..a95107029d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx @@ -1,4 +1,5 @@ import { memo, useCallback } from 'react' +import clsx from 'clsx' import { X } from 'lucide-react' import { useParams } from 'next/navigation' import { Button } from '@/components/emcn' @@ -10,6 +11,7 @@ import { openCopilotWithMessage, useNotificationStore, } from '@/stores/notifications' +import { useTerminalStore } from '@/stores/terminal' const logger = createLogger('Notifications') const MAX_VISIBLE_NOTIFICATIONS = 4 @@ -29,6 +31,7 @@ export const Notifications = memo(function Notifications() { const removeNotification = useNotificationStore((state) => state.removeNotification) const clearNotifications = useNotificationStore((state) => state.clearNotifications) const visibleNotifications = notifications.slice(0, MAX_VISIBLE_NOTIFICATIONS) + const isTerminalResizing = useTerminalStore((state) => state.isResizing) /** * Executes a notification action and handles side effects. @@ -95,7 +98,12 @@ export const Notifications = memo(function Notifications() { } return ( -
+
{[...visibleNotifications].reverse().map((notification, index, stacked) => { const depth = stacked.length - index - 1 const xOffset = depth * 3 diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx index 508cf39916..92bc338c8b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx @@ -37,7 +37,7 @@ function ShimmerOverlayText({ label, value, active = false }: ShimmerOverlayText return ( {label} - {value} + {value} {active ? (
) } @@ -264,7 +264,7 @@ const CopilotMessage: FC = memo( {/* Inline Checkpoint Discard Confirmation - shown below input in edit mode */} {showCheckpointDiscardModal && ( -
+

Continue from a previous message?

@@ -311,7 +311,7 @@ const CopilotMessage: FC = memo( onClick={handleMessageClick} onMouseEnter={() => setIsHoveringMessage(true)} onMouseLeave={() => setIsHoveringMessage(false)} - className='group relative w-full cursor-pointer rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[6px] py-[6px] transition-all duration-200 hover:border-[var(--surface-14)] hover:bg-[var(--surface-9)] dark:border-[var(--surface-11)] dark:bg-[var(--surface-9)] dark:hover:border-[var(--surface-13)] dark:hover:bg-[var(--surface-11)]' + className='group relative w-full cursor-pointer rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[6px] py-[6px] transition-all duration-200 hover:border-[var(--surface-14)] hover:bg-[var(--surface-9)] dark:bg-[var(--surface-9)] dark:hover:border-[var(--surface-13)] dark:hover:bg-[var(--surface-11)]' >
= memo( {/* Inline Restore Checkpoint Confirmation */} {showRestoreConfirmation && ( -
+

Revert to checkpoint? This will restore your workflow to the state saved at this checkpoint.{' '} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts index a151aef4d9..28de03e6cb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts @@ -1,6 +1,6 @@ export * from './copilot-message/copilot-message' -export * from './inline-tool-call/inline-tool-call' export * from './plan-mode-section/plan-mode-section' export * from './todo-list/todo-list' +export * from './tool-call/tool-call' export * from './user-input/user-input' export * from './welcome/welcome' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx index b9031f9bbe..ef4a664648 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx @@ -35,9 +35,9 @@ import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId /** * Shared border and background styles */ -const SURFACE_5 = 'bg-[var(--surface-5)] dark:bg-[var(--surface-5)]' -const SURFACE_9 = 'bg-[var(--surface-9)] dark:bg-[var(--surface-9)]' -const BORDER_STRONG = 'border-[var(--border-strong)] dark:border-[var(--border-strong)]' +const SURFACE_5 = 'bg-[var(--surface-5)]' +const SURFACE_9 = 'bg-[var(--surface-9)]' +const BORDER_STRONG = 'border-[var(--border-strong)]' export interface PlanModeSectionProps { /** @@ -184,8 +184,8 @@ const PlanModeSection: React.FC = ({ style={{ height: `${height}px` }} > {/* Header with build/edit/save/clear buttons */} -

- +
+ Workflow Plan
@@ -252,7 +252,7 @@ const PlanModeSection: React.FC = ({ ref={textareaRef} value={editedContent} onChange={(e) => setEditedContent(e.target.value)} - className='h-full min-h-full w-full resize-none border-0 bg-transparent p-0 font-[470] font-season text-[13px] text-[var(--text-primary)] leading-[1.4rem] outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 dark:text-[var(--text-primary)]' + className='h-full min-h-full w-full resize-none border-0 bg-transparent p-0 font-[470] font-season text-[13px] text-[var(--text-primary)] leading-[1.4rem] outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0' placeholder='Enter your workflow plan...' /> ) : ( @@ -265,7 +265,7 @@ const PlanModeSection: React.FC = ({ className={cn( 'group flex h-[20px] w-full cursor-ns-resize items-center justify-center border-t', BORDER_STRONG, - 'transition-colors hover:bg-[var(--surface-9)] dark:hover:bg-[var(--surface-9)]', + 'transition-colors hover:bg-[var(--surface-9)]', isResizing && SURFACE_9 )} onMouseDown={handleResizeStart} @@ -273,7 +273,7 @@ const PlanModeSection: React.FC = ({ aria-orientation='horizontal' aria-label='Resize plan section' > - +
) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/todo-list/todo-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/todo-list/todo-list.tsx index a5a173db62..b50ac6bd85 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/todo-list/todo-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/todo-list/todo-list.tsx @@ -66,7 +66,7 @@ export const TodoList = memo(function TodoList({ return (
@@ -84,19 +84,17 @@ export const TodoList = memo(function TodoList({ )} - - Todo: - - + Todo: + {completedCount}/{totalCount}
{/* Progress bar */} -
+
@@ -122,21 +120,20 @@ export const TodoList = memo(function TodoList({ key={todo.id} className={cn( 'flex items-start gap-2 px-3 py-1.5 transition-colors hover:bg-[var(--surface-9)]/50 dark:hover:bg-[var(--surface-11)]/50', - index !== todos.length - 1 && - 'border-[var(--surface-11)] border-b dark:border-[var(--surface-11)]' + index !== todos.length - 1 && 'border-[var(--surface-11)] border-b' )} > {todo.executing ? (
- +
) : (
{todo.completed ? : null} @@ -146,9 +143,7 @@ export const TodoList = memo(function TodoList({ {todo.content} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/inline-tool-call/inline-tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx similarity index 99% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/inline-tool-call/inline-tool-call.tsx rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx index 270775c386..2682aa8981 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/inline-tool-call/inline-tool-call.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx @@ -12,7 +12,7 @@ import { getEnv } from '@/lib/env' import { CLASS_TOOL_METADATA, useCopilotStore } from '@/stores/panel/copilot/store' import type { CopilotToolCall } from '@/stores/panel/copilot/types' -interface InlineToolCallProps { +interface ToolCallProps { toolCall?: CopilotToolCall toolCallId?: string onStateChange?: (state: any) => void @@ -188,7 +188,7 @@ function ShimmerOverlayText({ {actionVerb ? ( <> {actionVerb} - {remainder} + {remainder} ) : ( {text} @@ -453,11 +453,7 @@ function RunSkipButtons({ ) } -export function InlineToolCall({ - toolCall: toolCallProp, - toolCallId, - onStateChange, -}: InlineToolCallProps) { +export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }: ToolCallProps) { const [, forceUpdate] = useState({}) const liveToolCall = useCopilotStore((s) => toolCallId ? s.toolCallsById[toolCallId] : undefined @@ -869,7 +865,7 @@ export function InlineToolCall({ text={displayName} active={isLoadingState} isSpecial={isSpecial} - className='font-[470] font-season text-[#939393] text-sm dark:text-[#939393]' + className='font-[470] font-season text-[#939393] text-sm' />
{renderPendingDetails()}
{showButtons && ( @@ -895,7 +891,7 @@ export function InlineToolCall({ text={displayName} active={isLoadingState} isSpecial={isSpecial} - className='font-[470] font-season text-[#939393] text-sm dark:text-[#939393]' + className='font-[470] font-season text-[#939393] text-sm' />
{isExpandableTool && expanded &&
{renderPendingDetails()}
} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/mention-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/mention-menu.tsx index 7220bfdc85..d6202f23d0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/mention-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/mention-menu.tsx @@ -552,11 +552,11 @@ export function MentionMenu({ active={index === submenuActiveIndex} > {log.workflowName} - · + · {formatTimestamp(log.createdAt)} - · + · {(log.trigger || 'manual').toLowerCase()} @@ -583,9 +583,7 @@ export function MentionMenu({ {item.label} {item.category === 'logs' && ( <> - - · - + · {formatTimestamp(item.data.createdAt)} @@ -758,15 +756,11 @@ export function MentionMenu({ mentionData.logsList.map((log) => ( insertLogMention(log)}> {log.workflowName} - - · - + · {formatTimestamp(log.createdAt)} - - · - + · {(log.trigger || 'manual').toLowerCase()} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/welcome.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/welcome.tsx index d270281c38..b4c9dc2499 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/welcome.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/welcome.tsx @@ -64,16 +64,14 @@ export function Welcome({ onQuestionClick, mode = 'ask' }: WelcomeProps) { >

{title}

-

- {question} -

+

{question}

))}
{/* Tips */} -

+

Tip: Use @ to reference chats, workflows, knowledge, blocks, or templates

diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx index 96b5404fcd..07df6e8c4c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx @@ -385,8 +385,8 @@ export const Copilot = forwardRef(({ panelWidth }, ref className='flex h-full flex-col overflow-hidden' > {/* Header */} -
-

+
+

{currentChat?.title || 'New Chat'}

@@ -405,7 +405,7 @@ export const Copilot = forwardRef(({ panelWidth }, ref ) : groupedChats.length === 0 ? ( -
+
No chats yet
) : ( @@ -460,7 +460,7 @@ export const Copilot = forwardRef(({ panelWidth }, ref {!isInitialized ? (
-

Loading chat history...

+

Loading copilot

) : ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api-endpoint.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api-endpoint.tsx deleted file mode 100644 index c18800c123..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api-endpoint.tsx +++ /dev/null @@ -1,25 +0,0 @@ -'use client' - -import { Label } from '@/components/emcn' -import { CopyButton } from '@/components/ui/copy-button' - -interface ApiEndpointProps { - endpoint: string - showLabel?: boolean -} - -export function ApiEndpoint({ endpoint, showLabel = true }: ApiEndpointProps) { - return ( -
- {showLabel && ( -
- -
- )} -
-
{endpoint}
- -
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx new file mode 100644 index 0000000000..96225fb3b2 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx @@ -0,0 +1,664 @@ +'use client' + +import { useState } from 'react' +import { Check, Clipboard } from 'lucide-react' +import { + Badge, + Button, + Code, + Label, + Popover, + PopoverContent, + PopoverItem, + PopoverTrigger, + Tooltip, +} from '@/components/emcn' +import { Skeleton } from '@/components/ui' +import { getEnv, isTruthy } from '@/lib/env' +import { OutputSelect } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select' + +interface WorkflowDeploymentInfo { + isDeployed: boolean + deployedAt?: string + apiKey: string + endpoint: string + exampleCommand: string + needsRedeployment: boolean +} + +interface ApiDeployProps { + workflowId: string | null + deploymentInfo: WorkflowDeploymentInfo | null + isLoading: boolean + needsRedeployment: boolean + apiDeployError: string | null + getInputFormatExample: (includeStreaming?: boolean) => string + selectedStreamingOutputs: string[] + onSelectedStreamingOutputsChange: (outputs: string[]) => void +} + +type AsyncExampleType = 'execute' | 'status' | 'rate-limits' +type CodeLanguage = 'curl' | 'python' | 'javascript' | 'typescript' + +type CopiedState = { + endpoint: boolean // @remark: not used + sync: boolean + stream: boolean + async: boolean +} + +const LANGUAGE_LABELS: Record = { + curl: 'cURL', + python: 'Python', + javascript: 'JavaScript', + typescript: 'TypeScript', +} + +const LANGUAGE_SYNTAX: Record = { + curl: 'javascript', + python: 'python', + javascript: 'javascript', + typescript: 'javascript', +} + +export function ApiDeploy({ + workflowId, + deploymentInfo, + isLoading, + needsRedeployment, + apiDeployError, + getInputFormatExample, + selectedStreamingOutputs, + onSelectedStreamingOutputsChange, +}: ApiDeployProps) { + const [asyncExampleType, setAsyncExampleType] = useState('execute') + const [language, setLanguage] = useState('curl') + const [copied, setCopied] = useState({ + endpoint: false, // @remark: not used + sync: false, + stream: false, + async: false, + }) + + const isAsyncEnabled = isTruthy(getEnv('NEXT_PUBLIC_TRIGGER_DEV_ENABLED')) + const info = deploymentInfo ? { ...deploymentInfo, needsRedeployment } : null + + const getBaseEndpoint = () => { + if (!info) return '' + return info.endpoint.replace(info.apiKey, '$SIM_API_KEY') + } + + const getPayloadObject = (): Record => { + const inputExample = getInputFormatExample ? getInputFormatExample(false) : '' + const match = inputExample.match(/-d\s*'([\s\S]*)'/) + if (match) { + try { + return JSON.parse(match[1]) as Record + } catch { + return { input: 'your data here' } + } + } + return { input: 'your data here' } + } + + const getStreamPayloadObject = (): Record => { + const payload: Record = { ...getPayloadObject(), stream: true } + if (selectedStreamingOutputs && selectedStreamingOutputs.length > 0) { + payload.selectedOutputs = selectedStreamingOutputs + } + return payload + } + + const getSyncCommand = (): string => { + if (!info) return '' + const endpoint = getBaseEndpoint() + const payload = getPayloadObject() + + switch (language) { + case 'curl': + return `curl -X POST \\ + -H "X-API-Key: $SIM_API_KEY" \\ + -H "Content-Type: application/json" \\ + -d '${JSON.stringify(payload)}' \\ + ${endpoint}` + + case 'python': + return `import requests + +response = requests.post( + "${endpoint}", + headers={ + "X-API-Key": SIM_API_KEY, + "Content-Type": "application/json" + }, + json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')} +) + +print(response.json())` + + case 'javascript': + return `const response = await fetch("${endpoint}", { + method: "POST", + headers: { + "X-API-Key": SIM_API_KEY, + "Content-Type": "application/json" + }, + body: JSON.stringify(${JSON.stringify(payload)}) +}); + +const data = await response.json(); +console.log(data);` + + case 'typescript': + return `const response = await fetch("${endpoint}", { + method: "POST", + headers: { + "X-API-Key": SIM_API_KEY, + "Content-Type": "application/json" + }, + body: JSON.stringify(${JSON.stringify(payload)}) +}); + +const data: Record = await response.json(); +console.log(data);` + + default: + return '' + } + } + + const getStreamCommand = (): string => { + if (!info) return '' + const endpoint = getBaseEndpoint() + const payload = getStreamPayloadObject() + + switch (language) { + case 'curl': + return `curl -X POST \\ + -H "X-API-Key: $SIM_API_KEY" \\ + -H "Content-Type: application/json" \\ + -d '${JSON.stringify(payload)}' \\ + ${endpoint}` + + case 'python': + return `import requests + +response = requests.post( + "${endpoint}", + headers={ + "X-API-Key": SIM_API_KEY, + "Content-Type": "application/json" + }, + json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')}, + stream=True +) + +for line in response.iter_lines(): + if line: + print(line.decode("utf-8"))` + + case 'javascript': + return `const response = await fetch("${endpoint}", { + method: "POST", + headers: { + "X-API-Key": SIM_API_KEY, + "Content-Type": "application/json" + }, + body: JSON.stringify(${JSON.stringify(payload)}) +}); + +const reader = response.body.getReader(); +const decoder = new TextDecoder(); + +while (true) { + const { done, value } = await reader.read(); + if (done) break; + console.log(decoder.decode(value)); +}` + + case 'typescript': + return `const response = await fetch("${endpoint}", { + method: "POST", + headers: { + "X-API-Key": SIM_API_KEY, + "Content-Type": "application/json" + }, + body: JSON.stringify(${JSON.stringify(payload)}) +}); + +const reader = response.body!.getReader(); +const decoder = new TextDecoder(); + +while (true) { + const { done, value } = await reader.read(); + if (done) break; + console.log(decoder.decode(value)); +}` + + default: + return '' + } + } + + const getAsyncCommand = (): string => { + if (!info) return '' + const endpoint = getBaseEndpoint() + const baseUrl = endpoint.split('/api/workflows/')[0] + const payload = getPayloadObject() + + switch (asyncExampleType) { + case 'execute': + switch (language) { + case 'curl': + return `curl -X POST \\ + -H "X-API-Key: $SIM_API_KEY" \\ + -H "Content-Type: application/json" \\ + -H "X-Execution-Mode: async" \\ + -d '${JSON.stringify(payload)}' \\ + ${endpoint}` + + case 'python': + return `import requests + +response = requests.post( + "${endpoint}", + headers={ + "X-API-Key": SIM_API_KEY, + "Content-Type": "application/json", + "X-Execution-Mode": "async" + }, + json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')} +) + +job = response.json() +print(job) # Contains job_id for status checking` + + case 'javascript': + return `const response = await fetch("${endpoint}", { + method: "POST", + headers: { + "X-API-Key": SIM_API_KEY, + "Content-Type": "application/json", + "X-Execution-Mode": "async" + }, + body: JSON.stringify(${JSON.stringify(payload)}) +}); + +const job = await response.json(); +console.log(job); // Contains job_id for status checking` + + case 'typescript': + return `const response = await fetch("${endpoint}", { + method: "POST", + headers: { + "X-API-Key": SIM_API_KEY, + "Content-Type": "application/json", + "X-Execution-Mode": "async" + }, + body: JSON.stringify(${JSON.stringify(payload)}) +}); + +const job: { job_id: string } = await response.json(); +console.log(job); // Contains job_id for status checking` + + default: + return '' + } + + case 'status': + switch (language) { + case 'curl': + return `curl -H "X-API-Key: $SIM_API_KEY" \\ + ${baseUrl}/api/jobs/JOB_ID_FROM_EXECUTION` + + case 'python': + return `import requests + +response = requests.get( + "${baseUrl}/api/jobs/JOB_ID_FROM_EXECUTION", + headers={"X-API-Key": SIM_API_KEY} +) + +status = response.json() +print(status)` + + case 'javascript': + return `const response = await fetch( + "${baseUrl}/api/jobs/JOB_ID_FROM_EXECUTION", + { + headers: { "X-API-Key": SIM_API_KEY } + } +); + +const status = await response.json(); +console.log(status);` + + case 'typescript': + return `const response = await fetch( + "${baseUrl}/api/jobs/JOB_ID_FROM_EXECUTION", + { + headers: { "X-API-Key": SIM_API_KEY } + } +); + +const status: Record = await response.json(); +console.log(status);` + + default: + return '' + } + + case 'rate-limits': + switch (language) { + case 'curl': + return `curl -H "X-API-Key: $SIM_API_KEY" \\ + ${baseUrl}/api/users/me/usage-limits` + + case 'python': + return `import requests + +response = requests.get( + "${baseUrl}/api/users/me/usage-limits", + headers={"X-API-Key": SIM_API_KEY} +) + +limits = response.json() +print(limits)` + + case 'javascript': + return `const response = await fetch( + "${baseUrl}/api/users/me/usage-limits", + { + headers: { "X-API-Key": SIM_API_KEY } + } +); + +const limits = await response.json(); +console.log(limits);` + + case 'typescript': + return `const response = await fetch( + "${baseUrl}/api/users/me/usage-limits", + { + headers: { "X-API-Key": SIM_API_KEY } + } +); + +const limits: Record = await response.json(); +console.log(limits);` + + default: + return '' + } + + default: + return '' + } + } + + const getAsyncExampleTitle = () => { + switch (asyncExampleType) { + case 'execute': + return 'Execute Job' + case 'status': + return 'Check Status' + case 'rate-limits': + return 'Rate Limits' + default: + return 'Execute Job' + } + } + + const handleCopy = (key: keyof CopiedState, value: string) => { + navigator.clipboard.writeText(value) + setCopied((prev) => ({ ...prev, [key]: true })) + setTimeout(() => setCopied((prev) => ({ ...prev, [key]: false })), 2000) + } + + if (isLoading || !info) { + return ( +
+ {apiDeployError && ( +
+
API Deployment Error
+
{apiDeployError}
+
+ )} +
+ + +
+
+ + +
+
+ + +
+
+ ) + } + + return ( +
+ {apiDeployError && ( +
+
API Deployment Error
+
{apiDeployError}
+
+ )} + + {/*
+
+ + + + + + + {copied.endpoint ? 'Copied' : 'Copy'} + + +
+ +
*/} + +
+
+ +
+
+ {(Object.keys(LANGUAGE_LABELS) as CodeLanguage[]).map((lang, index, arr) => ( + + ))} +
+
+ +
+
+ + + + + + + {copied.sync ? 'Copied' : 'Copy'} + + +
+ +
+ +
+
+ +
+ + + + + + {copied.stream ? 'Copied' : 'Copy'} + + + +
+
+ +
+ + {isAsyncEnabled && ( +
+
+ +
+ + + + + + {copied.async ? 'Copied' : 'Copy'} + + + + +
+ + + {getAsyncExampleTitle()} + + +
+
+ + setAsyncExampleType('execute')} + > + Execute Job + + setAsyncExampleType('status')} + > + Check Status + + setAsyncExampleType('rate-limits')} + > + Rate Limits + + +
+
+
+ +
+ )} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/auth-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/auth-selector.tsx deleted file mode 100644 index 1bd5d72c26..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/auth-selector.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import { useState } from 'react' -import { Check, Copy, Eye, EyeOff, Plus, RefreshCw } from 'lucide-react' -import { Button, Input, Label } from '@/components/emcn' -import { Trash } from '@/components/emcn/icons/trash' -import { Card, CardContent } from '@/components/ui' -import { getEnv, isTruthy } from '@/lib/env' -import { cn, generatePassword } from '@/lib/utils' -import type { AuthType } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/hooks/use-chat-form' - -interface AuthSelectorProps { - authType: AuthType - password: string - emails: string[] - onAuthTypeChange: (type: AuthType) => void - onPasswordChange: (password: string) => void - onEmailsChange: (emails: string[]) => void - disabled?: boolean - isExistingChat?: boolean - error?: string -} - -export function AuthSelector({ - authType, - password, - emails, - onAuthTypeChange, - onPasswordChange, - onEmailsChange, - disabled = false, - isExistingChat = false, - error, -}: AuthSelectorProps) { - const [showPassword, setShowPassword] = useState(false) - const [newEmail, setNewEmail] = useState('') - const [emailError, setEmailError] = useState('') - const [copySuccess, setCopySuccess] = useState(false) - - const handleGeneratePassword = () => { - const password = generatePassword(24) - onPasswordChange(password) - } - - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text) - setCopySuccess(true) - setTimeout(() => setCopySuccess(false), 2000) - } - - const handleAddEmail = () => { - if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail) && !newEmail.startsWith('@')) { - setEmailError('Please enter a valid email or domain (e.g., user@example.com or @example.com)') - return - } - - if (emails.includes(newEmail)) { - setEmailError('This email or domain is already in the list') - return - } - - onEmailsChange([...emails, newEmail]) - setNewEmail('') - setEmailError('') - } - - const handleRemoveEmail = (email: string) => { - onEmailsChange(emails.filter((e) => e !== email)) - } - - const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) - const authOptions = ssoEnabled - ? (['public', 'password', 'email', 'sso'] as const) - : (['public', 'password', 'email'] as const) - - return ( -
- - - {/* Auth Type Selection */} -
- {authOptions.map((type) => ( - - -
- - {/* Auth Settings */} - {authType === 'password' && ( - - -

- Password Settings -

- - {isExistingChat && !password && ( -
-
- Password set -
- Current password is securely stored -
- )} - -
- onPasswordChange(e.target.value)} - disabled={disabled} - className='pr-28' - required={!isExistingChat} - autoComplete='new-password' - /> -
- - - -
-
- -

- {isExistingChat - ? 'Leaving this empty will keep the current password. Enter a new password to change it.' - : 'This password will be required to access your chat.'} -

-
-
- )} - - {(authType === 'email' || authType === 'sso') && ( - - -

- {authType === 'email' ? 'Email Access Settings' : 'SSO Access Settings'} -

- -
- setNewEmail(e.target.value)} - disabled={disabled} - className='flex-1' - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault() - handleAddEmail() - } - }} - /> - -
- - {emailError &&

{emailError}

} - - {emails.length > 0 && ( -
-
    - {emails.map((email) => ( -
  • -
    - {email} - -
    -
  • - ))} -
-
- )} - -

- {authType === 'email' - ? 'Add specific emails or entire domains (@example.com)' - : 'Add specific emails or entire domains (@example.com) that can access via SSO'} -

-
-
- )} - - {authType === 'public' && ( - - -

- Public Access Settings -

-

- This chat will be publicly accessible to anyone with the link. -

-
-
- )} - - {error &&

{error}

} -
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat-deploy.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat-deploy.tsx deleted file mode 100644 index 1ed663db4c..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat-deploy.tsx +++ /dev/null @@ -1,486 +0,0 @@ -'use client' - -import { useEffect, useRef, useState } from 'react' -import { AlertTriangle, Loader2 } from 'lucide-react' -import { - Button, - Input, - Label, - Modal, - ModalContent, - ModalDescription, - ModalFooter, - ModalHeader, - ModalTitle, - Textarea, -} from '@/components/emcn' -import { Alert, AlertDescription, Skeleton } from '@/components/ui' -import { createLogger } from '@/lib/logs/console/logger' -import { getEmailDomain } from '@/lib/urls/utils' -import { OutputSelect } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select' -import { AuthSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/auth-selector' -import { IdentifierInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/identifier-input' -import { SuccessView } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/success-view' -import { useChatDeployment } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/hooks/use-chat-deployment' -import { useChatForm } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/hooks/use-chat-form' - -const logger = createLogger('ChatDeploy') - -interface ChatDeployProps { - workflowId: string - deploymentInfo: { - apiKey: string - } | null - onChatExistsChange?: (exists: boolean) => void - chatSubmitting: boolean - setChatSubmitting: (submitting: boolean) => void - onValidationChange?: (isValid: boolean) => void - showDeleteConfirmation?: boolean - setShowDeleteConfirmation?: (show: boolean) => void - onDeploymentComplete?: () => void - onDeployed?: () => void - onUndeploy?: () => Promise - onVersionActivated?: () => void -} - -interface ExistingChat { - id: string - identifier: string - title: string - description: string - authType: 'public' | 'password' | 'email' - allowedEmails: string[] - outputConfigs: Array<{ blockId: string; path: string }> - customizations?: { - welcomeMessage?: string - } - isActive: boolean -} - -export function ChatDeploy({ - workflowId, - deploymentInfo, - onChatExistsChange, - chatSubmitting, - setChatSubmitting, - onValidationChange, - showDeleteConfirmation: externalShowDeleteConfirmation, - setShowDeleteConfirmation: externalSetShowDeleteConfirmation, - onDeploymentComplete, - onDeployed, - onUndeploy, - onVersionActivated, -}: ChatDeployProps) { - const [isLoading, setIsLoading] = useState(false) - const [existingChat, setExistingChat] = useState(null) - const [imageUrl, setImageUrl] = useState(null) - const [imageUploadError, setImageUploadError] = useState(null) - const [isDeleting, setIsDeleting] = useState(false) - const [isImageUploading, setIsImageUploading] = useState(false) - const [internalShowDeleteConfirmation, setInternalShowDeleteConfirmation] = useState(false) - const [showSuccessView, setShowSuccessView] = useState(false) - - // Use external state for delete confirmation if provided - const showDeleteConfirmation = - externalShowDeleteConfirmation !== undefined - ? externalShowDeleteConfirmation - : internalShowDeleteConfirmation - - const setShowDeleteConfirmation = - externalSetShowDeleteConfirmation || setInternalShowDeleteConfirmation - - const { formData, errors, updateField, setError, validateForm, setFormData } = useChatForm() - const { deployedUrl, deployChat } = useChatDeployment() - const formRef = useRef(null) - const [isIdentifierValid, setIsIdentifierValid] = useState(false) - - const isFormValid = - isIdentifierValid && - Boolean(formData.title.trim()) && - formData.selectedOutputBlocks.length > 0 && - (formData.authType !== 'password' || - Boolean(formData.password.trim()) || - Boolean(existingChat)) && - ((formData.authType !== 'email' && formData.authType !== 'sso') || formData.emails.length > 0) - - useEffect(() => { - onValidationChange?.(isFormValid) - }, [isFormValid, onValidationChange]) - - useEffect(() => { - if (workflowId) { - fetchExistingChat() - } - }, [workflowId]) - - const fetchExistingChat = async () => { - try { - setIsLoading(true) - const response = await fetch(`/api/workflows/${workflowId}/chat/status`) - - if (response.ok) { - const data = await response.json() - - if (data.isDeployed && data.deployment) { - const detailResponse = await fetch(`/api/chat/manage/${data.deployment.id}`) - - if (detailResponse.ok) { - const chatDetail = await detailResponse.json() - setExistingChat(chatDetail) - - setFormData({ - identifier: chatDetail.identifier || '', - title: chatDetail.title || '', - description: chatDetail.description || '', - authType: chatDetail.authType || 'public', - password: '', - emails: Array.isArray(chatDetail.allowedEmails) ? [...chatDetail.allowedEmails] : [], - welcomeMessage: - chatDetail.customizations?.welcomeMessage || 'Hi there! How can I help you today?', - selectedOutputBlocks: Array.isArray(chatDetail.outputConfigs) - ? chatDetail.outputConfigs.map( - (config: { blockId: string; path: string }) => - `${config.blockId}_${config.path}` - ) - : [], - }) - - if (chatDetail.customizations?.imageUrl) { - setImageUrl(chatDetail.customizations.imageUrl) - } - setImageUploadError(null) - - onChatExistsChange?.(true) - } - } else { - setExistingChat(null) - setImageUrl(null) - setImageUploadError(null) - onChatExistsChange?.(false) - } - } - } catch (error) { - logger.error('Error fetching chat status:', error) - } finally { - setIsLoading(false) - } - } - - const handleSubmit = async (e?: React.FormEvent) => { - if (e) e.preventDefault() - - if (chatSubmitting) return - - setChatSubmitting(true) - - try { - if (!validateForm()) { - setChatSubmitting(false) - return - } - - if (!isIdentifierValid && formData.identifier !== existingChat?.identifier) { - setError('identifier', 'Please wait for identifier validation to complete') - setChatSubmitting(false) - return - } - - await deployChat(workflowId, formData, null, existingChat?.id, imageUrl) - - onChatExistsChange?.(true) - setShowSuccessView(true) - onDeployed?.() - onVersionActivated?.() - - await fetchExistingChat() - } catch (error: any) { - if (error.message?.includes('identifier')) { - setError('identifier', error.message) - } else { - setError('general', error.message) - } - } finally { - setChatSubmitting(false) - } - } - - const handleDelete = async () => { - if (!existingChat || !existingChat.id) return - - try { - setIsDeleting(true) - - const response = await fetch(`/api/chat/manage/${existingChat.id}`, { - method: 'DELETE', - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Failed to delete chat') - } - - setExistingChat(null) - setImageUrl(null) - setImageUploadError(null) - onChatExistsChange?.(false) - - onDeploymentComplete?.() - } catch (error: any) { - logger.error('Failed to delete chat:', error) - setError('general', error.message || 'An unexpected error occurred while deleting') - } finally { - setIsDeleting(false) - setShowDeleteConfirmation(false) - } - } - - if (isLoading) { - return - } - - if (deployedUrl && showSuccessView) { - return ( - <> -
- setShowDeleteConfirmation(true)} - onUpdate={() => setShowSuccessView(false)} - /> -
- - {/* Delete Confirmation Dialog */} - - - - Delete Chat? - - This will delete your chat deployment at "{getEmailDomain()}/chat/ - {existingChat?.identifier}". All users will lose access to the chat interface. You - can recreate this chat deployment at any time. - - - - - - - - - - ) - } - - return ( - <> -
- {errors.general && ( - - - {errors.general} - - )} - -
- updateField('identifier', value)} - originalIdentifier={existingChat?.identifier || undefined} - disabled={chatSubmitting} - onValidationChange={setIsIdentifierValid} - isEditingExisting={!!existingChat} - /> - -
- - updateField('title', e.target.value)} - required - disabled={chatSubmitting} - /> - {errors.title &&

{errors.title}

} -
-
- -