From e67bfab6b50de903ed8e58b3741e7f5eb92c8c5b Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Tue, 5 May 2026 11:28:14 -0700 Subject: [PATCH 1/2] improvement(platform): squashed 6-commit branch onto staging Combined work across the platform branch: - removed product tour - consolidated sidebar header and simplified workspace menu - removed loading skeletons, simplified settings, refreshed icons - pruned CLAUDE.md - removed home templates - home credits + suggested actions, integrations route, sidebar polish --- CLAUDE.md | 519 ++---------- .../(landing)/blog/[slug]/share-button.tsx | 4 +- .../landing-preview-logs.tsx | 5 +- .../integrations/(shell)/[slug]/page.tsx | 2 +- .../integrations/data/templates.ts} | 0 apps/sim/app/_styles/globals.css | 6 +- apps/sim/app/chat/components/input/input.tsx | 2 +- .../message/components/file-download.tsx | 8 +- .../app/chat/components/message/message.tsx | 6 +- apps/sim/app/playground/page.tsx | 2 - apps/sim/app/templates/[id]/template.tsx | 4 +- .../components/conversation-list-item.tsx | 23 - .../[workspaceId]/components/index.ts | 1 + .../message-actions/message-actions.tsx | 6 +- .../components/product-tour/index.ts | 2 - .../components/product-tour/nav-tour-steps.ts | 76 -- .../components/product-tour/product-tour.tsx | 60 -- .../components/product-tour/tour-shared.tsx | 163 ---- .../components/product-tour/use-tour.ts | 236 ------ .../product-tour/workflow-tour-steps.ts | 56 -- .../components/product-tour/workflow-tour.tsx | 59 -- .../components/task-status-dot.tsx | 61 ++ .../file-viewer/editor-context-menu.tsx | 6 +- .../workspace/[workspaceId]/files/files.tsx | 7 +- .../components/credits-chip/credits-chip.tsx | 34 + .../home/components/credits-chip/index.ts | 1 + .../[workspaceId]/home/components/index.ts | 3 +- .../mothership-chat-skeleton.tsx | 2 +- .../mothership-chat/mothership-chat.tsx | 4 +- .../resource-content/resource-content.tsx | 7 +- .../components/suggested-actions/index.ts | 1 + .../suggested-actions/suggested-actions.tsx | 284 +++++++ .../home/components/template-prompts/index.ts | 3 - .../template-prompts/template-prompts.tsx | 391 --------- .../home/components/user-input/user-input.tsx | 2 +- .../app/workspace/[workspaceId]/home/home.tsx | 46 +- .../integrations.tsx} | 722 ++++++++-------- .../[workspaceId]/integrations/layout.tsx | 3 + .../[workspaceId]/integrations/page.tsx | 8 + .../chunk-context-menu/chunk-context-menu.tsx | 4 +- .../knowledge-base-context-menu.tsx | 4 +- .../app/workspace/[workspaceId]/layout.tsx | 2 - .../components/snapshot-context-menu.tsx | 4 +- .../execution-snapshot/execution-snapshot.tsx | 4 +- .../file-download/file-download.tsx | 7 +- .../components/trace-spans/trace-spans.tsx | 4 +- .../components/trace-view/trace-view.tsx | 4 +- .../components/log-details/log-details.tsx | 7 +- .../log-row-context-menu.tsx | 4 +- .../app/workspace/[workspaceId]/logs/logs.tsx | 4 +- .../settings/[section]/loading.tsx | 15 - .../[workspaceId]/settings/[section]/page.tsx | 1 - .../settings/[section]/settings.tsx | 211 ++--- .../components/admin/admin-skeleton.tsx | 72 -- .../settings/components/admin/admin.tsx | 10 +- .../components/api-keys/api-key-skeleton.tsx | 56 -- .../settings/components/api-keys/api-keys.tsx | 31 +- .../components/byok/byok-skeleton.tsx | 45 - .../settings/components/byok/byok.tsx | 9 +- .../components/copilot/copilot-skeleton.tsx | 14 - .../settings/components/copilot/copilot.tsx | 9 +- .../credential-sets-skeleton.tsx | 41 - .../credential-sets/credential-sets.tsx | 36 +- .../custom-tools/custom-tool-skeleton.tsx | 38 - .../components/custom-tools/custom-tools.tsx | 9 +- .../components/general/general-skeleton.tsx | 63 -- .../settings/components/general/general.tsx | 3 +- .../components/inbox/inbox-settings-tab.tsx | 7 +- .../components/inbox/inbox-skeleton.tsx | 84 -- .../components/inbox/inbox-task-list.tsx | 9 +- .../settings/components/inbox/inbox.tsx | 3 +- .../integrations/credential-skeleton.tsx | 22 - .../integrations/integrations-skeleton.tsx | 23 - .../components/integrations/integrations.tsx | 9 - .../components/mcp/components/index.ts | 1 - .../mcp-server-skeleton.tsx | 23 - .../settings/components/mcp/mcp-skeleton.tsx | 21 - .../settings/components/mcp/mcp.tsx | 10 +- .../components/mothership/mothership.tsx | 44 +- .../deleted-item-skeleton.tsx | 17 - .../recently-deleted-skeleton.tsx | 33 - .../recently-deleted/recently-deleted.tsx | 9 +- .../components/secrets/secrets-manager.tsx | 33 +- .../components/secrets/secrets-skeleton.tsx | 49 -- .../components/skills/skill-skeleton.tsx | 14 - .../settings/components/skills/skills.tsx | 9 +- .../components/subscription/subscription.tsx | 57 +- .../team-seats-overview.tsx | 27 +- .../team-management/team-management.tsx | 64 +- .../workflow-mcp-servers-skeleton.tsx | 6 - .../workflow-mcp-servers.tsx | 19 +- .../[workspaceId]/settings/navigation.ts | 3 - .../table-context-menu/table-context-menu.tsx | 4 +- .../components/action-bar/action-bar.tsx | 4 +- .../w/[workflowId]/components/chat/chat.tsx | 15 +- .../components/command-list/command-list.tsx | 1 - .../sub-block/components/code/code.tsx | 5 +- .../w/[workflowId]/components/panel/panel.tsx | 12 +- .../components/output-panel/output-panel.tsx | 8 +- .../components/terminal/terminal.tsx | 18 +- .../workflow-controls/workflow-controls.tsx | 3 +- .../[workspaceId]/w/[workflowId]/layout.tsx | 2 - .../[workspaceId]/w/[workflowId]/workflow.tsx | 6 +- .../collapsed-sidebar-menu.tsx | 243 ++---- .../nav-item-context-menu.tsx | 4 +- .../search-modal/components/command-items.tsx | 16 +- .../components/search-modal/search-modal.tsx | 99 ++- .../sidebar/components/search-modal/utils.ts | 6 +- .../settings-sidebar/settings-sidebar.tsx | 214 ++--- .../components/context-menu/context-menu.tsx | 50 +- .../components/folder-item/folder-item.tsx | 4 +- .../workflow-item/workflow-item.tsx | 4 +- .../workflow-list/workflow-list.tsx | 4 +- .../workspace-header/workspace-header.tsx | 800 ++++++++---------- .../w/components/sidebar/constants.ts | 24 + .../sidebar/hooks/use-workspace-management.ts | 2 +- .../w/components/sidebar/sidebar.tsx | 475 +++++------ .../w/components/sidebar/utils.ts | 13 +- .../emcn/components/button/button.tsx | 1 + .../emcn/components/code/copy-code-button.tsx | 6 +- .../dropdown-menu/dropdown-menu.tsx | 131 ++- apps/sim/components/emcn/components/index.ts | 8 +- .../emcn/components/popover/popover.tsx | 10 +- .../secret-reveal/secret-reveal.tsx | 4 +- .../components/tour-tooltip/tour-tooltip.tsx | 229 ----- .../emcn/icons/animate/copy.module.css | 74 -- apps/sim/components/emcn/icons/arrow-down.tsx | 2 +- apps/sim/components/emcn/icons/arrow-left.tsx | 2 +- .../sim/components/emcn/icons/arrow-right.tsx | 2 +- .../components/emcn/icons/arrow-up-down.tsx | 2 +- apps/sim/components/emcn/icons/arrow-up.tsx | 2 +- apps/sim/components/emcn/icons/asterisk.tsx | 2 +- apps/sim/components/emcn/icons/bell.tsx | 2 +- apps/sim/components/emcn/icons/book-open.tsx | 2 +- apps/sim/components/emcn/icons/bug.tsx | 2 +- apps/sim/components/emcn/icons/calendar.tsx | 2 +- apps/sim/components/emcn/icons/check.tsx | 2 +- .../components/emcn/icons/chevron-down.tsx | 2 +- .../components/emcn/icons/clipboard-list.tsx | 2 +- apps/sim/components/emcn/icons/clock.tsx | 26 + apps/sim/components/emcn/icons/columns2.tsx | 2 +- apps/sim/components/emcn/icons/columns3.tsx | 2 +- apps/sim/components/emcn/icons/copy.tsx | 52 -- apps/sim/components/emcn/icons/credit.tsx | 26 + apps/sim/components/emcn/icons/cursor.tsx | 2 +- apps/sim/components/emcn/icons/database-x.tsx | 2 +- apps/sim/components/emcn/icons/database.tsx | 2 +- apps/sim/components/emcn/icons/download.tsx | 2 +- apps/sim/components/emcn/icons/duplicate.tsx | 18 +- apps/sim/components/emcn/icons/expand.tsx | 8 +- apps/sim/components/emcn/icons/eye.tsx | 22 +- apps/sim/components/emcn/icons/file-x.tsx | 2 +- apps/sim/components/emcn/icons/file.tsx | 2 +- apps/sim/components/emcn/icons/files.tsx | 27 + .../sim/components/emcn/icons/fingerprint.tsx | 2 +- .../sim/components/emcn/icons/folder-plus.tsx | 6 +- apps/sim/components/emcn/icons/hammer.tsx | 2 +- apps/sim/components/emcn/icons/hand.tsx | 8 +- .../sim/components/emcn/icons/help-circle.tsx | 2 +- apps/sim/components/emcn/icons/hex-simple.tsx | 4 +- apps/sim/components/emcn/icons/home.tsx | 2 +- apps/sim/components/emcn/icons/image-up.tsx | 29 + apps/sim/components/emcn/icons/index.ts | 6 +- .../sim/components/emcn/icons/integration.tsx | 9 +- apps/sim/components/emcn/icons/key-square.tsx | 2 +- apps/sim/components/emcn/icons/key.tsx | 2 +- apps/sim/components/emcn/icons/layout.tsx | 2 +- apps/sim/components/emcn/icons/library.tsx | 2 +- apps/sim/components/emcn/icons/link.tsx | 2 +- .../sim/components/emcn/icons/list-filter.tsx | 2 +- apps/sim/components/emcn/icons/loader.tsx | 2 +- apps/sim/components/emcn/icons/lock.tsx | 2 +- apps/sim/components/emcn/icons/log-in.tsx | 2 +- apps/sim/components/emcn/icons/log-out.tsx | 2 +- apps/sim/components/emcn/icons/mail.tsx | 2 +- apps/sim/components/emcn/icons/no-wrap.tsx | 2 +- apps/sim/components/emcn/icons/palette.tsx | 2 +- apps/sim/components/emcn/icons/panel-left.tsx | 2 +- apps/sim/components/emcn/icons/pause.tsx | 2 +- apps/sim/components/emcn/icons/pencil.tsx | 6 +- apps/sim/components/emcn/icons/play.tsx | 2 +- apps/sim/components/emcn/icons/plus.tsx | 2 +- apps/sim/components/emcn/icons/redo.tsx | 4 +- apps/sim/components/emcn/icons/refresh-cw.tsx | 2 +- apps/sim/components/emcn/icons/rocket.tsx | 2 +- apps/sim/components/emcn/icons/rows3.tsx | 2 +- apps/sim/components/emcn/icons/search.tsx | 2 +- apps/sim/components/emcn/icons/send.tsx | 2 +- apps/sim/components/emcn/icons/server.tsx | 2 +- apps/sim/components/emcn/icons/settings.tsx | 2 +- .../components/emcn/icons/shield-check.tsx | 2 +- .../emcn/icons/square-arrow-up-right.tsx | 2 +- apps/sim/components/emcn/icons/table-x.tsx | 2 +- apps/sim/components/emcn/icons/table.tsx | 2 +- apps/sim/components/emcn/icons/tag.tsx | 2 +- apps/sim/components/emcn/icons/task.tsx | 26 + .../sim/components/emcn/icons/thumbs-down.tsx | 2 +- apps/sim/components/emcn/icons/thumbs-up.tsx | 2 +- .../components/emcn/icons/trash-outline.tsx | 2 +- .../components/emcn/icons/type-boolean.tsx | 2 +- apps/sim/components/emcn/icons/type-json.tsx | 2 +- .../sim/components/emcn/icons/type-number.tsx | 2 +- apps/sim/components/emcn/icons/type-text.tsx | 2 +- apps/sim/components/emcn/icons/undo.tsx | 4 +- apps/sim/components/emcn/icons/unlock.tsx | 2 +- apps/sim/components/emcn/icons/upload.tsx | 8 +- apps/sim/components/emcn/icons/user-plus.tsx | 14 +- apps/sim/components/emcn/icons/user.tsx | 2 +- apps/sim/components/emcn/icons/users.tsx | 2 +- apps/sim/components/emcn/icons/workflow-x.tsx | 2 +- apps/sim/components/emcn/icons/wrap.tsx | 2 +- apps/sim/components/emcn/icons/wrench.tsx | 2 +- apps/sim/components/emcn/icons/x.tsx | 2 +- apps/sim/components/emcn/icons/zoom-in.tsx | 8 +- apps/sim/components/emcn/icons/zoom-out.tsx | 6 +- apps/sim/components/icons.tsx | 16 +- .../components/access-control.tsx | 39 +- .../components/audit-logs-skeleton.tsx | 27 - .../ee/audit-logs/components/audit-logs.tsx | 17 +- apps/sim/ee/sso/components/sso-settings.tsx | 23 +- apps/sim/hooks/queries/tasks.ts | 37 +- apps/sim/hooks/queries/workspace.ts | 2 +- apps/sim/lib/posthog/events.ts | 13 - apps/sim/lib/workflows/colors.ts | 75 +- apps/sim/package.json | 1 - apps/sim/tailwind.config.ts | 12 +- bun.lock | 35 +- 227 files changed, 2312 insertions(+), 5069 deletions(-) rename apps/sim/app/{workspace/[workspaceId]/home/components/template-prompts/consts.ts => (landing)/integrations/data/templates.ts} (100%) delete mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/components/task-status-dot.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/credits-chip/credits-chip.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/credits-chip/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/template-prompts/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/template-prompts/template-prompts.tsx rename apps/sim/app/workspace/[workspaceId]/{settings/components/integrations/integrations-manager.tsx => integrations/integrations.tsx} (72%) create mode 100644 apps/sim/app/workspace/[workspaceId]/integrations/layout.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/integrations/page.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/[section]/loading.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tool-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/general/general-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/integrations/credential-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-skeleton/mcp-server-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/deleted-item-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/secrets/secrets-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/skills/skill-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers-skeleton.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/constants.ts delete mode 100644 apps/sim/components/emcn/components/tour-tooltip/tour-tooltip.tsx delete mode 100644 apps/sim/components/emcn/icons/animate/copy.module.css create mode 100644 apps/sim/components/emcn/icons/clock.tsx delete mode 100644 apps/sim/components/emcn/icons/copy.tsx create mode 100644 apps/sim/components/emcn/icons/credit.tsx create mode 100644 apps/sim/components/emcn/icons/files.tsx create mode 100644 apps/sim/components/emcn/icons/image-up.tsx create mode 100644 apps/sim/components/emcn/icons/task.tsx delete mode 100644 apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx diff --git a/CLAUDE.md b/CLAUDE.md index 4b27c318579..9e558fff080 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,287 +1,65 @@ # Sim Development Guidelines -You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient. - ## Global Standards -- **Linting / Audit**: `bun run check:api-validation` must pass on PRs. Do not introduce route-local boundary Zod schemas, direct route Zod imports, or ad-hoc client wire types — see "API Contracts" and "API Route Pattern" below -- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`. Inside API routes wrapped with `withRouteHandler`, loggers automatically include the request ID — no manual `withMetadata({ requestId })` needed -- **API Route Handlers**: All API route handlers (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) must be wrapped with `withRouteHandler` from `@/lib/core/utils/with-route-handler`. This provides request ID tracking, automatic error logging for 4xx/5xx responses, and unhandled error catching. See "API Route Pattern" section below -- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments -- **Styling**: Never update global styles. Keep all styling local to components -- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@sim/utils/id` -- **Common Utilities**: Use shared helpers from `@sim/utils` instead of inline implementations. `sleep(ms)` from `@sim/utils/helpers` for delays, `toError(e)` from `@sim/utils/errors` to normalize caught values. -- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx` - -## Architecture - -### Core Principles - -1. Single Responsibility: Each component, hook, store has one clear purpose -2. Composition Over Complexity: Break down complex logic into smaller pieces -3. Type Safety First: TypeScript interfaces for all props, state, return types -4. Predictable State: Zustand for global state, useState for UI-only concerns - -### Root Structure - -``` -apps/sim/ -├── app/ # Next.js app router (pages, API routes) -├── blocks/ # Block definitions and registry -├── components/ # Shared UI (emcn/, ui/) -├── executor/ # Workflow execution engine -├── hooks/ # Shared hooks (queries/, selectors/) -├── lib/ # App-wide utilities -├── providers/ # LLM provider integrations -├── stores/ # Zustand stores -├── tools/ # Tool definitions -└── triggers/ # Trigger definitions -``` - -### Naming Conventions - -- Components: PascalCase (`WorkflowList`) -- Hooks: `use` prefix (`useWorkflowOperations`) -- Files: kebab-case (`workflow-list.tsx`) -- Stores: `stores/feature/store.ts` -- Constants: SCREAMING_SNAKE_CASE -- Interfaces: PascalCase with suffix (`WorkflowListProps`) - -## Imports - -**Always use absolute imports.** Never use relative imports. - -```typescript -// ✓ Good -import { useWorkflowStore } from '@/stores/workflows/store' - -// ✗ Bad -import { useWorkflowStore } from '../../../stores/workflows/store' -``` - -Use barrel exports (`index.ts`) when a folder has 3+ exports. Do not re-export from non-barrel files; import directly from the source. - -### Import Order - -1. React/core libraries -2. External libraries -3. UI components (`@/components/emcn`, `@/components/ui`) -4. Utilities (`@/lib/...`) -5. Stores (`@/stores/...`) -6. Feature imports -7. CSS imports - -Use `import type { X }` for type-only imports. +- **Logging**: `createLogger` from `@sim/logger`. Inside `withRouteHandler`, request ID is injected automatically — don't pass it manually +- **API Routes**: Wrap every handler with `withRouteHandler` from `@/lib/core/utils/with-route-handler`. Never export bare `async function GET/POST/...` +- **IDs**: Never `crypto.randomUUID()`, `nanoid`, or `uuid`. Use `generateId()` or `generateShortId()` from `@sim/utils/id` +- **Helpers**: `sleep(ms)` from `@sim/utils/helpers`, `toError(e)` from `@sim/utils/errors`. Don't reimplement +- **Comments**: TSDoc only. No `====` separators, no non-TSDoc comments +- **Styling**: Tailwind only, no inline styles. Use `cn()` from `@/lib/utils`. Never touch global styles +- **Package Manager**: `bun` / `bunx`, never `npm` / `npx` ## TypeScript -1. No `any` - Use proper types or `unknown` with type guards -2. Always define props interface for components -3. `as const` for constant objects/arrays -4. Explicit ref types: `useRef(null)` - -## Components - -```typescript -'use client' // Only if using hooks - -const CONFIG = { SPACING: 8 } as const - -interface ComponentProps { - requiredProp: string - optionalProp?: boolean -} - -export function Component({ requiredProp, optionalProp = false }: ComponentProps) { - // Order: refs → external hooks → store hooks → custom hooks → state → useMemo → useCallback → useEffect → return -} -``` - -Extract when: 50+ lines, used in 2+ files, or has own state/logic. Keep inline when: < 10 lines, single use, purely presentational. - -## API Contracts - -Boundary HTTP request and response shapes for all routes under `apps/sim/app/api/**` live in `apps/sim/lib/api/contracts/**` (one file per resource family — `folders.ts`, `chats.ts`, `knowledge.ts`, etc.). Routes never define route-local boundary Zod schemas, and clients never define ad-hoc wire types — both sides consume the same contract. - -- Each contract is built with `defineRouteContract({ method, path, params?, query?, body?, headers?, response: { mode: 'json', schema } })` from `@/lib/api/contracts` -- Contracts export named schemas (e.g., `createFolderBodySchema`) AND named TypeScript type aliases (e.g., `export type CreateFolderBody = z.input`) -- Clients (hooks, utilities, components) import the named type aliases from the contract file. They must never write `z.input<...>` / `z.output<...>` themselves -- Shared identifier schemas live in `apps/sim/lib/api/contracts/primitives.ts` (e.g., `workspaceIdSchema`, `workflowIdSchema`). Reuse these instead of redefining string-based ID schemas -- Audit script: `bun run check:api-validation` enforces boundary policy and prints ratchet metrics for route Zod imports, route-local schema constructors, route `ZodError` references, client hook Zod imports, and related counters. It must pass on PRs. `bun run check:api-validation:strict` is the strict CI gate and additionally fails on annotations with empty reasons - -Domain validators that are not HTTP boundaries — tools, blocks, triggers, connectors, realtime handlers, and internal helpers — may still use Zod directly. The contract rule is boundary-only. - -### Boundary annotations - -A small number of legitimate exceptions to the boundary rules are tolerated when annotated. The audit script recognizes four annotation forms: - -- `// boundary-raw-fetch: ` — placed on the line directly above a raw `fetch(` call inside `apps/sim/hooks/queries/**` or `apps/sim/hooks/selectors/**`. Use only for documented exceptions: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests -- `// double-cast-allowed: ` — placed on the line directly above an `as unknown as X` cast outside test files -- `// boundary-raw-json: ` — placed on the line directly above a raw `await request.json()` / `await req.json()` read in a route handler. Use only when the body is a JSON-RPC envelope, a tolerant `.catch(() => ({}))` parse, or otherwise cannot go through `parseRequest` -- `// untyped-response: ` — placed on the line directly above a `schema: z.unknown()` response declaration in a contract file. Use only when the response body is genuinely opaque (user-supplied data, third-party passthrough) +- No `any` — use proper types or `unknown` with type guards +- Always define props interface for components +- `as const` for constant objects/arrays -Placement rule: the annotation must immediately precede the call or cast. Up to three non-empty preceding comment lines are tolerated, so additional context comments above the annotation are fine. The reason must be non-empty after trimming — annotations with empty reasons fail strict mode (`annotationsMissingReason`). - -Whole-file allowlists for routes (legitimate non-boundary or auth-handled routes that legitimately import Zod for non-boundary reasons) go through `INDIRECT_ZOD_ROUTES` in `scripts/check-api-validation-contracts.ts`, not per-line annotations. - -Examples: - -```ts -// boundary-raw-fetch: streaming SSE chunks must be processed as they arrive -const response = await fetch(`/api/copilot/chat/stream?chatId=${chatId}`, { signal }) -``` +## Imports -```ts -// double-cast-allowed: legacy provider type lacks the discriminator field we need -const provider = config as unknown as LegacyProvider -``` +Always absolute (`@/...`). Use barrel exports for folders with 3+ exports; otherwise import directly from source. `import type { X }` for type-only. ## API Route Pattern -Every API route handler must be wrapped with `withRouteHandler`. This sets up `AsyncLocalStorage`-based request context so all loggers in the request lifecycle automatically include the request ID. - -Routes never `import { z } from 'zod'` and never define route-local boundary schemas. They consume the contract from `@/lib/api/contracts/**` and validate with canonical helpers from `@/lib/api/server`: - -- `parseRequest(contract, request, context, options?)` — fully contract-bound routes; parses params, query, body, and headers in one call. Pass `{}` for `context` on routes without route params, or the route's `context` argument when route params exist. Returns a discriminated union; check `parsed.success` and return `parsed.response` on failure -- `validationErrorResponse(error)` and `getValidationErrorMessage(error, fallback)` — produce 400 responses from a `ZodError` -- `validationErrorResponseFromError(error)` — when handling unknown caught errors that may or may not be a `ZodError` -- `isZodError(error)` — type guard. Routes never use `instanceof z.ZodError` - -### Fully contract-bound route (`parseRequest`) - ```typescript import { createLogger } from '@sim/logger' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' -import { createFolderContract } from '@/lib/api/contracts/folders' -import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -const logger = createLogger('FoldersAPI') +const logger = createLogger('MyAPI') -export const POST = withRouteHandler(async (request: NextRequest) => { - const parsed = await parseRequest(createFolderContract, request, {}) - if (!parsed.success) return parsed.response - const { body } = parsed.data - logger.info('Creating folder', { workspaceId: body.workspaceId }) - return NextResponse.json({ ok: true }) +export const GET = withRouteHandler(async (request: NextRequest) => { + logger.info('Handling request') + return NextResponse.json({ ok: true }) }) -``` -### Composing with other middleware +export const DELETE = withRouteHandler(async ( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) => { + const { id } = await params + return NextResponse.json({ deleted: id }) +}) -```typescript +// withRouteHandler wraps the outermost layer export const POST = withRouteHandler(withAdminAuth(async (request) => { return NextResponse.json({ ok: true }) })) ``` -Routes under `apps/sim/app/api/v1/**` use the shared middleware in `apps/sim/app/api/v1/middleware.ts` for auth, rate-limit, and workspace access. Compose contract validation inside that middleware — never reimplement auth/rate-limit per-route. - -Never export a bare `async function GET/POST/...` — always use `export const METHOD = withRouteHandler(...)`. - -### Adding a new boundary feature end-to-end - -When adding a new route + client surface, follow this order. Each step has one place it lives. - -1. **Author the contract first** in `apps/sim/lib/api/contracts/.ts` (or a subdirectory for large domains: `knowledge/`, `selectors/`, `tools/`). Define one schema per request slice (`params`, `query`, `body`, `headers`) and one for the response, then wrap with `defineRouteContract`. Export named type aliases (`z.input` for inputs, `z.output` for outputs). -2. **Implement the route** in `apps/sim/app/api//route.ts`. Auth always runs **before** `parseRequest` — never validate untrusted input before authenticating the caller. The route returns exactly the shape declared in `contract.response.schema`. -3. **Add the React Query hook** in `apps/sim/hooks/queries/.ts`. Use `requestJson(contract, input)` for the call. Build a hierarchical query-key factory (`all` → `lists()` → `list(workspaceId)` → `details()` → `detail(id)`) so invalidations can target prefixes. -4. **Use the hook in the component**. The mutation's `data` and `error` are fully typed from the contract; surface `error.message` (already extracted from the response body's `error` or `message` field by `requestJson`). - -### Schema review checklist (read the contract diff like a DB migration) - -LLMs will write contracts that compile but are sloppy. The human reviewer should optimize attention on: - -- **`required` vs `optional` vs `nullable` is correct**. `optional()` allows omission; `nullable()` allows `null`; chaining both creates a tri-state that's almost never what you want. -- **Response schema matches the route's actual JSON output**. The most common drift bug — route emits a field the schema doesn't declare, or omits a required field. Walk every `NextResponse.json(...)` callsite against the schema. -- **Error messages are descriptive**. `'fileName cannot be empty'` beats `'Required'`. Use the second arg of `min(1, '...')`, `nonempty('...')`, etc. For cross-field refines, use `superRefine` with a `path` and a message that names the failing field. -- **Bounds are set** on arrays (`.min(1)`, `.max(N)`), strings (`.min(1).max(N)` for IDs/names), and numbers (`.min().max()` for limits/sizes). -- **`z.unknown()` is a smell** unless the data is genuinely arbitrary (provider passthrough, user-defined tool result, JSON-RPC envelope). When kept, must be annotated `// untyped-response: ` in a `schema:` slot. -- **Discriminated unions over plain unions** when the wire has a discriminant field — gives clients exhaustive narrowing. - -CI (`bun run check:api-validation:strict`) catches structural violations (Zod imports in routes, raw `request.json()`, double casts, missing annotations). It does **not** catch these schema-quality judgments — that's the human's job in PR review. - -## Hooks - -```typescript -interface UseFeatureProps { id: string } - -export function useFeature({ id }: UseFeatureProps) { - const idRef = useRef(id) - const [data, setData] = useState(null) - - useEffect(() => { idRef.current = id }, [id]) - - const fetchData = useCallback(async () => { ... }, []) // Empty deps when using refs - - return { data, fetchData } -} -``` - ## Zustand Stores -Stores live in `stores/`. Complex stores split into `store.ts` + `types.ts`. - -```typescript -import { create } from 'zustand' -import { devtools } from 'zustand/middleware' - -const initialState = { items: [] as Item[] } - -export const useFeatureStore = create()( - devtools( - (set, get) => ({ - ...initialState, - setItems: (items) => set({ items }), - reset: () => set(initialState), - }), - { name: 'feature-store' } - ) -) -``` - -Use `devtools` middleware. Use `persist` only when data should survive reload with `partialize` to persist only necessary state. +Live in `stores/`. Use `devtools` middleware. Use `persist` only when state must survive reload, with `partialize` to scope what's persisted. Complex stores split into `store.ts` + `types.ts`. ## React Query -All React Query hooks live in `hooks/queries/`. All server state must go through React Query — never use `useState` + `fetch` in components for data fetching or mutations. - -### Client Boundary +All hooks live in `hooks/queries/`. All server state goes through React Query — never `useState` + `fetch` for data fetching or mutations. -Hooks consume contracts the same way routes do. Every same-origin JSON call must go through `requestJson(contract, ...)` from `@/lib/api/client/request` instead of raw `fetch`: +### Query key factory -- Hooks import named type aliases from `@/lib/api/contracts/**`. Never write `z.input<...>` / `z.output<...>` in hooks, and never `import { z } from 'zod'` in client code -- `requestJson` parses params, query, body, and headers against the contract on the way out and validates the JSON response on the way back. Hooks always forward `signal` for cancellation -- Documented exceptions for raw `fetch`: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests. Mark each raw `fetch` with a TSDoc comment explaining which exception applies - -```typescript -import { keepPreviousData, useQuery } from '@tanstack/react-query' -import { requestJson } from '@/lib/api/client/request' -import { listEntitiesContract, type EntityList } from '@/lib/api/contracts/entities' - -async function fetchEntities(workspaceId: string, signal?: AbortSignal): Promise { - const data = await requestJson(listEntitiesContract, { - query: { workspaceId }, - signal, - }) - return data.entities -} - -export function useEntityList(workspaceId?: string) { - return useQuery({ - queryKey: entityKeys.list(workspaceId), - queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal), - enabled: Boolean(workspaceId), - staleTime: 60 * 1000, - placeholderData: keepPreviousData, - }) -} -``` - -### Query Key Factory - -Every file must have a hierarchical key factory with an `all` root key and intermediate plural keys for prefix invalidation: +Hierarchical with `all` root and intermediate plural keys for prefix invalidation: ```typescript export const entityKeys = { @@ -293,240 +71,47 @@ export const entityKeys = { } ``` -### Query Hooks - -- Every `queryFn` must forward `signal` for request cancellation -- Every query must have an explicit `staleTime` -- Use `keepPreviousData` only on variable-key queries (where params change), never on static keys +### Queries -```typescript -export function useEntityList(workspaceId?: string) { - return useQuery({ - queryKey: entityKeys.list(workspaceId), - queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal), - enabled: Boolean(workspaceId), - staleTime: 60 * 1000, - placeholderData: keepPreviousData, // OK: workspaceId varies - }) -} -``` +- Every `queryFn` must forward `signal` +- Every query must set explicit `staleTime` +- `keepPreviousData` only on variable-key queries, never on static keys -### Mutation Hooks +### Mutations -- Use targeted invalidation (`entityKeys.lists()`) not broad (`entityKeys.all`) when possible -- For optimistic updates: use `onSettled` (not `onSuccess`) for cache reconciliation — `onSettled` fires on both success and error -- Don't include mutation objects in `useCallback` deps — `.mutate()` is stable in TanStack Query v5 - -```typescript -export function useUpdateEntity() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: async (variables) => { /* ... */ }, - onMutate: async (variables) => { - await queryClient.cancelQueries({ queryKey: entityKeys.detail(variables.id) }) - const previous = queryClient.getQueryData(entityKeys.detail(variables.id)) - queryClient.setQueryData(entityKeys.detail(variables.id), /* optimistic */) - return { previous } - }, - onError: (_err, variables, context) => { - queryClient.setQueryData(entityKeys.detail(variables.id), context?.previous) - }, - onSettled: (_data, _error, variables) => { - queryClient.invalidateQueries({ queryKey: entityKeys.lists() }) - queryClient.invalidateQueries({ queryKey: entityKeys.detail(variables.id) }) - }, - }) -} -``` - -## Styling - -Use Tailwind only, no inline styles. Use `cn()` from `@/lib/utils` for conditional classes. - -```typescript -
-``` +- Targeted invalidation (`entityKeys.lists()`) over broad (`entityKeys.all`) +- Optimistic updates: reconcile in `onSettled`, not `onSuccess` (fires on error too) +- Don't put mutation objects in `useCallback` deps — `.mutate()` is stable in v5 ## EMCN Components -Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA when 2+ variants exist. +Import from `@/components/emcn`, never subpaths (except CSS). Use CVA when 2+ variants exist. ## Testing -Use Vitest. Test files: `feature.ts` → `feature.test.ts`. See `.cursor/rules/sim-testing.mdc` for full details. +Vitest. `feature.ts` → `feature.test.ts`. See `.claude/rules/sim-testing.md` for full pattern. -### Global Mocks (vitest.setup.ts) +### Global mocks (vitest.setup.ts) -`@sim/db`, `drizzle-orm`, `@sim/logger`, `@/blocks/registry`, `@trigger.dev/sdk`, and store mocks are provided globally. Do NOT re-mock them unless overriding behavior. +`@sim/db`, `drizzle-orm`, `@sim/logger`, `@/blocks/registry`, `@trigger.dev/sdk`, and store mocks are provided globally. Don't re-mock unless overriding behavior. -### Standard Test Pattern +### Performance rules -```typescript -/** - * @vitest-environment node - */ -import { createMockRequest } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const { mockGetSession } = vi.hoisted(() => ({ - mockGetSession: vi.fn(), -})) +- **NEVER** `vi.resetModules()` + `vi.doMock()` + `await import()` — use `vi.hoisted()` + `vi.mock()` + static imports +- **NEVER** `vi.importActual()` — mock everything explicitly +- **NEVER** `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` from `@sim/testing` — they use `vi.doMock()` internally +- Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them +- `@vitest-environment node` unless DOM APIs are needed +- Avoid real timers — 1ms delays or `vi.useFakeTimers()` -vi.mock('@/lib/auth', () => ({ - auth: { api: { getSession: vi.fn() } }, - getSession: mockGetSession, -})) - -import { GET } from '@/app/api/my-route/route' - -describe('my route', () => { - beforeEach(() => { - vi.clearAllMocks() - mockGetSession.mockResolvedValue({ user: { id: 'user-1' } }) - }) - it('returns data', async () => { ... }) -}) -``` +Prefer `@sim/testing` mocks/factories over local test data. -### Performance Rules +## Utils -- **NEVER** use `vi.resetModules()` + `vi.doMock()` + `await import()` — use `vi.hoisted()` + `vi.mock()` + static imports -- **NEVER** use `vi.importActual()` — mock everything explicitly -- **NEVER** use `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` from `@sim/testing` — they use `vi.doMock()` internally -- **Mock heavy deps** (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them -- **Use `@vitest-environment node`** unless DOM APIs are needed (`window`, `document`, `FormData`) -- **Avoid real timers** — use 1ms delays or `vi.useFakeTimers()` - -Use `@sim/testing` mocks/factories over local test data. - -## Utils Rules - -- Never create `utils.ts` for single consumer - inline it -- Create `utils.ts` when 2+ files need the same helper -- Check existing sources in `lib/` before duplicating +Don't create `utils.ts` for a single consumer — inline it. Create one when 2+ files need the same helper. Check `lib/` before duplicating. ## Adding Integrations -New integrations require: **Tools** → **Block** → **Icon** → (optional) **Trigger** - -Always look up the service's API docs first. - -### 1. Tools (`tools/{service}/`) - -``` -tools/{service}/ -├── index.ts # Barrel export -├── types.ts # Params/response types -└── {action}.ts # Tool implementation -``` - -**Tool structure:** - -```typescript -export const serviceTool: ToolConfig = { - id: 'service_action', - name: 'Service Action', - description: '...', - version: '1.0.0', - oauth: { required: true, provider: 'service' }, - params: { /* ... */ }, - request: { url: '/api/tools/service/action', method: 'POST', ... }, - transformResponse: async (response) => { /* ... */ }, - outputs: { /* ... */ }, -} -``` - -Register in `tools/registry.ts`. - -### 2. Block (`blocks/blocks/{service}.ts`) - -```typescript -export const ServiceBlock: BlockConfig = { - type: 'service', - name: 'Service', - description: '...', - category: 'tools', - bgColor: '#hexcolor', - icon: ServiceIcon, - subBlocks: [ /* see SubBlock Properties */ ], - tools: { access: ['service_action'], config: { tool: (p) => `service_${p.operation}`, params: (p) => ({ /* type coercions here */ }) } }, - inputs: { /* ... */ }, - outputs: { /* ... */ }, -} -``` - -Register in `blocks/registry.ts` (alphabetically). - -**Important:** `tools.config.tool` runs during serialization (before variable resolution). Never do `Number()` or other type coercions there — dynamic references like `` will be destroyed. Use `tools.config.params` for type coercions (it runs during execution, after variables are resolved). - -**SubBlock Properties:** - -```typescript -{ - id: 'field', title: 'Label', type: 'short-input', placeholder: '...', - required: true, // or condition object - condition: { field: 'op', value: 'send' }, // show/hide - dependsOn: ['credential'], // clear when dep changes - mode: 'basic', // 'basic' | 'advanced' | 'both' | 'trigger' -} -``` - -**condition examples:** - -- `{ field: 'op', value: 'send' }` - show when op === 'send' -- `{ field: 'op', value: ['a','b'] }` - show when op is 'a' OR 'b' -- `{ field: 'op', value: 'x', not: true }` - show when op !== 'x' -- `{ field: 'op', value: 'x', not: true, and: { field: 'type', value: 'dm', not: true } }` - complex - -**dependsOn:** `['field']` or `{ all: ['a'], any: ['b', 'c'] }` - -**File Input Pattern (basic/advanced mode):** - -```typescript -// Basic: file-upload UI -{ id: 'uploadFile', type: 'file-upload', canonicalParamId: 'file', mode: 'basic' }, -// Advanced: reference from other blocks -{ id: 'fileRef', type: 'short-input', canonicalParamId: 'file', mode: 'advanced' }, -``` - -In `tools.config.tool`, normalize with: - -```typescript -import { normalizeFileInput } from '@/blocks/utils' -const file = normalizeFileInput(params.uploadFile || params.fileRef, { single: true }) -if (file) params.file = file -``` - -For file uploads, create an internal API route (`/api/tools/{service}/upload`) that uses `downloadFileFromStorage` to get file content from `UserFile` objects. - -### 3. Icon (`components/icons.tsx`) - -```typescript -export function ServiceIcon(props: SVGProps) { - return /* SVG from brand assets */ -} -``` - -### 4. Trigger (`triggers/{service}/`) - Optional - -``` -triggers/{service}/ -├── index.ts # Barrel export -├── webhook.ts # Webhook handler -└── {event}.ts # Event-specific handlers -``` - -Register in `triggers/registry.ts`. - -### Integration Checklist - -- Look up API docs -- Create `tools/{service}/` with types and tools -- Register tools in `tools/registry.ts` -- Add icon to `components/icons.tsx` -- Create block in `blocks/blocks/{service}.ts` -- Register block in `blocks/registry.ts` -- (Optional) Create and register triggers -- (If file uploads) Create internal API route with `downloadFileFromStorage` -- (If file uploads) Use `normalizeFileInput` in block config +Use the `add-integration`, `add-block`, `add-tools`, `add-trigger`, or `add-connector` skills. Full reference in `.claude/rules/sim-integrations.md`. +**Critical gotcha:** `tools.config.tool` runs during serialization (before variable resolution). Never do `Number()` or other type coercions there — dynamic references like `` will be destroyed. Put coercions in `tools.config.params` (runs at execution). diff --git a/apps/sim/app/(landing)/blog/[slug]/share-button.tsx b/apps/sim/app/(landing)/blog/[slug]/share-button.tsx index 6916dc2c14b..679c6f44c6e 100644 --- a/apps/sim/app/(landing)/blog/[slug]/share-button.tsx +++ b/apps/sim/app/(landing)/blog/[slug]/share-button.tsx @@ -8,7 +8,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/emcn' -import { Copy } from '@/components/emcn/icons' +import { Duplicate } from '@/components/emcn/icons' import { LinkedInIcon, xIcon as XIcon } from '@/components/icons' interface ShareButtonProps { @@ -52,7 +52,7 @@ export function ShareButton({ url, title }: ShareButtonProps) { - + {copied ? 'Copied!' : 'Copy link'} diff --git a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs.tsx b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs.tsx index 6b691bfcd3f..d71ca9289fe 100644 --- a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs.tsx +++ b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs.tsx @@ -1,8 +1,7 @@ 'use client' import { useMemo, useState } from 'react' -import { Download } from 'lucide-react' -import { ArrowUpDown, Badge, Library, ListFilter, Search } from '@/components/emcn' +import { ArrowUpDown, Badge, Library, ListFilter, Search, Upload } from '@/components/emcn' import type { BadgeProps } from '@/components/emcn/components/badge/badge' import { cn } from '@/lib/core/utils/cn' import { workflowBorderColor } from '@/lib/workspaces/colors' @@ -179,7 +178,7 @@ export function LandingPreviewLogs() {
- + Export
) diff --git a/apps/sim/app/chat/components/message/message.tsx b/apps/sim/app/chat/components/message/message.tsx index f803e82c771..1e1d8f7d548 100644 --- a/apps/sim/app/chat/components/message/message.tsx +++ b/apps/sim/app/chat/components/message/message.tsx @@ -1,8 +1,8 @@ 'use client' import { memo, useState } from 'react' -import { Check, Copy, File as FileIcon, FileText, Image as ImageIcon } from 'lucide-react' -import { Tooltip } from '@/components/emcn' +import { Check, File as FileIcon, FileText, Image as ImageIcon } from 'lucide-react' +import { Duplicate, Tooltip } from '@/components/emcn' import { ChatFileDownload, ChatFileDownloadAll, @@ -214,7 +214,7 @@ export const ClientChatMessage = memo( {isCopied ? ( ) : ( - + )} diff --git a/apps/sim/app/playground/page.tsx b/apps/sim/app/playground/page.tsx index c6c84bd5cfa..47bf71e45c4 100644 --- a/apps/sim/app/playground/page.tsx +++ b/apps/sim/app/playground/page.tsx @@ -20,7 +20,6 @@ import { Code, Combobox, Connections, - Copy, Cursor, DatePicker, DocumentAttachment, @@ -999,7 +998,6 @@ export default function PlaygroundPage() { { Icon: CardIcon, name: 'Card' }, { Icon: ChevronDown, name: 'ChevronDown' }, { Icon: Connections, name: 'Connections' }, - { Icon: Copy, name: 'Copy' }, { Icon: Cursor, name: 'Cursor' }, { Icon: DocumentAttachment, name: 'DocumentAttachment' }, { Icon: Download, name: 'Download' }, diff --git a/apps/sim/app/templates/[id]/template.tsx b/apps/sim/app/templates/[id]/template.tsx index 38e9f746bc2..158c1bd52db 100644 --- a/apps/sim/app/templates/[id]/template.tsx +++ b/apps/sim/app/templates/[id]/template.tsx @@ -19,11 +19,11 @@ import 'streamdown/styles.css' import { Breadcrumb, Button, - Copy, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, + Duplicate, Popover, PopoverContent, PopoverItem, @@ -726,7 +726,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template - + Copy link diff --git a/apps/sim/app/workspace/[workspaceId]/components/conversation-list-item.tsx b/apps/sim/app/workspace/[workspaceId]/components/conversation-list-item.tsx index 465e3147865..4a2afc90b6f 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/conversation-list-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/conversation-list-item.tsx @@ -1,5 +1,4 @@ import type { ReactNode } from 'react' -import { Blimp } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' interface ConversationListItemProps { @@ -14,34 +13,12 @@ interface ConversationListItemProps { export function ConversationListItem({ title, - isActive = false, - isUnread = false, className, titleClassName, - statusIndicatorClassName, actions, }: ConversationListItemProps) { return (
- - - {isActive && ( - - )} - {!isActive && isUnread && ( - - )} - {title} {actions &&
{actions}
}
diff --git a/apps/sim/app/workspace/[workspaceId]/components/index.ts b/apps/sim/app/workspace/[workspaceId]/components/index.ts index 415055b23e3..b57fd0ff706 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/index.ts @@ -28,3 +28,4 @@ export type { SelectableConfig, } from './resource/resource' export { Resource, ResourceTable } from './resource/resource' +export { TaskStatusDot } from './task-status-dot' diff --git a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx index 25eb175ceff..72686bca1bf 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx @@ -6,7 +6,7 @@ import { useParams, useRouter } from 'next/navigation' import { Button, Check, - Copy, + Duplicate, Modal, ModalBody, ModalContent, @@ -179,7 +179,7 @@ export const MessageActions = memo(function MessageActions({ onClick={copyToClipboard} className={BUTTON_CLASS} > - {copied ? : } + {copied ? : } @@ -256,7 +256,7 @@ export const MessageActions = memo(function MessageActions({ {copiedRequestId ? ( ) : ( - + )} diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts b/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts deleted file mode 100644 index 38779f28cbb..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { NavTour, START_NAV_TOUR_EVENT } from './product-tour' -export { START_WORKFLOW_TOUR_EVENT, WorkflowTour } from './workflow-tour' diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps.ts b/apps/sim/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps.ts deleted file mode 100644 index c18aae9734e..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { Step } from 'react-joyride' - -export const navTourSteps: Step[] = [ - { - target: '[data-tour="nav-home"]', - title: 'Home', - content: - 'Your starting point. Describe what you want to build in plain language or pick a template to get started.', - placement: 'right', - disableBeacon: true, - spotlightPadding: 0, - }, - { - target: '[data-tour="nav-search"]', - title: 'Search', - content: 'Quickly find workflows, blocks, and tools. Use Cmd+K to open it from anywhere.', - placement: 'right', - disableBeacon: true, - spotlightPadding: 0, - }, - { - target: '[data-tour="nav-tables"]', - title: 'Tables', - content: - 'Store and query structured data. Your workflows can read and write to tables directly.', - placement: 'right', - disableBeacon: true, - }, - { - target: '[data-tour="nav-files"]', - title: 'Files', - content: 'Upload and manage files that your workflows can process, transform, or reference.', - placement: 'right', - disableBeacon: true, - }, - { - target: '[data-tour="nav-knowledge-base"]', - title: 'Knowledge Base', - content: - 'Build knowledge bases from your documents. Set up connectors to give your agents realtime access to your data sources from sources like Notion, Drive, Slack, Confluence, and more.', - placement: 'right', - disableBeacon: true, - }, - { - target: '[data-tour="nav-scheduled-tasks"]', - title: 'Scheduled Tasks', - content: - 'View and manage background tasks. Set up new tasks, or view the tasks the Mothership is monitoring for upcoming or past executions.', - placement: 'right', - disableBeacon: true, - }, - { - target: '[data-tour="nav-logs"]', - title: 'Logs', - content: - 'Monitor every workflow execution. See inputs, outputs, errors, and timing for each run. View analytics on performance and costs, filter previous runs, and view snapshots of the workflow at the time of execution.', - placement: 'right', - disableBeacon: true, - }, - { - target: '[data-tour="nav-tasks"]', - title: 'Tasks', - content: - 'Tasks that work for you. Mothership can create, edit, and delete resources throughout the platform. It can also perform actions on your behalf, like sending emails, creating tasks, and more.', - placement: 'right', - disableBeacon: true, - }, - { - target: '[data-tour="nav-workflows"]', - title: 'Workflows', - content: - 'All your workflows live here. Create new ones with the + button and organize them into folders. Deploy your workflows as API, webhook, schedule, or chat widget. Then hit Run to test it out.', - placement: 'right', - disableBeacon: true, - }, -] diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx b/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx deleted file mode 100644 index 11487f191db..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/product-tour.tsx +++ /dev/null @@ -1,60 +0,0 @@ -'use client' - -import { useMemo } from 'react' -import dynamic from 'next/dynamic' -import { usePathname } from 'next/navigation' -import { navTourSteps } from '@/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps' -import type { TourState } from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared' -import { - getSharedJoyrideProps, - TourStateContext, - TourTooltipAdapter, -} from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared' -import { useTour } from '@/app/workspace/[workspaceId]/components/product-tour/use-tour' - -const Joyride = dynamic(() => import('react-joyride'), { - ssr: false, -}) - -export const START_NAV_TOUR_EVENT = 'start-nav-tour' - -export function NavTour() { - const pathname = usePathname() - const isWorkflowPage = /\/w\/[^/]+/.test(pathname) - - const { run, stepIndex, tourKey, isTooltipVisible, isEntrance, handleCallback } = useTour({ - steps: navTourSteps, - triggerEvent: START_NAV_TOUR_EVENT, - tourName: 'Navigation tour', - tourType: 'nav', - disabled: isWorkflowPage, - }) - - const tourState = useMemo( - () => ({ - isTooltipVisible, - isEntrance, - totalSteps: navTourSteps.length, - }), - [isTooltipVisible, isEntrance] - ) - - return ( - - - - ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx deleted file mode 100644 index 774ed8ad876..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx +++ /dev/null @@ -1,163 +0,0 @@ -'use client' - -import { createContext, useCallback, useContext } from 'react' -import type { TooltipRenderProps } from 'react-joyride' -import { TourTooltip } from '@/components/emcn' - -/** Shared state passed from the tour component to the tooltip adapter via context */ -export interface TourState { - isTooltipVisible: boolean - isEntrance: boolean - totalSteps: number -} - -export const TourStateContext = createContext({ - isTooltipVisible: true, - isEntrance: true, - totalSteps: 0, -}) - -/** - * Maps Joyride placement strings to TourTooltip placement values. - */ -function mapPlacement(placement?: string): 'top' | 'right' | 'bottom' | 'left' | 'center' { - switch (placement) { - case 'top': - case 'top-start': - case 'top-end': - return 'top' - case 'right': - case 'right-start': - case 'right-end': - return 'right' - case 'bottom': - case 'bottom-start': - case 'bottom-end': - return 'bottom' - case 'left': - case 'left-start': - case 'left-end': - return 'left' - case 'center': - return 'center' - default: - return 'bottom' - } -} - -/** - * Adapter that bridges Joyride's tooltip render props to the EMCN TourTooltip component. - * Reads transition state from TourStateContext to coordinate fade animations. - */ -export function TourTooltipAdapter({ - step, - index, - isLastStep, - tooltipProps, - primaryProps, - backProps, - closeProps, -}: TooltipRenderProps) { - const { isTooltipVisible, isEntrance, totalSteps } = useContext(TourStateContext) - - const { target } = step - const targetEl = - typeof target === 'string' - ? document.querySelector(target) - : target instanceof HTMLElement - ? target - : null - - /** - * Forwards the Joyride tooltip ref safely, handling both - * callback refs and RefObject refs from the library. - * Memoized to prevent ref churn (null → node cycling) on re-renders. - */ - const setJoyrideRef = useCallback( - (node: HTMLDivElement | null) => { - const { ref } = tooltipProps - if (!ref) return - if (typeof ref === 'function') { - ref(node) - } else { - ;(ref as React.MutableRefObject).current = node - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [tooltipProps.ref] - ) - - const placement = mapPlacement(step.placement) - - return ( - <> -
- void} - onBack={backProps.onClick as () => void} - onClose={closeProps.onClick as () => void} - /> - - ) -} - -const SPOTLIGHT_TRANSITION = - 'top 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94), left 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94), width 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94), height 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94)' - -/** - * Returns the shared Joyride floaterProps and styles config used by both tours. - * Only `spotlightPadding` and spotlight `borderRadius` differ between tours. - */ -export function getSharedJoyrideProps(overrides: { spotlightBorderRadius: number }) { - return { - floaterProps: { - disableAnimation: true, - hideArrow: true, - styles: { - floater: { - filter: 'none', - opacity: 0, - pointerEvents: 'none' as React.CSSProperties['pointerEvents'], - width: 0, - height: 0, - }, - }, - }, - styles: { - options: { - zIndex: 10000, - }, - spotlight: { - backgroundColor: 'transparent', - border: '1px solid rgba(255, 255, 255, 0.1)', - borderRadius: overrides.spotlightBorderRadius, - boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.55)', - position: 'fixed' as React.CSSProperties['position'], - transition: SPOTLIGHT_TRANSITION, - }, - overlay: { - backgroundColor: 'transparent', - mixBlendMode: 'unset' as React.CSSProperties['mixBlendMode'], - position: 'fixed' as React.CSSProperties['position'], - height: '100%', - overflow: 'visible', - pointerEvents: 'none' as React.CSSProperties['pointerEvents'], - }, - }, - } as const -} diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts b/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts deleted file mode 100644 index dc41bf013fe..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts +++ /dev/null @@ -1,236 +0,0 @@ -'use client' - -import { useCallback, useEffect, useRef, useState } from 'react' -import { createLogger } from '@sim/logger' -import { usePostHog } from 'posthog-js/react' -import { ACTIONS, type CallBackProps, EVENTS, STATUS, type Step } from 'react-joyride' -import { captureEvent } from '@/lib/posthog/client' - -const logger = createLogger('useTour') - -/** Transition delay before updating step index (ms) */ -const FADE_OUT_MS = 80 - -interface UseTourOptions { - /** Tour step definitions */ - steps: Step[] - /** Custom event name to listen for manual triggers */ - triggerEvent?: string - /** Identifier for logging */ - tourName?: string - /** Analytics tour type for PostHog events */ - tourType?: 'nav' | 'workflow' - /** When true, stops a running tour (e.g. navigating away from the relevant page) */ - disabled?: boolean -} - -interface UseTourReturn { - /** Whether the tour is currently running */ - run: boolean - /** Current step index */ - stepIndex: number - /** Key to force Joyride remount on retrigger */ - tourKey: number - /** Whether the tooltip is visible (false during step transitions) */ - isTooltipVisible: boolean - /** Whether this is the initial entrance animation */ - isEntrance: boolean - /** Joyride callback handler */ - handleCallback: (data: CallBackProps) => void -} - -/** - * Shared hook for managing product tour state with smooth transitions. - * - * Handles manual triggering via custom events and coordinated fade - * transitions between steps to prevent layout shift. - */ -export function useTour({ - steps, - triggerEvent, - tourName = 'tour', - tourType, - disabled = false, -}: UseTourOptions): UseTourReturn { - const posthog = usePostHog() - const [run, setRun] = useState(false) - const [stepIndex, setStepIndex] = useState(0) - const [tourKey, setTourKey] = useState(0) - const [isTooltipVisible, setIsTooltipVisible] = useState(true) - const [isEntrance, setIsEntrance] = useState(true) - - const retriggerTimerRef = useRef | null>(null) - const transitionTimerRef = useRef | null>(null) - const rafRef = useRef(null) - - /** - * Schedules a two-frame rAF to reveal the tooltip after the browser - * finishes repositioning. Stores the outer frame ID in `rafRef` so - * it can be cancelled on unmount or when the tour is interrupted. - */ - const scheduleReveal = useCallback(() => { - if (rafRef.current) { - cancelAnimationFrame(rafRef.current) - } - rafRef.current = requestAnimationFrame(() => { - rafRef.current = requestAnimationFrame(() => { - rafRef.current = null - setIsTooltipVisible(true) - }) - }) - }, []) - - /** Cancels any pending transition timer and rAF reveal */ - const cancelPendingTransitions = useCallback(() => { - if (transitionTimerRef.current) { - clearTimeout(transitionTimerRef.current) - transitionTimerRef.current = null - } - if (rafRef.current) { - cancelAnimationFrame(rafRef.current) - rafRef.current = null - } - }, []) - - const stopTour = useCallback(() => { - cancelPendingTransitions() - setRun(false) - setIsTooltipVisible(true) - setIsEntrance(true) - }, [cancelPendingTransitions]) - - /** Transition to a new step with a coordinated fade-out/fade-in */ - const transitionToStep = useCallback( - (newIndex: number) => { - if (newIndex < 0 || newIndex >= steps.length) { - stopTour() - return - } - - setIsTooltipVisible(false) - cancelPendingTransitions() - - transitionTimerRef.current = setTimeout(() => { - transitionTimerRef.current = null - setStepIndex(newIndex) - setIsEntrance(false) - scheduleReveal() - }, FADE_OUT_MS) - }, - [steps.length, stopTour, cancelPendingTransitions, scheduleReveal] - ) - - useEffect(() => { - if (!run) return - const html = document.documentElement - const prev = html.style.scrollbarGutter - html.style.scrollbarGutter = 'stable' - return () => { - html.style.scrollbarGutter = prev - } - }, [run]) - - /** Stop the tour when disabled becomes true (e.g. navigating away from the relevant page) */ - useEffect(() => { - if (disabled && run) { - stopTour() - logger.info(`${tourName} paused — disabled became true`) - } - }, [disabled, run, tourName, stopTour]) - - /** Listen for manual trigger events */ - useEffect(() => { - if (!triggerEvent) return - - const handleTrigger = () => { - setRun(false) - setTourKey((k) => k + 1) - - if (retriggerTimerRef.current) { - clearTimeout(retriggerTimerRef.current) - } - - retriggerTimerRef.current = setTimeout(() => { - retriggerTimerRef.current = null - setStepIndex(0) - setIsEntrance(true) - setIsTooltipVisible(false) - setRun(true) - logger.info(`${tourName} triggered via event`) - scheduleReveal() - if (tourType) { - captureEvent(posthog, 'tour_started', { tour_type: tourType }) - } - }, 50) - } - - window.addEventListener(triggerEvent, handleTrigger) - return () => { - window.removeEventListener(triggerEvent, handleTrigger) - if (retriggerTimerRef.current) { - clearTimeout(retriggerTimerRef.current) - } - } - }, [triggerEvent, tourName, scheduleReveal]) - - /** Clean up all pending async work on unmount */ - useEffect(() => { - return () => { - cancelPendingTransitions() - if (retriggerTimerRef.current) { - clearTimeout(retriggerTimerRef.current) - } - } - }, [cancelPendingTransitions]) - - const handleCallback = useCallback( - (data: CallBackProps) => { - const { action, index, status, type } = data - - if (status === STATUS.FINISHED || status === STATUS.SKIPPED) { - stopTour() - logger.info(`${tourName} ended`, { status }) - if (tourType) { - if (status === STATUS.FINISHED) { - captureEvent(posthog, 'tour_completed', { tour_type: tourType }) - } else { - captureEvent(posthog, 'tour_skipped', { tour_type: tourType, step_index: index }) - } - } - return - } - - if (type === EVENTS.STEP_AFTER || type === EVENTS.TARGET_NOT_FOUND) { - if (action === ACTIONS.CLOSE) { - stopTour() - logger.info(`${tourName} closed by user`) - if (tourType) { - captureEvent(posthog, 'tour_skipped', { tour_type: tourType, step_index: index }) - } - return - } - - const nextIndex = index + (action === ACTIONS.PREV ? -1 : 1) - - if (type === EVENTS.TARGET_NOT_FOUND) { - logger.info(`${tourName} step target not found, skipping`, { - stepIndex: index, - target: steps[index]?.target, - }) - } - - transitionToStep(nextIndex) - } - }, - [stopTour, transitionToStep, steps, tourName, tourType, posthog] - ) - - return { - run, - stepIndex, - tourKey, - isTooltipVisible, - isEntrance, - handleCallback, - } -} diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps.ts b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps.ts deleted file mode 100644 index cb7105eaf68..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { Step } from 'react-joyride' - -export const workflowTourSteps: Step[] = [ - { - target: '[data-tour="canvas"]', - title: 'The Canvas', - content: - 'This is where you build visually. Drag blocks onto the canvas and connect them to create AI workflows.', - placement: 'center', - disableBeacon: true, - }, - { - target: '[data-tour="tab-copilot"]', - title: 'AI Copilot', - content: - 'Build and debug workflows using natural language. Describe what you want and Copilot creates the blocks for you.', - placement: 'bottom', - disableBeacon: true, - spotlightPadding: 0, - }, - { - target: '[data-tour="tab-toolbar"]', - title: 'Block Library', - content: - 'Browse all available blocks and triggers. Drag them onto the canvas to build your workflow step by step.', - placement: 'bottom', - disableBeacon: true, - spotlightPadding: 0, - }, - { - target: '[data-tour="tab-editor"]', - title: 'Block Editor', - content: - 'Click any block on the canvas to configure it here. Set inputs, credentials, and fine-tune behavior.', - placement: 'bottom', - disableBeacon: true, - spotlightPadding: 0, - }, - { - target: '[data-tour="deploy-run"]', - title: 'Deploy & Run', - content: - 'Deploy your workflow as an API, webhook, schedule, or chat widget. Then hit Run to test it out.', - placement: 'bottom', - disableBeacon: true, - }, - { - target: '[data-tour="workflow-controls"]', - title: 'Canvas Controls', - content: - 'Switch between pointer and hand mode, undo or redo changes, and fit the canvas to your view.', - placement: 'top', - spotlightPadding: 0, - disableBeacon: true, - }, -] diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx deleted file mode 100644 index d9c7f334549..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour.tsx +++ /dev/null @@ -1,59 +0,0 @@ -'use client' - -import { useMemo } from 'react' -import dynamic from 'next/dynamic' -import type { TourState } from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared' -import { - getSharedJoyrideProps, - TourStateContext, - TourTooltipAdapter, -} from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared' -import { useTour } from '@/app/workspace/[workspaceId]/components/product-tour/use-tour' -import { workflowTourSteps } from '@/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps' - -const Joyride = dynamic(() => import('react-joyride'), { - ssr: false, -}) - -export const START_WORKFLOW_TOUR_EVENT = 'start-workflow-tour' - -/** - * Workflow tour that covers the canvas, blocks, copilot, and deployment. - * Triggered via "Take a tour" in the sidebar menu. - */ -export function WorkflowTour() { - const { run, stepIndex, tourKey, isTooltipVisible, isEntrance, handleCallback } = useTour({ - steps: workflowTourSteps, - triggerEvent: START_WORKFLOW_TOUR_EVENT, - tourName: 'Workflow tour', - tourType: 'workflow', - }) - - const tourState = useMemo( - () => ({ - isTooltipVisible, - isEntrance, - totalSteps: workflowTourSteps.length, - }), - [isTooltipVisible, isEntrance] - ) - - return ( - - - - ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/components/task-status-dot.tsx b/apps/sim/app/workspace/[workspaceId]/components/task-status-dot.tsx new file mode 100644 index 00000000000..f2cd5120869 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/task-status-dot.tsx @@ -0,0 +1,61 @@ +import type { SVGProps } from 'react' +import { cn } from '@/lib/core/utils/cn' + +type TaskStatus = 'active' | 'unread' | 'done' + +const STATUS_COLOR_CLASS: Record = { + active: 'text-yellow-500', + unread: 'text-[var(--brand-accent)]', + done: 'text-[var(--text-icon)]', +} + +interface TaskStatusDotProps extends Omit, 'children'> { + isActive?: boolean + isUnread?: boolean +} + +/** + * Linear-style task status indicator. + * + * - **active** — outlined ring with a play wedge (in progress). + * - **unread** — outlined ring with a half-pie fill (started, awaiting attention). + * - **done** — filled disc with an inverse-color check (settled). + * + * Renders as a 16×16 SVG so it slots into rows beside other 16/14px row icons. + * Override size via `className` (e.g. `h-[14px] w-[14px]`) — the SVG scales. + */ +export function TaskStatusDot({ isActive, isUnread, className, ...props }: TaskStatusDotProps) { + const status: TaskStatus = isActive ? 'active' : isUnread ? 'unread' : 'done' + + return ( + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx index 77b5ed2cfdd..27ed6d8464c 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx @@ -9,7 +9,7 @@ import { DropdownMenuShortcut, DropdownMenuTrigger, } from '@/components/emcn' -import { Clipboard, Copy, Search, SelectAll } from '@/components/emcn/icons' +import { Clipboard, Duplicate, Search, SelectAll } from '@/components/emcn/icons' interface EditorContextMenuProps { isOpen: boolean @@ -68,12 +68,12 @@ export function EditorContextMenu({ )} - + Copy ⌘C - + Copy all {canEdit && ( diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index e259a7dad2a..4e87b6cd4cf 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -8,7 +8,6 @@ import { Columns2, Combobox, type ComboboxOption, - Download, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -572,7 +571,7 @@ export function Files() { }, { label: 'Download', - icon: Download, + icon: Upload, onClick: handleDownloadSelected, }, { @@ -804,7 +803,7 @@ export function Files() { : []), { label: 'Download', - icon: Download, + icon: Upload, onClick: handleDownloadSelected, }, { @@ -1269,7 +1268,7 @@ const FileRowContextMenu = memo(function FileRowContextMenu({ Open - + Download {canEdit && ( diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/credits-chip/credits-chip.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/credits-chip/credits-chip.tsx new file mode 100644 index 00000000000..6cfbd1e3f6e --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/credits-chip/credits-chip.tsx @@ -0,0 +1,34 @@ +'use client' + +import { Credit } from '@/components/emcn/icons' +import { formatCredits } from '@/lib/billing/credits/conversion' +import { isBillingEnabled } from '@/app/workspace/[workspaceId]/settings/navigation' +import { useSubscriptionData } from '@/hooks/queries/subscription' +import { useSettingsNavigation } from '@/hooks/use-settings-navigation' + +export function CreditsChip() { + if (!isBillingEnabled) return null + + return +} + +function CreditsChipInner() { + const { data, isLoading } = useSubscriptionData() + const { navigateToSettings } = useSettingsNavigation() + + if (isLoading || !data?.data) return null + + const chipLabel = formatCredits(data.data.creditBalance) + + return ( + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/credits-chip/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/credits-chip/index.ts new file mode 100644 index 00000000000..ba8f8d5aee4 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/credits-chip/index.ts @@ -0,0 +1 @@ +export { CreditsChip } from './credits-chip' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/index.ts index 38debef7f14..8a123d2cd16 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/index.ts @@ -1,5 +1,6 @@ export { ChatMessageAttachments } from './chat-message-attachments' export { ContextMentionIcon } from './context-mention-icon' +export { CreditsChip } from './credits-chip' export { assistantMessageHasRenderableContent, MessageContent, @@ -7,6 +8,6 @@ export { export { MothershipChat } from './mothership-chat/mothership-chat' export { MothershipView } from './mothership-view' export { QueuedMessages } from './queued-messages' -export { TemplatePrompts } from './template-prompts' +export { SuggestedActions } from './suggested-actions' export { UserInput } from './user-input' export { UserMessageContent } from './user-message-content' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat-skeleton.tsx index 6a940e8bc35..7693ded69c3 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat-skeleton.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat-skeleton.tsx @@ -2,7 +2,7 @@ import { Skeleton } from '@/components/emcn' const LAYOUT_SKELETON_STYLES = { 'mothership-view': { - content: 'mx-auto max-w-[42rem] space-y-6', + content: 'mx-auto max-w-[44rem] space-y-6', userRow: 'flex flex-col items-end gap-[6px] pt-3', }, 'copilot-view': { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx index 4693b19de4a..d44864c930e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx @@ -58,13 +58,13 @@ const LAYOUT_STYLES = { 'mothership-view': { scrollContainer: 'min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 pt-4 pb-8 [scrollbar-gutter:stable_both-edges]', - content: 'mx-auto max-w-[42rem] space-y-6', + content: 'mx-auto max-w-[44rem] space-y-6', userRow: 'flex flex-col items-end gap-[6px] pt-3', attachmentWidth: 'max-w-[70%]', userBubble: 'max-w-[70%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3.5 py-2', assistantRow: 'group/msg', footer: 'flex-shrink-0 px-[24px] pb-[16px]', - footerInner: 'mx-auto max-w-[42rem]', + footerInner: 'mx-auto max-w-[44rem]', }, 'copilot-view': { scrollContainer: 'min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-3 pt-2 pb-4', diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx index e793991957c..888a74f5d3d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx @@ -4,9 +4,8 @@ import { lazy, memo, Suspense, useEffect, useMemo, useRef } from 'react' import { createLogger } from '@sim/logger' import { Square } from 'lucide-react' import { useRouter } from 'next/navigation' -import { Button, PlayOutline, Skeleton, Tooltip } from '@/components/emcn' +import { Button, PlayOutline, Skeleton, Tooltip, Upload } from '@/components/emcn' import { - Download, FileX, Folder as FolderIcon, Library, @@ -403,7 +402,7 @@ function EmbeddedTableActions({ workspaceId, tableId, tableName }: EmbeddedTable className={RESOURCE_TAB_ICON_BUTTON_CLASS} aria-label='Export table as CSV' > - + @@ -465,7 +464,7 @@ function EmbeddedFileActions({ workspaceId, fileId }: EmbeddedFileActionsProps) className={RESOURCE_TAB_ICON_BUTTON_CLASS} aria-label='Download file' > - + diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/index.ts new file mode 100644 index 00000000000..bedde2535f8 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/index.ts @@ -0,0 +1 @@ +export { SuggestedActions } from './suggested-actions' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx new file mode 100644 index 00000000000..92216d574aa --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx @@ -0,0 +1,284 @@ +'use client' + +import { type ComponentType, useEffect, useMemo, useState } from 'react' +import { useParams, useRouter } from 'next/navigation' +import { ArrowRight, ChevronDown, Expandable, ExpandableContent } from '@/components/emcn' +import { Table } from '@/components/emcn/icons' +import { + GithubIcon, + GmailIcon, + GoogleCalendarIcon, + HubspotIcon, + JiraIcon, + LinearIcon, + NotionIcon, + SalesforceIcon, + SlackIcon, +} from '@/components/icons' +import { cn } from '@/lib/core/utils/cn' +import { useWorkspaceCredentials } from '@/hooks/queries/credentials' +import { useOAuthConnections } from '@/hooks/queries/oauth/oauth-connections' + +type Icon = ComponentType<{ className?: string }> + +type Action = + | { kind: 'prompt'; id: string; label: string; prompt: string; icon: Icon } + | { kind: 'integration'; id: string; label: string; icon: Icon } + +interface PromptOption { + id: string + label: string + prompt: string + icon: Icon + providerId?: string +} + +const TABLE_PROMPTS: readonly PromptOption[] = [ + { + id: 'crm', + label: 'Create a CRM with sample data', + prompt: 'Create a CRM with sample data.', + icon: Table, + }, + { + id: 'project-tracker', + label: 'Build a project tracker', + prompt: 'Build a project tracker table.', + icon: Table, + }, + { + id: 'content-calendar', + label: 'Create a content calendar', + prompt: 'Create a content calendar table.', + icon: Table, + }, + { + id: 'expense-tracker', + label: 'Build an expense tracker', + prompt: 'Build an expense tracker table.', + icon: Table, + }, + { + id: 'bug-tracker', + label: 'Create a bug tracker', + prompt: 'Create a bug tracker table.', + icon: Table, + }, +] + +const INTEGRATION_PROMPTS: readonly PromptOption[] = [ + { + id: 'gmail-auto-reply', + providerId: 'gmail', + icon: GmailIcon, + label: 'Build an auto-reply email agent', + prompt: + 'Create a workflow that reads my Gmail inbox, identifies emails that need a response, and drafts contextual replies for each one. Schedule it to run every hour.', + }, + { + id: 'slack-qa', + providerId: 'slack', + icon: SlackIcon, + label: 'Build a Slack Q&A bot', + prompt: + 'Create a knowledge base connected to my Notion workspace. Then build a workflow that monitors Slack channels for questions and answers them with source citations.', + }, + { + id: 'jira-search', + providerId: 'jira', + icon: JiraIcon, + label: 'Search across Jira tickets', + prompt: + 'Create a knowledge base connected to my Jira project so all tickets and resolutions are searchable. Then build an agent I can ask questions about past work.', + }, + { + id: 'notion-search', + providerId: 'notion', + icon: NotionIcon, + label: 'Search across Notion', + prompt: + 'Create a knowledge base connected to my Notion workspace. Then build an agent I can ask questions and get answers with page links.', + }, + { + id: 'github-pr-review', + providerId: 'github', + icon: GithubIcon, + label: 'Review pull requests automatically', + prompt: + 'Build a workflow that reviews new GitHub pull requests against my style guide and posts review comments with specific suggestions.', + }, + { + id: 'meeting-prep', + providerId: 'google_calendar', + icon: GoogleCalendarIcon, + label: 'Prep for meetings automatically', + prompt: + 'Create an agent that checks my Google Calendar each morning, researches every attendee, and prepares a brief for each meeting.', + }, + { + id: 'linear-search', + providerId: 'linear', + icon: LinearIcon, + label: 'Search across Linear issues', + prompt: + 'Create a knowledge base connected to my Linear workspace. Then build an agent I can ask questions about past issues and decisions.', + }, + { + id: 'gmail-triage', + providerId: 'gmail', + icon: GmailIcon, + label: 'Triage your email inbox', + prompt: + 'Build a workflow that scans my Gmail inbox hourly, categorizes emails by urgency, drafts replies for routine messages, and Slacks me a prioritized summary.', + }, + { + id: 'hubspot-search', + providerId: 'hubspot', + icon: HubspotIcon, + label: 'Search HubSpot deals', + prompt: + 'Create a knowledge base connected to my HubSpot account. Then build an agent I can ask questions about deals, contacts, and activity.', + }, + { + id: 'salesforce-search', + providerId: 'salesforce', + icon: SalesforceIcon, + label: 'Search across Salesforce', + prompt: + 'Create a knowledge base connected to my Salesforce account. Then build an agent I can ask questions about deals, contacts, and notes.', + }, +] + +const EMPTY_CREDENTIALS: ReturnType['data'] = [] +const EMPTY_SERVICES: ReturnType['data'] = [] + +/** Returns up to `n` random items from the array (Fisher–Yates). */ +function sample(arr: readonly T[], n: number): T[] { + const out = arr.slice() + for (let i = out.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[out[i], out[j]] = [out[j], out[i]] + } + return out.slice(0, n) +} + +interface SuggestedActionsProps { + onSelectPrompt: (prompt: string) => void +} + +export function SuggestedActions({ onSelectPrompt }: SuggestedActionsProps) { + const { workspaceId } = useParams<{ workspaceId: string }>() + const router = useRouter() + const [expanded, setExpanded] = useState(true) + + const { data: credentials = EMPTY_CREDENTIALS } = useWorkspaceCredentials({ + workspaceId, + enabled: Boolean(workspaceId), + }) + const { data: services = EMPTY_SERVICES } = useOAuthConnections() + + const connectedProviders = useMemo( + () => + new Set( + credentials + .filter((c) => c.type === 'oauth' || c.type === 'service_account') + .map((c) => c.providerId) + .filter((id): id is string => Boolean(id)) + ), + [credentials] + ) + + const [actions, setActions] = useState([]) + + useEffect(() => { + const toIntegrationAction = (s: (typeof services)[number]): Action => ({ + kind: 'integration', + id: `integrate-${s.providerId}`, + label: `Integrate with ${s.name}`, + icon: s.icon, + }) + + const availableServices = services.filter((s) => !connectedProviders.has(s.providerId)) + const integrations: Action[] = + connectedProviders.size === 0 + ? ['slack', 'google-email'] + .map((id) => availableServices.find((s) => s.providerId === id)) + .filter((s): s is (typeof services)[number] => Boolean(s)) + .map(toIntegrationAction) + : sample(availableServices, 2).map(toIntegrationAction) + + const toPromptAction = (option: PromptOption): Action => ({ + kind: 'prompt', + id: option.id, + label: option.label, + prompt: option.prompt, + icon: option.icon, + }) + + const [tablePick] = sample(TABLE_PROMPTS, 1) + const integrationPool = INTEGRATION_PROMPTS.filter( + (p) => !p.providerId || !connectedProviders.has(p.providerId) + ) + const [integrationPick] = sample( + integrationPool.length > 0 ? integrationPool : INTEGRATION_PROMPTS, + 1 + ) + + setActions([...integrations, toPromptAction(tablePick), toPromptAction(integrationPick)]) + }, [connectedProviders, services]) + + if (actions.length === 0) return null + + const handleSelect = (action: Action) => { + if (action.kind === 'prompt') { + onSelectPrompt(action.prompt) + return + } + router.push(`/workspace/${workspaceId}/integrations`) + } + + return ( +
+ + + +
+ {actions.map((action, i) => { + const Icon = action.icon + return ( + + ) + })} +
+
+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/template-prompts/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/template-prompts/index.ts deleted file mode 100644 index 17388866dc9..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/home/components/template-prompts/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { Category, ModuleTag, Tag, TemplatePrompt } from './consts' -export { CATEGORY_META, MODULE_META, TEMPLATES } from './consts' -export { TemplatePrompts } from './template-prompts' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/template-prompts/template-prompts.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/template-prompts/template-prompts.tsx deleted file mode 100644 index b0bf8532f99..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/home/components/template-prompts/template-prompts.tsx +++ /dev/null @@ -1,391 +0,0 @@ -'use client' - -import { type ComponentType, memo, type SVGProps } from 'react' -import Image from 'next/image' -import { AgentIcon, ScheduleIcon, StartIcon } from '@/components/icons' -import type { Category, ModuleTag } from './consts' -import { CATEGORY_META, TEMPLATES } from './consts' - -const FEATURED_TEMPLATES = TEMPLATES.filter((t) => t.featured) -const EXTRA_TEMPLATES = TEMPLATES.filter((t) => !t.featured) - -function getGroupedExtras() { - const groups: { category: Category; label: string; templates: typeof TEMPLATES }[] = [] - const byCategory = new Map() - - for (const t of EXTRA_TEMPLATES) { - const existing = byCategory.get(t.category) - if (existing) { - existing.push(t) - } else { - const arr = [t] - byCategory.set(t.category, arr) - } - } - - for (const [key, meta] of Object.entries(CATEGORY_META)) { - const cat = key as Category - if (cat === 'popular') continue - const items = byCategory.get(cat) - if (items?.length) { - groups.push({ category: cat, label: meta.label, templates: items }) - } - } - - return groups -} - -const GROUPED_EXTRAS = getGroupedExtras() - -const MINI_TABLE_DATA = [ - ['Sarah Chen', 'sarah@acme.co', 'Acme Inc', 'Qualified'], - ['James Park', 'james@globex.io', 'Globex', 'New'], - ['Maria Santos', 'maria@initech.com', 'Initech', 'Contacted'], - ['Alex Kim', 'alex@umbrella.co', 'Umbrella', 'Qualified'], - ['Emma Wilson', 'emma@stark.io', 'Stark Ind', 'New'], -] as const - -const STATUS_DOT: Record = { - Qualified: 'bg-emerald-400', - New: 'bg-blue-400', - Contacted: 'bg-amber-400', -} - -const MINI_KB_DATA = [ - ['product-specs.pdf', '4.2 MB', '12.4k', 'Enabled'], - ['eng-handbook.md', '1.8 MB', '8.2k', 'Enabled'], - ['api-reference.json', '920 KB', '4.1k', 'Enabled'], - ['release-notes.md', '340 KB', '2.8k', 'Enabled'], - ['onboarding.pdf', '2.1 MB', '6.5k', 'Processing'], -] as const - -const KB_BADGE: Record = { - Enabled: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400', - Processing: 'bg-violet-500/15 text-violet-700 dark:text-violet-400', -} - -interface WorkflowBlockDef { - color: string - name: string - icon: ComponentType> - rows: { title: string; value: string }[] -} - -function PreviewTable() { - return ( -
-
- {['Name', 'Email', 'Company', 'Status'].map((col) => ( -
- {col} -
- ))} -
- {MINI_TABLE_DATA.map((row, i) => ( -
- {row.map((cell, j) => ( -
- {j === 3 ? ( -
-
- {cell} -
- ) : ( - - {cell} - - )} -
- ))} -
- ))} -
- ) -} - -function PreviewKnowledge() { - return ( -
-
- {['Name', 'Size', 'Tokens', 'Status'].map((col) => ( -
- {col} -
- ))} -
- {MINI_KB_DATA.map((row, i) => ( -
-
- - {row[0]} - -
-
- {row[1]} -
-
- {row[2]} -
-
- - {row[3]} - -
-
- ))} -
- ) -} - -function PreviewFile() { - return ( -
-
- Files - / - meeting-notes.md -
-
-

Meeting Notes

-

Action Items

-

- • Review Q1 metrics with Sarah -

-

• Update API documentation

-

- • Schedule design review for v2.0 -

-

Discussion Points

-

- The team agreed to prioritize the new onboarding flow... -

-

Next Steps

-

- Follow up with engineering on the API v2 migration. -

-
-
- ) -} - -const WorkflowMiniBlock = memo(function WorkflowMiniBlock({ - color, - name, - icon: Icon, - rows, -}: WorkflowBlockDef) { - const hasRows = rows.length > 0 - return ( -
-
-
- -
- {name} -
- {rows.map((row) => ( -
- {row.title} - {row.value} -
- ))} -
- ) -}) - -function buildWorkflowBlocks(template: (typeof TEMPLATES)[number]): WorkflowBlockDef[] { - const modules = template.modules - const toolName = template.title.split(' ')[0] - const hasAgent = modules.includes('agent') - const isScheduled = modules.includes('scheduled') - - const starter: WorkflowBlockDef = isScheduled - ? { - color: '#6366F1', - name: 'Schedule', - icon: ScheduleIcon, - rows: [{ title: 'Cron', value: '0 9 * * 1' }], - } - : { - color: '#2FB3FF', - name: 'Starter', - icon: StartIcon, - rows: [{ title: 'Trigger', value: 'Manual' }], - } - - const agent: WorkflowBlockDef = { - color: '#802FFF', - name: 'Agent', - icon: AgentIcon, - rows: [{ title: 'Model', value: 'gpt-4o' }], - } - - const tool: WorkflowBlockDef = { - color: '#3B3B3B', - name: toolName, - icon: template.icon, - rows: [{ title: 'Action', value: 'Run' }], - } - - if (hasAgent) return [starter, agent, tool] - return [starter, tool] -} - -const BLOCK_W = 76 -const EDGE_W = 14 - -function PreviewWorkflow({ template }: { template: (typeof TEMPLATES)[number] }) { - const blocks = buildWorkflowBlocks(template) - const goesUp = template.title.charCodeAt(0) % 2 === 0 - - const twoBlock = blocks.length === 2 - const offsets = twoBlock - ? goesUp - ? [-10, 10] - : [10, -10] - : goesUp - ? [-12, 12, -12] - : [12, -12, 12] - - const totalW = blocks.length * BLOCK_W + (blocks.length - 1) * EDGE_W - - return ( -
-
- - {blocks.slice(1).map((_, i) => { - const x1 = i * (BLOCK_W + EDGE_W) + BLOCK_W - const y1 = 35 + offsets[i] - const x2 = (i + 1) * (BLOCK_W + EDGE_W) - const y2 = 35 + offsets[i + 1] - const midX = (x1 + x2) / 2 - return ( - - ) - })} - - - {blocks.map((block, i) => { - const x = i * (BLOCK_W + EDGE_W) - const yCenter = 35 + offsets[i] - return ( -
- -
- ) - })} -
-
- ) -} - -function TemplatePreview({ - modules, - template, -}: { - modules: ModuleTag[] - template: (typeof TEMPLATES)[number] -}) { - if (modules.includes('tables')) return - if (modules.includes('knowledge-base')) return - if (modules.includes('files')) return - return -} - -interface TemplatePromptsProps { - onSelect: (prompt: string) => void -} - -export function TemplatePrompts({ onSelect }: TemplatePromptsProps) { - return ( -
-
- {FEATURED_TEMPLATES.map((template) => ( - - ))} -
- - {GROUPED_EXTRAS.map((group) => ( -
-

{group.label}

-
- {group.templates.map((template) => ( - - ))} -
-
- ))} -
- ) -} - -interface TemplateCardProps { - template: (typeof TEMPLATES)[number] - onSelect: (prompt: string) => void -} - -const TemplateCard = memo(function TemplateCard({ template, onSelect }: TemplateCardProps) { - const Icon = template.icon - - return ( - - ) -}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx index 1304d009086..e639ddb4d51 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx @@ -902,7 +902,7 @@ export const UserInput = forwardRef(function Us
(null) - const templateRef = useRef(null) - const baseInputHeightRef = useRef(null) const [isInputEntering, setIsInputEntering] = useState(false) @@ -277,34 +281,18 @@ export function Home({ chatId }: HomeProps = {}) { const showChatSkeleton = Boolean(chatId) && !hasMessages && isChatHistoryPending const draftScopeKey = `${workspaceId}:${chatId ?? 'new'}` - useEffect(() => { - if (hasMessages) return - const input = initialViewInputRef.current - const templates = templateRef.current - if (!input || !templates) return - - const ro = new ResizeObserver((entries) => { - const height = entries[0].contentRect.height - if (baseInputHeightRef.current === null) baseInputHeightRef.current = height - const delta = Math.max(0, (height - baseInputHeightRef.current) / 2) - templates.style.marginTop = delta > 0 ? `calc(-30vh + ${delta}px)` : '' - }) - ro.observe(input) - return () => ro.disconnect() - }, [hasMessages]) - if (!hasMessages && !showChatSkeleton) { return ( -
+
+
+ +
-

+

What should we get done {session?.user?.name ? `, ${session.user.name.split(' ')[0]}` : ''}?

-
+
+ handleSubmit(prompt)} />
-
- -
) } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx similarity index 72% rename from apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx rename to apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx index bbf31a891fb..1a6d7429eec 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx @@ -18,7 +18,6 @@ import { ModalContent, ModalFooter, ModalHeader, - Skeleton, Textarea, Tooltip, } from '@/components/emcn' @@ -35,7 +34,6 @@ import { import { getCanonicalScopesForProvider, getServiceConfigByProviderId } from '@/lib/oauth' import { getScopeDescription } from '@/lib/oauth/utils' import { getUserColor } from '@/lib/workspaces/colors' -import { CredentialSkeleton } from '@/app/workspace/[workspaceId]/settings/components/integrations/credential-skeleton' import { useCreateCredentialDraft, useCreateWorkspaceCredential, @@ -55,9 +53,8 @@ import { } from '@/hooks/queries/oauth/oauth-connections' import { useWorkspacePermissionsQuery } from '@/hooks/queries/workspace' import { useOAuthReturnRouter } from '@/hooks/use-oauth-return' -import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' -const logger = createLogger('IntegrationsManager') +const logger = createLogger('Integrations') const ROLE_OPTIONS = [ { value: 'member', label: 'Member' }, @@ -69,7 +66,7 @@ const roleComboOptions = ROLE_OPTIONS.map((option) => ({ label: option.label, })) -export function IntegrationsManager() { +export function Integrations() { const params = useParams() const workspaceId = (params?.workspaceId as string) || '' @@ -255,15 +252,6 @@ export function IntegrationsManager() { const isDetailsDirty = isDescriptionDirty || isDisplayNameDirty - const setNavGuardDirty = useSettingsDirtyStore((s) => s.setDirty) - const resetNavGuard = useSettingsDirtyStore((s) => s.reset) - - useEffect(() => { - setNavGuardDirty(isDetailsDirty) - }, [isDetailsDirty, setNavGuardDirty]) - - useEffect(() => () => resetNavGuard(), [resetNavGuard]) - const handleSaveDetails = async () => { if (!selectedCredential || !isSelectedAdmin || !isDetailsDirty || updateCredential.isPending) return @@ -1195,400 +1183,408 @@ export function IntegrationsManager() { if (selectedCredential) { return ( - <> -
-
-
-
-
- {selectedOAuthServiceConfig ? ( - createElement(selectedOAuthServiceConfig.icon, { - className: 'h-[18px] w-[18px]', - }) - ) : ( - - {resolveProviderLabel(selectedCredential.providerId).slice(0, 1)} - - )} -
-
-
-

- {selectedOAuthServiceConfig?.name || - resolveProviderLabel(selectedCredential.providerId) || - 'Unknown service'} -

- - {selectedOAuthServiceConfig?.authType === 'service_account' - ? 'service account' - : 'oauth'} - - {selectedCredential.role && ( +
+
+

Integrations

+
+
+
+
+
+ {selectedOAuthServiceConfig ? ( + createElement(selectedOAuthServiceConfig.icon, { + className: 'h-[18px] w-[18px]', + }) + ) : ( + + {resolveProviderLabel(selectedCredential.providerId).slice(0, 1)} + + )} +
+
+
+

+ {selectedOAuthServiceConfig?.name || + resolveProviderLabel(selectedCredential.providerId) || + 'Unknown service'} +

- {selectedCredential.role} + {selectedOAuthServiceConfig?.authType === 'service_account' + ? 'service account' + : 'oauth'} - )} + {selectedCredential.role && ( + + {selectedCredential.role} + + )} +
+

+ {selectedOAuthServiceConfig?.description || 'Connected service'} +

-

- {selectedOAuthServiceConfig?.description || 'Connected service'} -

-
-
- - setSelectedDisplayNameDraft(event.target.value)} - autoComplete='off' - data-lpignore='true' - disabled={!isSelectedAdmin} - /> -
- -
- -