From f0eb9768831a6ca4fdc9e4930b0944c7f48e90c0 Mon Sep 17 00:00:00 2001 From: urjitc <135136842+urjitc@users.noreply.github.com> Date: Sun, 19 Apr 2026 06:53:56 +0000 Subject: [PATCH] Remove assistant-ui chat and replace side panel with AI coming soon placeholder --- drizzle/0003_drop_chat_tables.sql | 2 + drizzle/meta/0003_snapshot.json | 2473 +++++++++++++++++ drizzle/meta/_journal.json | 7 + package.json | 16 +- src/app/api/chat/route.ts | 403 --- src/app/api/flashcards/create/route.ts | 68 - src/app/api/threads/[id]/archive/route.ts | 24 - .../[id]/messages/[messageId]/route.ts | 78 - src/app/api/threads/[id]/messages/route.ts | 146 - src/app/api/threads/[id]/route.ts | 135 - src/app/api/threads/[id]/title/route.ts | 132 - src/app/api/threads/[id]/unarchive/route.ts | 24 - src/app/api/threads/route.ts | 112 - src/app/api/workspaces/autogen/route.ts | 10 +- .../api/workspaces/generate-title/route.ts | 4 +- src/app/generate/page.tsx | 2 +- src/app/globals.css | 8 - .../ai-elements/inline-citation.tsx | 200 -- .../assistant-ui/AIFeedbackDialog.tsx | 86 - .../assistant-ui/AddYoutubeVideoToolUI.tsx | 155 -- .../assistant-ui/AssistantDropzone.tsx | 215 -- .../assistant-ui/AssistantPanel.tsx | 149 - .../AssistantTextSelectionManager.tsx | 118 - .../assistant-ui/CreateDocumentToolUI.tsx | 262 -- .../assistant-ui/CreateFlashcardToolUI.tsx | 324 --- .../assistant-ui/CreateQuizToolUI.tsx | 284 -- .../assistant-ui/EditItemToolUI.tsx | 203 -- .../assistant-ui/ExecuteCodeToolUI.tsx | 135 - src/components/assistant-ui/ModelPicker.tsx | 260 -- .../assistant-ui/ModelSettingsMenu.tsx | 81 - .../assistant-ui/PromptBuilderDialog.tsx | 774 ------ .../assistant-ui/ReadWorkspaceToolUI.tsx | 92 - .../assistant-ui/SearchWorkspaceToolUI.tsx | 55 - .../assistant-ui/SpeechToTextButton.tsx | 73 - .../assistant-ui/URLContextToolUI.tsx | 337 --- .../assistant-ui/WebSearchToolUI.tsx | 303 -- .../assistant-ui/WorkspaceRuntimeProvider.tsx | 259 -- .../assistant-ui/YouTubeSearchToolUI.tsx | 392 --- .../assistant-ui/assistant-loader.tsx | 34 - .../assistant-thread-selection.tsx | 228 -- src/components/assistant-ui/attachment.tsx | 518 ---- src/components/assistant-ui/chat-toolkit.tsx | 94 - src/components/assistant-ui/diff-viewer.tsx | 625 ----- src/components/assistant-ui/file.tsx | 235 -- src/components/assistant-ui/image.tsx | 264 -- src/components/assistant-ui/markdown-text.tsx | 298 -- src/components/assistant-ui/reasoning.tsx | 326 --- src/components/assistant-ui/sources.tsx | 148 - .../assistant-ui/standalone-markdown.tsx | 289 -- .../assistant-ui/thread-list-dropdown.tsx | 326 --- src/components/assistant-ui/thread.tsx | 1510 ---------- src/components/assistant-ui/tool-fallback.tsx | 46 - src/components/assistant-ui/tool-group.tsx | 257 -- .../assistant-ui/tool-ui-error-shell.tsx | 51 - .../assistant-ui/tool-ui-loading-shell.tsx | 43 - src/components/chat/AppChatHeader.tsx | 448 --- src/components/chat/CardContextDisplay.tsx | 177 -- src/components/chat/ChatFloatingButton.tsx | 37 - src/components/chat/MentionMenu.tsx | 154 - src/components/chat/MessageContextBadges.tsx | 53 - src/components/chat/ReplyContextDisplay.tsx | 95 - .../chat/ReplySelectionRichText.tsx | 51 - src/components/chat/WorkspaceItemPicker.tsx | 269 -- src/components/editor/DocumentEditor.tsx | 39 +- src/components/home/HomePromptInput.tsx | 2 +- src/components/layout/AiComingSoonPanel.tsx | 104 + src/components/layout/AiPanelToggleButton.tsx | 36 + src/components/layout/DashboardLayout.tsx | 54 +- src/components/pdf/AppPdfViewer.tsx | 157 +- src/components/pdf/PdfPanelHeader.tsx | 38 +- .../code-block-node/code-block-node.tsx | 2 +- src/components/tool-ui/shared/_adapter.tsx | 11 - .../tool-ui/shared/action-buttons.tsx | 101 - .../tool-ui/shared/actions-config.ts | 48 - .../tool-ui/shared/error-boundary.tsx | 57 - src/components/tool-ui/shared/index.ts | 8 - src/components/tool-ui/shared/parse.ts | 37 - src/components/tool-ui/shared/schema.ts | 122 - .../tool-ui/shared/use-action-buttons.tsx | 128 - .../tool-ui/shared/use-copy-to-clipboard.ts | 63 - src/components/tool-ui/shared/utils.ts | 29 - .../{ai-elements => ui}/code-block.tsx | 0 src/components/ui/selection-tooltip.tsx | 1 - .../tooltip-icon-button.tsx | 0 .../workspace-canvas/ItemPanelContent.tsx | 17 +- .../workspace-canvas/QuizContent.tsx | 129 +- .../workspace-canvas/WorkspaceHeader.tsx | 41 +- .../WorkspaceSearchDialog.tsx | 7 +- .../workspace-canvas/WorkspaceSection.tsx | 27 - .../workspace-canvas/workspace-menu-items.tsx | 63 +- src/contexts/AssistantAvailabilityContext.tsx | 49 - .../ai/use-workspace-context-provider.ts | 28 - .../legacy-tool-message-compat.test.ts | 384 --- .../ai/__tests__/process-urls-shared.test.ts | 32 - src/lib/ai/chat-tool-names.ts | 94 - src/lib/ai/code-execute-environment.ts | 20 - src/lib/ai/code-execute-shared.ts | 46 - src/lib/ai/legacy-tool-message-compat.ts | 137 - src/lib/ai/process-urls-shared.ts | 67 - src/lib/ai/supermemory.ts | 58 - src/lib/ai/tool-result-schemas.ts | 155 -- .../ai/tools/__tests__/edit-item-tool.test.ts | 238 -- .../__tests__/tool-utils-sanitize.test.ts | 314 --- src/lib/ai/tools/edit-item-tool.ts | 172 -- src/lib/ai/tools/execute-code.ts | 102 - src/lib/ai/tools/flashcard-tools.ts | 67 - src/lib/ai/tools/index.ts | 86 - src/lib/ai/tools/process-urls.ts | 118 - src/lib/ai/tools/quiz-tools.ts | 95 - src/lib/ai/tools/read-workspace.ts | 218 -- src/lib/ai/tools/search-workspace.ts | 168 -- src/lib/ai/tools/tool-utils.ts | 145 - src/lib/ai/tools/workspace-search-utils.ts | 159 -- src/lib/ai/tools/workspace-tools.ts | 142 - src/lib/ai/tools/youtube-tools.ts | 74 - src/lib/ai/web-search-shared.ts | 58 - src/lib/api/thread-archive.ts | 42 - src/lib/api/workspace-helpers.ts | 13 - .../supabase-attachment-adapter.ts | 110 - src/lib/attachments/url-utils.ts | 178 -- src/lib/{ai/tools => autogen}/web-search.ts | 70 +- .../custom-thread-history-adapter.test.ts | 281 -- .../chat/custom-thread-history-adapter.tsx | 229 -- src/lib/chat/custom-thread-list-adapter.tsx | 137 - src/lib/chat/toCreateMessageWithContext.ts | 62 - src/lib/db/relations.ts | 16 - src/lib/db/schema.ts | 88 - .../firecrawl.ts => firecrawl/client.ts} | 0 src/lib/{ai => gateway}/models.ts | 6 - .../provider-options.ts} | 2 +- src/lib/stores/attachment-upload-store.ts | 32 - src/lib/stores/ui-store.ts | 154 +- .../format-workspace-context.test.ts | 50 - src/lib/utils/format-workspace-context.ts | 774 ------ .../workers => workspace-worker}/common.ts | 0 .../{ai/workers => workspace-worker}/index.ts | 0 .../workspace-worker.ts | 0 137 files changed, 2811 insertions(+), 18957 deletions(-) create mode 100644 drizzle/0003_drop_chat_tables.sql create mode 100644 drizzle/meta/0003_snapshot.json delete mode 100644 src/app/api/chat/route.ts delete mode 100644 src/app/api/flashcards/create/route.ts delete mode 100644 src/app/api/threads/[id]/archive/route.ts delete mode 100644 src/app/api/threads/[id]/messages/[messageId]/route.ts delete mode 100644 src/app/api/threads/[id]/messages/route.ts delete mode 100644 src/app/api/threads/[id]/route.ts delete mode 100644 src/app/api/threads/[id]/title/route.ts delete mode 100644 src/app/api/threads/[id]/unarchive/route.ts delete mode 100644 src/app/api/threads/route.ts delete mode 100644 src/components/ai-elements/inline-citation.tsx delete mode 100644 src/components/assistant-ui/AIFeedbackDialog.tsx delete mode 100644 src/components/assistant-ui/AddYoutubeVideoToolUI.tsx delete mode 100644 src/components/assistant-ui/AssistantDropzone.tsx delete mode 100644 src/components/assistant-ui/AssistantPanel.tsx delete mode 100644 src/components/assistant-ui/AssistantTextSelectionManager.tsx delete mode 100644 src/components/assistant-ui/CreateDocumentToolUI.tsx delete mode 100644 src/components/assistant-ui/CreateFlashcardToolUI.tsx delete mode 100644 src/components/assistant-ui/CreateQuizToolUI.tsx delete mode 100644 src/components/assistant-ui/EditItemToolUI.tsx delete mode 100644 src/components/assistant-ui/ExecuteCodeToolUI.tsx delete mode 100644 src/components/assistant-ui/ModelPicker.tsx delete mode 100644 src/components/assistant-ui/ModelSettingsMenu.tsx delete mode 100644 src/components/assistant-ui/PromptBuilderDialog.tsx delete mode 100644 src/components/assistant-ui/ReadWorkspaceToolUI.tsx delete mode 100644 src/components/assistant-ui/SearchWorkspaceToolUI.tsx delete mode 100644 src/components/assistant-ui/SpeechToTextButton.tsx delete mode 100644 src/components/assistant-ui/URLContextToolUI.tsx delete mode 100644 src/components/assistant-ui/WebSearchToolUI.tsx delete mode 100644 src/components/assistant-ui/WorkspaceRuntimeProvider.tsx delete mode 100644 src/components/assistant-ui/YouTubeSearchToolUI.tsx delete mode 100644 src/components/assistant-ui/assistant-loader.tsx delete mode 100644 src/components/assistant-ui/assistant-thread-selection.tsx delete mode 100644 src/components/assistant-ui/attachment.tsx delete mode 100644 src/components/assistant-ui/chat-toolkit.tsx delete mode 100644 src/components/assistant-ui/diff-viewer.tsx delete mode 100644 src/components/assistant-ui/file.tsx delete mode 100644 src/components/assistant-ui/image.tsx delete mode 100644 src/components/assistant-ui/markdown-text.tsx delete mode 100644 src/components/assistant-ui/reasoning.tsx delete mode 100644 src/components/assistant-ui/sources.tsx delete mode 100644 src/components/assistant-ui/standalone-markdown.tsx delete mode 100644 src/components/assistant-ui/thread-list-dropdown.tsx delete mode 100644 src/components/assistant-ui/thread.tsx delete mode 100644 src/components/assistant-ui/tool-fallback.tsx delete mode 100644 src/components/assistant-ui/tool-group.tsx delete mode 100644 src/components/assistant-ui/tool-ui-error-shell.tsx delete mode 100644 src/components/assistant-ui/tool-ui-loading-shell.tsx delete mode 100644 src/components/chat/AppChatHeader.tsx delete mode 100644 src/components/chat/CardContextDisplay.tsx delete mode 100644 src/components/chat/ChatFloatingButton.tsx delete mode 100644 src/components/chat/MentionMenu.tsx delete mode 100644 src/components/chat/MessageContextBadges.tsx delete mode 100644 src/components/chat/ReplyContextDisplay.tsx delete mode 100644 src/components/chat/ReplySelectionRichText.tsx delete mode 100644 src/components/chat/WorkspaceItemPicker.tsx create mode 100644 src/components/layout/AiComingSoonPanel.tsx create mode 100644 src/components/layout/AiPanelToggleButton.tsx delete mode 100644 src/components/tool-ui/shared/_adapter.tsx delete mode 100644 src/components/tool-ui/shared/action-buttons.tsx delete mode 100644 src/components/tool-ui/shared/actions-config.ts delete mode 100644 src/components/tool-ui/shared/error-boundary.tsx delete mode 100644 src/components/tool-ui/shared/index.ts delete mode 100644 src/components/tool-ui/shared/parse.ts delete mode 100644 src/components/tool-ui/shared/schema.ts delete mode 100644 src/components/tool-ui/shared/use-action-buttons.tsx delete mode 100644 src/components/tool-ui/shared/use-copy-to-clipboard.ts delete mode 100644 src/components/tool-ui/shared/utils.ts rename src/components/{ai-elements => ui}/code-block.tsx (100%) rename src/components/{assistant-ui => ui}/tooltip-icon-button.tsx (100%) delete mode 100644 src/contexts/AssistantAvailabilityContext.tsx delete mode 100644 src/hooks/ai/use-workspace-context-provider.ts delete mode 100644 src/lib/ai/__tests__/legacy-tool-message-compat.test.ts delete mode 100644 src/lib/ai/__tests__/process-urls-shared.test.ts delete mode 100644 src/lib/ai/chat-tool-names.ts delete mode 100644 src/lib/ai/code-execute-environment.ts delete mode 100644 src/lib/ai/code-execute-shared.ts delete mode 100644 src/lib/ai/legacy-tool-message-compat.ts delete mode 100644 src/lib/ai/process-urls-shared.ts delete mode 100644 src/lib/ai/supermemory.ts delete mode 100644 src/lib/ai/tool-result-schemas.ts delete mode 100644 src/lib/ai/tools/__tests__/edit-item-tool.test.ts delete mode 100644 src/lib/ai/tools/__tests__/tool-utils-sanitize.test.ts delete mode 100644 src/lib/ai/tools/edit-item-tool.ts delete mode 100644 src/lib/ai/tools/execute-code.ts delete mode 100644 src/lib/ai/tools/flashcard-tools.ts delete mode 100644 src/lib/ai/tools/index.ts delete mode 100644 src/lib/ai/tools/process-urls.ts delete mode 100644 src/lib/ai/tools/quiz-tools.ts delete mode 100644 src/lib/ai/tools/read-workspace.ts delete mode 100644 src/lib/ai/tools/search-workspace.ts delete mode 100644 src/lib/ai/tools/tool-utils.ts delete mode 100644 src/lib/ai/tools/workspace-search-utils.ts delete mode 100644 src/lib/ai/tools/workspace-tools.ts delete mode 100644 src/lib/ai/tools/youtube-tools.ts delete mode 100644 src/lib/ai/web-search-shared.ts delete mode 100644 src/lib/api/thread-archive.ts delete mode 100644 src/lib/attachments/supabase-attachment-adapter.ts delete mode 100644 src/lib/attachments/url-utils.ts rename src/lib/{ai/tools => autogen}/web-search.ts (73%) delete mode 100644 src/lib/chat/custom-thread-history-adapter.test.ts delete mode 100644 src/lib/chat/custom-thread-history-adapter.tsx delete mode 100644 src/lib/chat/custom-thread-list-adapter.tsx delete mode 100644 src/lib/chat/toCreateMessageWithContext.ts rename src/lib/{ai/utils/firecrawl.ts => firecrawl/client.ts} (100%) rename src/lib/{ai => gateway}/models.ts (98%) rename src/lib/{ai/gateway-provider-options.ts => gateway/provider-options.ts} (97%) delete mode 100644 src/lib/stores/attachment-upload-store.ts delete mode 100644 src/lib/utils/__tests__/format-workspace-context.test.ts delete mode 100644 src/lib/utils/format-workspace-context.ts rename src/lib/{ai/workers => workspace-worker}/common.ts (100%) rename src/lib/{ai/workers => workspace-worker}/index.ts (100%) rename src/lib/{ai/workers => workspace-worker}/workspace-worker.ts (100%) diff --git a/drizzle/0003_drop_chat_tables.sql b/drizzle/0003_drop_chat_tables.sql new file mode 100644 index 00000000..0846212f --- /dev/null +++ b/drizzle/0003_drop_chat_tables.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS "chat_messages" CASCADE;--> statement-breakpoint +DROP TABLE IF EXISTS "chat_threads" CASCADE; diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 00000000..a7314ccb --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,2473 @@ +{ + "id": "4cbfb010-a01e-4a92-9e69-3be675ed2c15", + "prevId": "77a2e4b0-3244-4eda-8ffd-41ba7999b3e1", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_account_user_id": { + "name": "idx_account_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_session_user_id": { + "name": "idx_session_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_token": { + "name": "idx_session_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_anonymous": { + "name": "is_anonymous", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_user_email": { + "name": "idx_user_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_profiles": { + "name": "user_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "onboarding_completed": { + "name": "onboarding_completed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "onboarding_completed_at": { + "name": "onboarding_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_user_profiles_user_id": { + "name": "idx_user_profiles_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_profiles_user_id_key": { + "name": "user_profiles_user_id_key", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": { + "Users can insert their own profile": { + "name": "Users can insert their own profile", + "as": "PERMISSIVE", + "for": "INSERT", + "to": [ + "authenticated" + ], + "withCheck": "(( SELECT (auth.jwt() ->> 'sub'::text)) = user_id)" + }, + "Users can update their own profile": { + "name": "Users can update their own profile", + "as": "PERMISSIVE", + "for": "UPDATE", + "to": [ + "authenticated" + ] + }, + "Users can view their own profile": { + "name": "Users can view their own profile", + "as": "PERMISSIVE", + "for": "SELECT", + "to": [ + "authenticated" + ] + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_verification_identifier": { + "name": "idx_verification_identifier", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_collaborators": { + "name": "workspace_collaborators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'editor'" + }, + "invite_token": { + "name": "invite_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_opened_at": { + "name": "last_opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_workspace_collaborators_lookup": { + "name": "idx_workspace_collaborators_lookup", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_collaborators_workspace": { + "name": "idx_workspace_collaborators_workspace", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_collaborators_last_opened_at": { + "name": "idx_workspace_collaborators_last_opened_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "last_opened_at", + "isExpression": false, + "asc": false, + "nulls": "first", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_collaborators_workspace_id_fkey": { + "name": "workspace_collaborators_workspace_id_fkey", + "tableFrom": "workspace_collaborators", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_collaborators_invite_token_unique": { + "name": "workspace_collaborators_invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "invite_token" + ] + }, + "workspace_collaborators_workspace_user_unique": { + "name": "workspace_collaborators_workspace_user_unique", + "nullsNotDistinct": false, + "columns": [ + "workspace_id", + "user_id" + ] + } + }, + "policies": { + "Owners can manage collaborators": { + "name": "Owners can manage collaborators", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "authenticated" + ], + "using": "(EXISTS ( SELECT 1\n FROM workspaces w\n WHERE ((w.id = workspace_collaborators.workspace_id) AND (w.user_id = (auth.jwt() ->> 'sub'::text)))))" + }, + "Collaborators can view their access": { + "name": "Collaborators can view their access", + "as": "PERMISSIVE", + "for": "SELECT", + "to": [ + "authenticated" + ], + "using": "(user_id = (auth.jwt() ->> 'sub'::text))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_events": { + "name": "workspace_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_workspace_events_event_id": { + "name": "idx_workspace_events_event_id", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_events_timestamp": { + "name": "idx_workspace_events_timestamp", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int8_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_events_user_name": { + "name": "idx_workspace_events_user_name", + "columns": [ + { + "expression": "user_name", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_events_workspace": { + "name": "idx_workspace_events_workspace", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_events_workspace_id_fkey": { + "name": "workspace_events_workspace_id_fkey", + "tableFrom": "workspace_events", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_events_event_id_key": { + "name": "workspace_events_event_id_key", + "nullsNotDistinct": false, + "columns": [ + "event_id" + ] + } + }, + "policies": { + "Users can insert workspace events they have write access to": { + "name": "Users can insert workspace events they have write access to", + "as": "PERMISSIVE", + "for": "INSERT", + "to": [ + "public" + ], + "withCheck": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_events.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_events.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))" + }, + "Users can read workspace events they have access to": { + "name": "Users can read workspace events they have access to", + "as": "PERMISSIVE", + "for": "SELECT", + "to": [ + "public" + ], + "using": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_events.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_events.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)))))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invites": { + "name": "workspace_invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'editor'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_workspace_invites_token": { + "name": "idx_workspace_invites_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_invites_email": { + "name": "idx_workspace_invites_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_invites_workspace": { + "name": "idx_workspace_invites_workspace", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_invites_workspace_id_fkey": { + "name": "workspace_invites_workspace_id_fkey", + "tableFrom": "workspace_invites", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invites_token_key": { + "name": "workspace_invites_token_key", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": { + "Public can view invite by token": { + "name": "Public can view invite by token", + "as": "PERMISSIVE", + "for": "SELECT", + "to": [ + "public" + ], + "using": "true" + }, + "Users can insert invites for workspaces they own/edit": { + "name": "Users can insert invites for workspaces they own/edit", + "as": "PERMISSIVE", + "for": "INSERT", + "to": [ + "authenticated" + ], + "withCheck": "(EXISTS ( SELECT 1\n FROM workspaces w\n WHERE ((w.id = workspace_invites.workspace_id) AND (w.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_invites.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_item_content": { + "name": "workspace_item_content", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data_schema_version": { + "name": "data_schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "text_content": { + "name": "text_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "structured_data": { + "name": "structured_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "asset_data": { + "name": "asset_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "embed_data": { + "name": "embed_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_data": { + "name": "source_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_workspace_item_content_workspace": { + "name": "idx_workspace_item_content_workspace", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_item_content_workspace_item_fkey": { + "name": "workspace_item_content_workspace_item_fkey", + "tableFrom": "workspace_item_content", + "tableTo": "workspace_items", + "columnsFrom": [ + "workspace_id", + "item_id" + ], + "columnsTo": [ + "workspace_id", + "item_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_item_content_pkey": { + "name": "workspace_item_content_pkey", + "columns": [ + "workspace_id", + "item_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": { + "Users can read workspace item content they have access to": { + "name": "Users can read workspace item content they have access to", + "as": "PERMISSIVE", + "for": "SELECT", + "to": [ + "public" + ], + "using": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_content.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_content.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)))))" + }, + "Users can write workspace item content they have write access to": { + "name": "Users can write workspace item content they have write access to", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "public" + ], + "using": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_content.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_content.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))", + "withCheck": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_content.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_content.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_item_extracted": { + "name": "workspace_item_extracted", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "search_text": { + "name": "search_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "content_preview": { + "name": "content_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ocr_text": { + "name": "ocr_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transcript_text": { + "name": "transcript_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ocr_pages": { + "name": "ocr_pages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "transcript_segments": { + "name": "transcript_segments", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_workspace_item_extracted_workspace": { + "name": "idx_workspace_item_extracted_workspace", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_item_extracted_workspace_item_fkey": { + "name": "workspace_item_extracted_workspace_item_fkey", + "tableFrom": "workspace_item_extracted", + "tableTo": "workspace_items", + "columnsFrom": [ + "workspace_id", + "item_id" + ], + "columnsTo": [ + "workspace_id", + "item_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_item_extracted_pkey": { + "name": "workspace_item_extracted_pkey", + "columns": [ + "workspace_id", + "item_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": { + "Users can read workspace item extracted data they have access to": { + "name": "Users can read workspace item extracted data they have access to", + "as": "PERMISSIVE", + "for": "SELECT", + "to": [ + "public" + ], + "using": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_extracted.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_extracted.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)))))" + }, + "Users can write workspace item extracted data they have write access to": { + "name": "Users can write workspace item extracted data they have write access to", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "public" + ], + "using": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_extracted.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_extracted.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))", + "withCheck": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_extracted.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_extracted.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_item_projection_state": { + "name": "workspace_item_projection_state", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "last_applied_version": { + "name": "last_applied_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_workspace_item_projection_state_version": { + "name": "idx_workspace_item_projection_state_version", + "columns": [ + { + "expression": "last_applied_version", + "isExpression": false, + "asc": false, + "nulls": "first", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_item_projection_state_workspace_id_fkey": { + "name": "workspace_item_projection_state_workspace_id_fkey", + "tableFrom": "workspace_item_projection_state", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "Users can read workspace item projection state they have access to": { + "name": "Users can read workspace item projection state they have access to", + "as": "PERMISSIVE", + "for": "SELECT", + "to": [ + "public" + ], + "using": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_projection_state.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_projection_state.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)))))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_item_user_state": { + "name": "workspace_item_user_state", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'item'" + }, + "state_type": { + "name": "state_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_schema_version": { + "name": "state_schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_workspace_item_user_state_workspace_user": { + "name": "idx_workspace_item_user_state_workspace_user", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_item_user_state_item": { + "name": "idx_workspace_item_user_state_item", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_item_user_state_workspace_item_fkey": { + "name": "workspace_item_user_state_workspace_item_fkey", + "tableFrom": "workspace_item_user_state", + "tableTo": "workspace_items", + "columnsFrom": [ + "workspace_id", + "item_id" + ], + "columnsTo": [ + "workspace_id", + "item_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_item_user_state_pkey": { + "name": "workspace_item_user_state_pkey", + "columns": [ + "workspace_id", + "item_id", + "user_id", + "state_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": { + "Users can read their workspace item user state": { + "name": "Users can read their workspace item user state", + "as": "PERMISSIVE", + "for": "SELECT", + "to": [ + "public" + ], + "using": "((workspace_item_user_state.user_id = (auth.jwt() ->> 'sub'::text)) AND ((EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_user_state.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_user_state.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)))))))" + }, + "Users can write their workspace item user state": { + "name": "Users can write their workspace item user state", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "public" + ], + "using": "((workspace_item_user_state.user_id = (auth.jwt() ->> 'sub'::text)) AND ((EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_user_state.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_user_state.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)))))))", + "withCheck": "((workspace_item_user_state.user_id = (auth.jwt() ->> 'sub'::text)) AND ((EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_user_state.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_user_state.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)))))))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_items": { + "name": "workspace_items", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "layout": { + "name": "layout", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_modified": { + "name": "last_modified", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_version": { + "name": "source_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "data_schema_version": { + "name": "data_schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_ocr": { + "name": "has_ocr", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ocr_status": { + "name": "ocr_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ocr_page_count": { + "name": "ocr_page_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "has_transcript": { + "name": "has_transcript", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "source_count": { + "name": "source_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_workspace_items_workspace": { + "name": "idx_workspace_items_workspace", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_items_workspace_folder": { + "name": "idx_workspace_items_workspace_folder", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_items_workspace_type": { + "name": "idx_workspace_items_workspace_type", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_items_workspace_updated": { + "name": "idx_workspace_items_workspace_updated", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "first", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_items_workspace_version": { + "name": "idx_workspace_items_workspace_version", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "source_version", + "isExpression": false, + "asc": false, + "nulls": "first", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_items_workspace_processing": { + "name": "idx_workspace_items_workspace_processing", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_items_workspace_ocr": { + "name": "idx_workspace_items_workspace_ocr", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "has_ocr", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bool_ops" + }, + { + "expression": "ocr_status", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_items_workspace_ocr_page_count": { + "name": "idx_workspace_items_workspace_ocr_page_count", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "ocr_page_count", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_items_workspace_has_transcript": { + "name": "idx_workspace_items_workspace_has_transcript", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "has_transcript", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bool_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_items_workspace_source_count": { + "name": "idx_workspace_items_workspace_source_count", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "source_count", + "isExpression": false, + "asc": false, + "nulls": "first", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_items_workspace_id_fkey": { + "name": "workspace_items_workspace_id_fkey", + "tableFrom": "workspace_items", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_items_pkey": { + "name": "workspace_items_pkey", + "columns": [ + "workspace_id", + "item_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": { + "Users can read workspace items they have access to": { + "name": "Users can read workspace items they have access to", + "as": "PERMISSIVE", + "for": "SELECT", + "to": [ + "public" + ], + "using": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_items.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_items.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)))))" + }, + "Users can write workspace items they have write access to": { + "name": "Users can write workspace items they have write access to", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "public" + ], + "using": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_items.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_items.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))", + "withCheck": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_items.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_items.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_share_links": { + "name": "workspace_share_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'editor'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_workspace_share_links_token": { + "name": "idx_workspace_share_links_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_share_links_workspace": { + "name": "idx_workspace_share_links_workspace", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_share_links_workspace_id_fkey": { + "name": "workspace_share_links_workspace_id_fkey", + "tableFrom": "workspace_share_links", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_share_links_token_key": { + "name": "workspace_share_links_token_key", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + }, + "workspace_share_links_workspace_key": { + "name": "workspace_share_links_workspace_key", + "nullsNotDistinct": false, + "columns": [ + "workspace_id" + ] + } + }, + "policies": { + "Public can view share link by token": { + "name": "Public can view share link by token", + "as": "PERMISSIVE", + "for": "SELECT", + "to": [ + "public" + ], + "using": "true" + }, + "Owners and editors can manage share links": { + "name": "Owners and editors can manage share links", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "authenticated" + ], + "using": "(EXISTS ( SELECT 1\n FROM workspaces w\n WHERE ((w.id = workspace_share_links.workspace_id) AND (w.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_share_links.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_snapshots": { + "name": "workspace_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_version": { + "name": "snapshot_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "event_count": { + "name": "event_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_workspace_snapshots_version": { + "name": "idx_workspace_snapshots_version", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "snapshot_version", + "isExpression": false, + "asc": false, + "nulls": "first", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspace_snapshots_workspace": { + "name": "idx_workspace_snapshots_workspace", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_snapshots_workspace_id_fkey": { + "name": "workspace_snapshots_workspace_id_fkey", + "tableFrom": "workspace_snapshots", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_snapshots_workspace_id_snapshot_version_key": { + "name": "workspace_snapshots_workspace_id_snapshot_version_key", + "nullsNotDistinct": false, + "columns": [ + "workspace_id", + "snapshot_version" + ] + } + }, + "policies": { + "Service role can insert workspace snapshots": { + "name": "Service role can insert workspace snapshots", + "as": "PERMISSIVE", + "for": "INSERT", + "to": [ + "public" + ], + "withCheck": "true" + }, + "Users can read workspace snapshots they have access to": { + "name": "Users can read workspace snapshots they have access to", + "as": "PERMISSIVE", + "for": "SELECT", + "to": [ + "public" + ] + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspaces": { + "name": "workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'blank'" + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_opened_at": { + "name": "last_opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_workspaces_created_at": { + "name": "idx_workspaces_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "first", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspaces_slug": { + "name": "idx_workspaces_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspaces_user_id": { + "name": "idx_workspaces_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspaces_user_slug": { + "name": "idx_workspaces_user_slug", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspaces_user_sort_order": { + "name": "idx_workspaces_user_sort_order", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workspaces_last_opened_at": { + "name": "idx_workspaces_last_opened_at", + "columns": [ + { + "expression": "last_opened_at", + "isExpression": false, + "asc": false, + "nulls": "first", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "Users can delete their own workspaces": { + "name": "Users can delete their own workspaces", + "as": "PERMISSIVE", + "for": "DELETE", + "to": [ + "authenticated" + ], + "using": "(( SELECT (auth.jwt() ->> 'sub'::text)) = user_id)" + }, + "Users can insert their own workspaces": { + "name": "Users can insert their own workspaces", + "as": "PERMISSIVE", + "for": "INSERT", + "to": [ + "authenticated" + ] + }, + "Users can update their own workspaces": { + "name": "Users can update their own workspaces", + "as": "PERMISSIVE", + "for": "UPDATE", + "to": [ + "authenticated" + ] + }, + "Users can view their own workspaces": { + "name": "Users can view their own workspaces", + "as": "PERMISSIVE", + "for": "SELECT", + "to": [ + "authenticated" + ] + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index c5478bf0..1d02e425 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1775793600000, "tag": "0002_zero_publication", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1776581304199, + "tag": "0003_drop_chat_tables", + "breakpoints": true } ] } diff --git a/package.json b/package.json index 92ea88fa..f967d4dd 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "private": true, "scripts": { - "dev": "concurrently \"next dev\" \"npx @ai-sdk/devtools\"", + "dev": "next dev", "build": "next build", "start": "next start", "lint": "oxlint", @@ -30,14 +30,8 @@ }, "packageManager": "pnpm@10.14.0", "dependencies": { - "@ai-sdk/devtools": "^0.0.15", "@ai-sdk/google": "^3.0.62", - "@assistant-ui/react": "^0.12.24", - "@assistant-ui/react-ai-sdk": "^1.3.18", - "@assistant-ui/react-devtools": "^1.0.5", - "@assistant-ui/react-markdown": "^0.12.8", "@contextcompany/otel": "^1.0.15", - "@e2b/code-interpreter": "^2.4.0", "@embedpdf/core": "^2.14.0", "@embedpdf/engines": "^2.14.0", "@embedpdf/models": "^2.14.0", @@ -64,7 +58,6 @@ "@opentelemetry/api": "^1.9.1", "@opentelemetry/api-logs": "^0.214.0", "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", - "@posthog/ai": "^7.14.0", "@posthog/nextjs-config": "^1.9.2", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", @@ -89,7 +82,6 @@ "@streamdown/math": "^1.0.2", "@streamdown/mermaid": "^1.0.2", "@supabase/supabase-js": "^2.101.1", - "@supermemory/tools": "^1.4.3", "@tanstack/pacer": "^0.20.1", "@tanstack/react-pacer": "^0.21.1", "@tanstack/react-query": "^5.96.1", @@ -116,7 +108,6 @@ "@workflow/errors": "4.1.0", "ai": "^6.0.158", "assemblyai": "^4.29.0", - "assistant-stream": "^0.3.10", "better-auth": "^1.6.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -128,7 +119,6 @@ "heic-to": "^1.4.2", "jsonrepair": "^3.13.2", "katex": "^0.16.45", - "lodash.throttle": "^4.1.1", "lucide-react": "^1.7.0", "next": "16.2.3", "next-themes": "^0.4.6", @@ -150,7 +140,6 @@ "react-quizlet-flashcard": "^4.0.22", "react-resizable-panels": "^4.9.0", "react-shiki": "^0.9.1", - "react-speech-recognition": "^4.0.1", "regenerator-runtime": "^0.14.1", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", @@ -170,16 +159,13 @@ "devDependencies": { "@tailwindcss/postcss": "^4.2.1", "@types/bun": "^1.3.11", - "@types/lodash.throttle": "^4.1.9", "@types/node": "^25.3.0", "@types/pg": "^8.16.0", "@types/react": "19.2.14", "@types/react-color": "^3.0.13", "@types/react-dom": "19.2.3", - "@types/react-speech-recognition": "^3.9.6", "@typescript/native-preview": "7.0.0-dev.20260408.1", "babel-plugin-react-compiler": "^1.0.0", - "concurrently": "^9.2.1", "cross-env": "^10.1.0", "drizzle-zero": "^0.18.0", "knip": "^6.0.6", diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts deleted file mode 100644 index dc2e83b4..00000000 --- a/src/app/api/chat/route.ts +++ /dev/null @@ -1,403 +0,0 @@ -import { - streamText, - smoothStream, - convertToModelMessages, - pruneMessages, - safeValidateUIMessages, - stepCountIs, - wrapLanguageModel, -} from "ai"; -import { devToolsMiddleware } from "@ai-sdk/devtools"; -import { withTracing } from "@posthog/ai"; -import type { UIMessage } from "ai"; -import { logger } from "@/lib/utils/logger"; -import { auth } from "@/lib/auth"; -import { headers } from "next/headers"; -import { createChatTools } from "@/lib/ai/tools"; -import { - capturePostHogServerException, - getPostHogServerClient, -} from "@/lib/posthog-server"; -import { withServerObservability } from "@/lib/with-server-observability"; -import { normalizeLegacyToolMessages } from "@/lib/ai/legacy-tool-message-compat"; -import { maybeWithSupermemory } from "@/lib/ai/supermemory"; -import type { ReplySelection } from "@/lib/stores/ui-store"; -import { getDefaultChatModelId, resolveGatewayModelId } from "@/lib/ai/models"; -import { - buildGatewayProviderOptions, - createGatewayLanguageModel, - getGatewayAttributionHeaders, -} from "@/lib/ai/gateway-provider-options"; - -/** - * Extract workspaceId from system context or request body - */ -function extractWorkspaceId(body: any): string | null { - if (body.workspaceId) { - return body.workspaceId; - } - - const system = body.system || ""; - const workspaceIdMatch = system.match(/Workspace ID: ([a-f0-9-]{36})/); - if (workspaceIdMatch) { - return workspaceIdMatch[1]; - } - - return null; -} - -/** - * Selected cards context is now formatted on the client side and sent directly. - * This eliminates the need for server-side database fetch. - * If selectedCardsContext is provided, use it; otherwise return empty string. - */ -function getSelectedCardsContext(body: any): string { - // Client now sends pre-formatted context string - return body.selectedCardsContext || ""; -} - -/** - * Inject user-selected context (selected cards + reply quotes / workspace passages) into the last user message. - * `custom` is body.metadata.custom from the composer's runConfig (replySelections only). - */ -function injectSelectionContext( - messages: any[], - custom?: { - replySelections?: ReplySelection[]; - }, - selectedCardsContext?: string, -): void { - const parts: string[] = []; - - // Selected cards (pre-formatted from client) - if (selectedCardsContext && selectedCardsContext.trim()) { - parts.push(`[Selected cards context:\n${selectedCardsContext.trim()}]`); - } - - if (custom?.replySelections && custom.replySelections.length > 0) { - const quoted = custom.replySelections - .map((sel) => - sel.title ? `> From: ${sel.title}\n> ${sel.text}` : `> ${sel.text}`, - ) - .join("\n\n"); - parts.push(`[Referring to:\n${quoted}]`); - } - - if (parts.length === 0) return; - - const prefix = parts.join("\n") + "\n\n"; - - // Find the last user message and prepend the context - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; - if (msg.role !== "user") continue; - - if (Array.isArray(msg.content)) { - const textIdx = msg.content.findIndex((p: any) => p.type === "text"); - if (textIdx !== -1) { - msg.content[textIdx] = { - ...msg.content[textIdx], - text: prefix + msg.content[textIdx].text, - }; - } - } else if (typeof msg.content === "string") { - messages[i] = { ...msg, content: prefix + msg.content }; - } - break; - } -} - -async function handlePOST(req: Request) { - let workspaceId: string | null = null; - let userId: string | null = null; - let activeFolderId: string | undefined; - - // Check for API key early (Standardizing on Google Key for now if not using OIDC) - // With Gateway, you can check for other keys too, or rely on Gateway's auth - if ( - !process.env.GOOGLE_GENERATIVE_AI_API_KEY && - !process.env.AI_GATEWAY_API_KEY - ) { - // Optional: make this check more robust or permissive if using OIDC - } - - try { - // FIX: Parallelize headers() and req.json() to eliminate waterfall - const [headersObj, body] = await Promise.all([headers(), req.json()]); - - // Get authenticated user ID - const session = await auth.api.getSession({ headers: headersObj }); - userId = session?.user?.id || null; - - const { messages = [] }: { messages?: UIMessage[] } = body; - const system = body.system || ""; - workspaceId = extractWorkspaceId(body); - activeFolderId = body.activeFolderId; - // AssistantChatTransport passes thread remoteId as body.id (see assistant-ui react-ai-sdk) - const threadId = body.id ?? body.threadId ?? null; - // Client-controlled memory toggle (composer settings menu). Server double-checks auth/api key. - const memoryEnabled = body.memoryEnabled === true; - logger.info("🧵 [CHAT-API] Thread ID:", { - threadId, - isDefault: threadId === "DEFAULT_THREAD_ID", - }); - - // Create tools using the modular factory (before convertToModelMessages so - // toModelOutput can sanitize historical tool results for the model) - const tools = createChatTools({ - workspaceId, - userId, - activeFolderId, - threadId, - clientTools: body.tools, - }); - - const compatibleMessages = normalizeLegacyToolMessages(messages, { - availableToolNames: Object.keys(tools), - }); - - const validation = await safeValidateUIMessages({ - messages: compatibleMessages, - tools, - }); - - if (!validation.success) { - throw validation.error; - } - - const validatedMessages = validation.data; - - // Convert messages (pass tools so toModelOutput strips event from historical tool results) - let convertedMessages; - try { - convertedMessages = await convertToModelMessages(validatedMessages, { - tools, - }); - } catch (convertError) { - logger.error("❌ [CHAT-API] convertToModelMessages FAILED:", { - error: - convertError instanceof Error - ? convertError.message - : String(convertError), - stack: convertError instanceof Error ? convertError.stack : undefined, - }); - throw convertError; - } - - // Prune older reasoning and tool calls to save context - convertedMessages = pruneMessages({ - messages: convertedMessages, - reasoning: "before-last-message", - toolCalls: "before-last-5-messages", - emptyMessages: "remove", - }); - - // Get pre-formatted selected cards context from client (no DB fetch needed) - const selectedCardsContext = getSelectedCardsContext(body); - - const modelId = resolveGatewayModelId( - body.modelId || getDefaultChatModelId(), - ); - - // Inject selected cards + reply selections into the last user message - injectSelectionContext( - convertedMessages, - body.metadata?.custom, - selectedCardsContext, - ); - - const posthogClient = getPostHogServerClient(); - const baseGatewayModel = createGatewayLanguageModel(modelId); - const tracedModel = posthogClient - ? withTracing(baseGatewayModel as any, posthogClient, { - posthogDistinctId: userId || "anonymous", - posthogProperties: { - workspaceId, - activeFolderId, - modelId, - memoryEnabled, - }, - }) - : (baseGatewayModel as any); - - // Supermemory: personalizes prompts via user profile + search, and - // auto-saves conversation turns when userId + SUPERMEMORY_API_KEY present. - // Sits inside wrapLanguageModel (devtools stays outermost) and outside - // withTracing so PostHog telemetry captures the *final* prompt the model - // actually receives (with memories already injected). - const memoryWrappedModel = maybeWithSupermemory(tracedModel, { - userId: userId ?? "", - threadId, - memoryEnabled, - }); - - // Use AI Gateway - const model = wrapLanguageModel({ - model: memoryWrappedModel, - middleware: - process.env.NODE_ENV === "development" ? devToolsMiddleware() : [], - }); - - // Stream the response - logger.debug("🔍 [CHAT-API] Final messages before streamText:", { - count: convertedMessages.length, - modelId, - }); - - const providerOptions = buildGatewayProviderOptions(modelId, { userId }); - - const result = streamText({ - model: model, - temperature: 1.0, - system, - messages: convertedMessages, - stopWhen: stepCountIs(25), - tools, - providerOptions: providerOptions as any, - headers: getGatewayAttributionHeaders(), - experimental_telemetry: { - isEnabled: true, - metadata: { - "tcc.conversational": "true", - ...(threadId ? { "tcc.sessionId": String(threadId) } : {}), - ...(userId ? { userId } : {}), - }, - }, - experimental_transform: smoothStream({ chunking: "word", delayInMs: 15 }), - onFinish: ({ usage, finishReason }) => { - const usageInfo = { - inputTokens: usage?.inputTokens, - outputTokens: usage?.outputTokens, - totalTokens: usage?.totalTokens, - cachedInputTokens: usage?.cachedInputTokens, // Standard property - reasoningTokens: usage?.reasoningTokens, - // Note: Extended provider-specific properties might not be available consistently via Gateway - finishReason, - }; - - logger.info("📊 [CHAT-API] Final Token Usage:", usageInfo); - }, - onStepFinish: (result) => { - // stepType exists in runtime but may not be in type definitions - const stepResult = result as typeof result & { - stepType?: "initial" | "continue" | "tool-result"; - }; - const { stepType, usage, finishReason } = stepResult; - - if (usage) { - const stepUsageInfo = { - stepType: stepType || "unknown", - inputTokens: usage?.inputTokens, - outputTokens: usage?.outputTokens, - totalTokens: usage?.totalTokens, - cachedInputTokens: usage?.cachedInputTokens, // Standard property - reasoningTokens: usage?.reasoningTokens, - finishReason, - }; - - logger.debug( - `📊 [CHAT-API] Step Usage (${stepType || "unknown"}):`, - stepUsageInfo, - ); - } - }, - }); - - logger.debug( - "🔍 [CHAT-API] streamText returned, calling toUIMessageStreamResponse...", - ); - // Log which provider the Gateway actually used (resolves when stream completes) - void Promise.resolve((result as any).providerMetadata).then((meta: any) => { - const provider = - meta?.gateway?.routing?.resolvedProvider ?? - meta?.gateway?.routing?.finalProvider; - if (provider) { - logger.info("🔍 [CHAT-API] Gateway resolved provider:", provider); - } - }); - // assistant-ui already persists and rehydrates message history via the - // thread history adapter. Passing originalMessages here enables a second - // persistence flow in AI SDK that can relink the same ids into a different - // parent chain when history loads, triggering duplicate-id repository errors. - const response = result.toUIMessageStreamResponse(); - logger.debug("🔍 [CHAT-API] toUIMessageStreamResponse succeeded"); - return response; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - - // Detect timeout errors - const isTimeout = - errorMessage.includes("timeout") || - errorMessage.includes("TIMEOUT") || - errorMessage.includes("Function execution exceeded") || - errorMessage.includes("Execution timeout") || - (error && - typeof error === "object" && - "code" in error && - error.code === "TIMEOUT"); - - if (isTimeout) { - logger.error("⏱️ [CHAT-API] Request timed out after 30 seconds", { - errorMessage, - workspaceId, - }); - - capturePostHogServerException(error, { - distinctId: userId ?? undefined, - properties: { - route_name: "POST /api/chat", - workspaceId: workspaceId ?? undefined, - chat_error_kind: "timeout", - }, - }); - - return new Response( - JSON.stringify({ - error: "Request timeout", - message: - "The request took too long to process (exceeded 30 seconds). This can happen with complex queries that require multiple tool calls or extensive processing. Please try breaking your question into smaller parts or simplifying your request.", - code: "TIMEOUT", - }), - { - status: 504, - headers: { "Content-Type": "application/json" }, - }, - ); - } - - // Log other errors - logger.error("❌ [CHAT-API] Error processing request", { - errorMessage, - errorStack: error instanceof Error ? error.stack : undefined, - workspaceId, - }); - - capturePostHogServerException(error, { - distinctId: userId ?? undefined, - properties: { - route_name: "POST /api/chat", - workspaceId: workspaceId ?? undefined, - chat_error_kind: "internal", - }, - }); - - return new Response( - JSON.stringify({ - error: "Internal server error", - message: - "An unexpected error occurred while processing your request. Please try again.", - details: - process.env.NODE_ENV === "development" ? errorMessage : undefined, - code: "INTERNAL_ERROR", - }), - { - status: 500, - headers: { "Content-Type": "application/json" }, - }, - ); - } -} - -export const POST = withServerObservability(handlePOST, { - routeName: "POST /api/chat", -}); diff --git a/src/app/api/flashcards/create/route.ts b/src/app/api/flashcards/create/route.ts deleted file mode 100644 index 2a0402b1..00000000 --- a/src/app/api/flashcards/create/route.ts +++ /dev/null @@ -1,68 +0,0 @@ - -import { workspaceWorker } from "@/lib/ai/workers"; -import { auth } from "@/lib/auth"; -import { logger } from "@/lib/utils/logger"; -import { headers } from "next/headers"; -import { z } from "zod"; -import { flashcardCardInputSchema } from "@/lib/workspace-state/item-data-schemas"; - -const createFlashcardSchema = z.object({ - workspaceId: z.string().uuid(), - title: z.string().min(1), - cards: z.array(flashcardCardInputSchema).min(1), -}); - -export async function POST(req: Request) { - try { - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session) { - return new Response("Unauthorized", { status: 401 }); - } - - const body = await req.json(); - const result = createFlashcardSchema.safeParse(body); - - if (!result.success) { - return new Response(JSON.stringify({ error: "Invalid request body", details: result.error }), { - status: 400, - headers: { "Content-Type": "application/json" }, - }); - } - - const { workspaceId, title, cards } = result.data; - - logger.info("🎴 [API] Creating flashcard deck:", { workspaceId, title, cardCount: cards.length }); - - const workerResult = await workspaceWorker("create", { - workspaceId, - title, - itemType: "flashcard", - flashcardData: { - cards - }, - - }); - - if (!workerResult.success) { - return new Response(JSON.stringify({ error: workerResult.message }), { - status: 500, - headers: { "Content-Type": "application/json" }, - }); - } - - return new Response(JSON.stringify(workerResult), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - - } catch (error) { - logger.error("❌ [API] Error creating flashcard deck:", error); - return new Response(JSON.stringify({ error: "Internal server error" }), { - status: 500, - headers: { "Content-Type": "application/json" }, - }); - } -} diff --git a/src/app/api/threads/[id]/archive/route.ts b/src/app/api/threads/[id]/archive/route.ts deleted file mode 100644 index cf4dabb8..00000000 --- a/src/app/api/threads/[id]/archive/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { setThreadArchived } from "@/lib/api/thread-archive"; -import { withServerObservability } from "@/lib/with-server-observability"; - -/** - * POST /api/threads/[id]/archive - * Archive a thread - */ -export const POST = withServerObservability(async function POST( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - try { - const { id } = await params; - return await setThreadArchived(id, true); - } catch (error) { - if (error instanceof Response) return error; - console.error("[threads] archive error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); - } -}, { routeName: "POST /api/threads/[id]/archive" }); diff --git a/src/app/api/threads/[id]/messages/[messageId]/route.ts b/src/app/api/threads/[id]/messages/[messageId]/route.ts deleted file mode 100644 index ef810028..00000000 --- a/src/app/api/threads/[id]/messages/[messageId]/route.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { db } from "@/lib/db/client"; -import { chatThreads, chatMessages } from "@/lib/db/schema"; -import { - requireAuth, - verifyWorkspaceAccess, - verifyThreadOwnership, -} from "@/lib/api/workspace-helpers"; -import { eq, and } from "drizzle-orm"; -import { withServerObservability } from "@/lib/with-server-observability"; - -async function getThreadAndVerify(threadId: string, userId: string) { - const [thread] = await db - .select() - .from(chatThreads) - .where(eq(chatThreads.id, threadId)) - .limit(1); - - if (!thread) { - throw NextResponse.json({ error: "Thread not found" }, { status: 404 }); - } - - await verifyWorkspaceAccess(thread.workspaceId, userId); - verifyThreadOwnership(thread, userId); - - return thread; -} - -/** - * PATCH /api/threads/[id]/messages/[messageId] - * Update an existing message (e.g. step timestamps/duration from useExternalHistory) - */ -export const PATCH = withServerObservability(async function PATCH( - req: NextRequest, - { params }: { params: Promise<{ id: string; messageId: string }> } -) { - try { - const userId = await requireAuth(); - const { id: threadId, messageId } = await params; - const body = await req.json().catch(() => ({})); - const { content } = body; - - if (content === undefined) { - return NextResponse.json( - { error: "content is required" }, - { status: 400 } - ); - } - - await getThreadAndVerify(threadId, userId); - - const [updated] = await db - .update(chatMessages) - .set({ - content: typeof content === "object" ? content : { raw: content }, - }) - .where( - and( - eq(chatMessages.threadId, threadId), - eq(chatMessages.messageId, messageId) - ) - ) - .returning({ messageId: chatMessages.messageId }); - - if (!updated) { - return NextResponse.json({ error: "Message not found" }, { status: 404 }); - } - - return NextResponse.json({ ok: true }); - } catch (error) { - if (error instanceof Response) return error; - console.error("[threads] messages PATCH error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); - } -}, { routeName: "PATCH /api/threads/[id]/messages/[messageId]" }); diff --git a/src/app/api/threads/[id]/messages/route.ts b/src/app/api/threads/[id]/messages/route.ts deleted file mode 100644 index cf9a2113..00000000 --- a/src/app/api/threads/[id]/messages/route.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { db } from "@/lib/db/client"; -import { chatThreads, chatMessages } from "@/lib/db/schema"; -import { - requireAuth, - verifyWorkspaceAccess, - verifyThreadOwnership, -} from "@/lib/api/workspace-helpers"; -import { eq, and, desc } from "drizzle-orm"; -import { withServerObservability } from "@/lib/with-server-observability"; - -async function getThreadAndVerify(id: string, userId: string) { - const [thread] = await db - .select() - .from(chatThreads) - .where(eq(chatThreads.id, id)) - .limit(1); - - if (!thread) { - throw NextResponse.json({ error: "Thread not found" }, { status: 404 }); - } - - await verifyWorkspaceAccess(thread.workspaceId, userId); - verifyThreadOwnership(thread, userId); - - return thread; -} - -/** - * GET /api/threads/[id]/messages?format=ai-sdk/v6 - * Load messages for a thread. Format filter is strict (ai-sdk/v6 only). - */ -export const GET = withServerObservability(async function GET( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - try { - const userId = await requireAuth(); - const { id } = await params; - const { searchParams } = new URL(req.url); - const format = searchParams.get("format") ?? "ai-sdk/v6"; - - const thread = await getThreadAndVerify(id, userId); - - const rows = await db - .select() - .from(chatMessages) - .where(and(eq(chatMessages.threadId, id), eq(chatMessages.format, format))) - .orderBy(desc(chatMessages.createdAt)); - - const messages = rows.map((r) => ({ - id: r.messageId, - parent_id: r.parentId, - format: r.format, - content: r.content, - created_at: r.createdAt, - })); - - return NextResponse.json({ - messages, - headId: thread.headMessageId ?? undefined, - }); - } catch (error) { - if (error instanceof Response) return error; - console.error("[threads] messages GET error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); - } -}, { routeName: "GET /api/threads/[id]/messages" }); - -/** - * POST /api/threads/[id]/messages - * Append a message to a thread - */ -export const POST = withServerObservability(async function POST( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - try { - const userId = await requireAuth(); - const { id } = await params; - const body = await req.json().catch(() => ({})); - const { messageId, parentId, format, content } = body; - - if (!messageId || !format || content === undefined) { - return NextResponse.json( - { error: "messageId, format, and content are required" }, - { status: 400 } - ); - } - - const thread = await getThreadAndVerify(id, userId); - - try { - await db.transaction(async (tx) => { - await tx.insert(chatMessages).values({ - threadId: id, - messageId: String(messageId), - parentId: parentId ?? null, - format: String(format), - content: typeof content === "object" ? content : { raw: content }, - }); - - // Only update headMessageId when appending to the current head - // (avoids overwriting explicit branch head set via PATCH) - const shouldUpdateHead = - thread.headMessageId == null || - (parentId != null && parentId === thread.headMessageId); - const updates: { - lastMessageAt: string; - updatedAt: string; - headMessageId?: string; - } = { - lastMessageAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - if (shouldUpdateHead) updates.headMessageId = String(messageId); - - await tx - .update(chatThreads) - .set(updates) - .where(eq(chatThreads.id, id)); - }); - } catch (txError: unknown) { - const err = txError as { code?: string }; - if (err?.code === "23505") { - return NextResponse.json( - { error: "Message already exists (duplicate messageId)" }, - { status: 409 } - ); - } - throw txError; - } - - return NextResponse.json({ ok: true }); - } catch (error) { - if (error instanceof Response) return error; - console.error("[threads] messages POST error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); - } -}, { routeName: "POST /api/threads/[id]/messages" }); diff --git a/src/app/api/threads/[id]/route.ts b/src/app/api/threads/[id]/route.ts deleted file mode 100644 index b55ae6c1..00000000 --- a/src/app/api/threads/[id]/route.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { db } from "@/lib/db/client"; -import { chatThreads, chatMessages } from "@/lib/db/schema"; -import { - requireAuth, - verifyWorkspaceAccess, - verifyThreadOwnership, -} from "@/lib/api/workspace-helpers"; -import { eq, and } from "drizzle-orm"; -import { withServerObservability } from "@/lib/with-server-observability"; - -async function getThreadAndVerify( - id: string, - userId: string, - permission: "viewer" | "editor" = "viewer" -) { - const [thread] = await db - .select() - .from(chatThreads) - .where(eq(chatThreads.id, id)) - .limit(1); - - if (!thread) { - throw NextResponse.json({ error: "Thread not found" }, { status: 404 }); - } - - await verifyWorkspaceAccess(thread.workspaceId, userId, permission); - verifyThreadOwnership(thread, userId); - - return thread; -} - -/** - * GET /api/threads/[id] - * Fetch a single thread - */ -export const GET = withServerObservability(async function GET( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - const userId = await requireAuth(); - const { id } = await params; - - const thread = await getThreadAndVerify(id, userId); - - return NextResponse.json({ - id: thread.id, - remoteId: thread.id, - status: thread.isArchived ? "archived" : "regular", - title: thread.title ?? undefined, - externalId: thread.externalId ?? undefined, - }); -}, { routeName: "GET /api/threads/[id]" }); - -/** - * PATCH /api/threads/[id] - * Update thread (e.g. rename) - */ -export const PATCH = withServerObservability(async function PATCH( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - try { - const userId = await requireAuth(); - const { id } = await params; - const body = await req.json().catch(() => ({})); - const { title, headMessageId } = body; - - await getThreadAndVerify(id, userId, "editor"); - - const updates: Partial<{ title: string; headMessageId: string | null; updatedAt: string }> = { - updatedAt: new Date().toISOString(), - }; - if (title !== undefined) updates.title = String(title); - if (headMessageId !== undefined) { - if (headMessageId === null) { - updates.headMessageId = null; - } else { - const msgId = String(headMessageId); - const [existing] = await db - .select({ messageId: chatMessages.messageId }) - .from(chatMessages) - .where( - and(eq(chatMessages.threadId, id), eq(chatMessages.messageId, msgId)) - ) - .limit(1); - if (!existing) { - return NextResponse.json( - { error: "headMessageId must reference an existing message in this thread" }, - { status: 400 } - ); - } - updates.headMessageId = msgId; - } - } - - await db.update(chatThreads).set(updates).where(eq(chatThreads.id, id)); - - return new Response(null, { status: 204 }); - } catch (error) { - if (error instanceof Response) return error; - console.error("[threads] PATCH error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); - } -}, { routeName: "PATCH /api/threads/[id]" }); - -/** - * DELETE /api/threads/[id] - * Delete a thread (and its messages via cascade) - */ -export const DELETE = withServerObservability(async function DELETE( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - try { - const userId = await requireAuth(); - const { id } = await params; - - await getThreadAndVerify(id, userId, "editor"); - - await db.delete(chatThreads).where(eq(chatThreads.id, id)); - - return new Response(null, { status: 204 }); - } catch (error) { - if (error instanceof Response) return error; - console.error("[threads] DELETE error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); - } -}, { routeName: "DELETE /api/threads/[id]" }); diff --git a/src/app/api/threads/[id]/title/route.ts b/src/app/api/threads/[id]/title/route.ts deleted file mode 100644 index 96db1999..00000000 --- a/src/app/api/threads/[id]/title/route.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { generateText } from "ai"; -import { db } from "@/lib/db/client"; -import { chatThreads } from "@/lib/db/schema"; -import { - requireAuth, - verifyWorkspaceAccess, - verifyThreadOwnership, -} from "@/lib/api/workspace-helpers"; -import { eq } from "drizzle-orm"; -import { withServerObservability } from "@/lib/with-server-observability"; -import { getGatewayModelIdForPurpose } from "@/lib/ai/models"; -import { - buildGatewayProviderOptions, - createGatewayLanguageModel, - getGatewayAttributionHeaders, -} from "@/lib/ai/gateway-provider-options"; - -function extractTextFromMessage(msg: { content?: unknown[] }): string { - if (!msg.content || !Array.isArray(msg.content)) return ""; - return (msg.content as { type?: string; text?: string }[]) - .filter((c) => c.type === "text") - .map((c) => c.text ?? "") - .join(" ") - .trim(); -} - -/** - * POST /api/threads/[id]/title - * Generate a title from messages using Gemini Flash Lite. - * Body: { messages: ThreadMessage[] } - */ -export const POST = withServerObservability( - async function POST( - req: NextRequest, - { params }: { params: Promise<{ id: string }> }, - ) { - try { - const userId = await requireAuth(); - const { id } = await params; - const body = await req.json().catch(() => ({})); - const { messages } = body; - - const [thread] = await db - .select() - .from(chatThreads) - .where(eq(chatThreads.id, id)) - .limit(1); - - if (!thread) { - return NextResponse.json( - { error: "Thread not found" }, - { status: 404 }, - ); - } - - await verifyWorkspaceAccess(thread.workspaceId, userId); - verifyThreadOwnership(thread, userId); - - let title = "New Chat"; - - if (messages && Array.isArray(messages) && messages.length > 0) { - const conversationText = messages - .slice(0, 6) - .map((m: { role?: string; content?: unknown[] }) => { - const text = extractTextFromMessage(m); - if (!text) return ""; - const role = m.role === "user" ? "User" : "Assistant"; - return `${role}: ${text}`; - }) - .filter(Boolean) - .join("\n\n"); - - if (conversationText.trim()) { - try { - const gatewayModelId = - getGatewayModelIdForPurpose("title-generation"); - const { text, providerMetadata } = await generateText({ - model: createGatewayLanguageModel(gatewayModelId), - providerOptions: buildGatewayProviderOptions(gatewayModelId, { - userId, - }) as any, - headers: getGatewayAttributionHeaders(), - system: `Generate a very short chat title (2-6 words) that captures the topic. Output ONLY the title, no quotes or punctuation.`, - prompt: `Conversation:\n\n${conversationText}\n\nTitle:`, - experimental_telemetry: { - isEnabled: true, - metadata: { - "tcc.sessionId": id, - ...(userId ? { userId } : {}), - }, - }, - }); - const provider = - (providerMetadata as any)?.gateway?.routing?.resolvedProvider ?? - (providerMetadata as any)?.gateway?.routing?.finalProvider; - if (provider) { - console.log( - "[threads/title] Gateway resolved provider:", - provider, - ); - } - const generated = text.trim().slice(0, 60); - if (generated) title = generated; - } catch (err) { - console.warn("[threads] title Gemini fallback:", err); - const firstUser = messages.find( - (m: { role?: string }) => m.role === "user", - ); - const fallback = extractTextFromMessage(firstUser ?? {}); - if (fallback) { - title = - fallback.slice(0, 50) + (fallback.length > 50 ? "..." : ""); - } - } - } - } - - await db.update(chatThreads).set({ title }).where(eq(chatThreads.id, id)); - - return NextResponse.json({ title }); - } catch (error) { - if (error instanceof Response) return error; - console.error("[threads] title error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 }, - ); - } - }, - { routeName: "POST /api/threads/[id]/title" }, -); diff --git a/src/app/api/threads/[id]/unarchive/route.ts b/src/app/api/threads/[id]/unarchive/route.ts deleted file mode 100644 index ca2fd5a1..00000000 --- a/src/app/api/threads/[id]/unarchive/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { setThreadArchived } from "@/lib/api/thread-archive"; -import { withServerObservability } from "@/lib/with-server-observability"; - -/** - * POST /api/threads/[id]/unarchive - * Unarchive a thread - */ -export const POST = withServerObservability(async function POST( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - try { - const { id } = await params; - return await setThreadArchived(id, false); - } catch (error) { - if (error instanceof Response) return error; - console.error("[threads] unarchive error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); - } -}, { routeName: "POST /api/threads/[id]/unarchive" }); diff --git a/src/app/api/threads/route.ts b/src/app/api/threads/route.ts deleted file mode 100644 index f966f108..00000000 --- a/src/app/api/threads/route.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { db } from "@/lib/db/client"; -import { chatThreads } from "@/lib/db/schema"; -import { - requireAuth, - verifyWorkspaceAccess, -} from "@/lib/api/workspace-helpers"; -import { eq, and, desc } from "drizzle-orm"; -import { withServerObservability } from "@/lib/with-server-observability"; - -/** - * GET /api/threads?workspaceId=xxx - * List threads for a workspace - */ -export const GET = withServerObservability(async function GET(req: NextRequest) { - try { - const userId = await requireAuth(); - const { searchParams } = new URL(req.url); - const workspaceId = searchParams.get("workspaceId"); - - if (!workspaceId) { - return NextResponse.json( - { error: "workspaceId is required" }, - { status: 400 } - ); - } - - await verifyWorkspaceAccess(workspaceId, userId); - - const threads = await db - .select({ - id: chatThreads.id, - title: chatThreads.title, - isArchived: chatThreads.isArchived, - externalId: chatThreads.externalId, - }) - .from(chatThreads) - .where( - and( - eq(chatThreads.workspaceId, workspaceId), - eq(chatThreads.userId, userId) - ) - ) - .orderBy(desc(chatThreads.lastMessageAt)); - - return NextResponse.json({ - threads: threads.map((t) => ({ - remoteId: t.id, - status: t.isArchived ? "archived" : "regular", - title: t.title ?? undefined, - externalId: t.externalId ?? undefined, - })), - }); - } catch (error) { - if (error instanceof Response) return error; - console.error("[threads] GET error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); - } -}, { routeName: "GET /api/threads" }); - -/** - * POST /api/threads - * Create a new thread - */ -export const POST = withServerObservability(async function POST(req: NextRequest) { - try { - const userId = await requireAuth(); - const body = await req.json().catch(() => ({})); - const { workspaceId, localId, externalId } = body; - - if (!workspaceId) { - return NextResponse.json( - { error: "workspaceId is required" }, - { status: 400 } - ); - } - - await verifyWorkspaceAccess(workspaceId, userId, "editor"); - - const [inserted] = await db - .insert(chatThreads) - .values({ - workspaceId, - userId, - externalId: externalId ?? undefined, - }) - .returning({ id: chatThreads.id, externalId: chatThreads.externalId }); - - if (!inserted) { - return NextResponse.json( - { error: "Failed to create thread" }, - { status: 500 } - ); - } - - return NextResponse.json({ - id: inserted.id, - remoteId: inserted.id, - externalId: inserted.externalId ?? undefined, - }); - } catch (error) { - if (error instanceof Response) return error; - console.error("[threads] POST error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); - } -}, { routeName: "POST /api/threads" }); diff --git a/src/app/api/workspaces/autogen/route.ts b/src/app/api/workspaces/autogen/route.ts index 90b3b6df..2a385c07 100644 --- a/src/app/api/workspaces/autogen/route.ts +++ b/src/app/api/workspaces/autogen/route.ts @@ -1,15 +1,15 @@ import { NextRequest } from "next/server"; import { streamText, generateText, Output } from "ai"; import { z } from "zod"; -import { executeWebSearch } from "@/lib/ai/tools/web-search"; +import { executeWebSearch } from "@/lib/autogen/web-search"; import { randomUUID } from "crypto"; import { desc, eq, sql } from "drizzle-orm"; import { requireAuthWithUserInfo } from "@/lib/api/workspace-helpers"; import { db, workspaces } from "@/lib/db/client"; import { generateSlug } from "@/lib/workspace/slug"; -import { workspaceWorker, type CreateItemParams } from "@/lib/ai/workers"; +import { workspaceWorker, type CreateItemParams } from "@/lib/workspace-worker"; import { searchVideos } from "@/lib/youtube"; -import { FirecrawlClient } from "@/lib/ai/utils/firecrawl"; +import { FirecrawlClient } from "@/lib/firecrawl/client"; import { findNextAvailablePosition } from "@/lib/workspace-state/grid-layout-helpers"; import { generateItemId } from "@/lib/workspace-state/item-helpers"; import type { Item, QuizQuestion } from "@/lib/workspace-state/types"; @@ -29,12 +29,12 @@ import { type UploadedAsset, } from "@/lib/uploads/uploaded-asset"; import { startAssetProcessing } from "@/lib/uploads/start-asset-processing"; -import { getGatewayModelIdForPurpose } from "@/lib/ai/models"; +import { getGatewayModelIdForPurpose } from "@/lib/gateway/models"; import { buildGatewayProviderOptions, createGatewayLanguageModel, getGatewayAttributionHeaders, -} from "@/lib/ai/gateway-provider-options"; +} from "@/lib/gateway/provider-options"; const MAX_TITLE_LENGTH = 60; const LOG_TRUNCATE = 400; diff --git a/src/app/api/workspaces/generate-title/route.ts b/src/app/api/workspaces/generate-title/route.ts index 763ef58f..02ce5739 100644 --- a/src/app/api/workspaces/generate-title/route.ts +++ b/src/app/api/workspaces/generate-title/route.ts @@ -7,12 +7,12 @@ import { WORKSPACE_ICON_NAMES, formatIconForStorage, } from "@/lib/workspace-icons"; -import { getGatewayModelIdForPurpose } from "@/lib/ai/models"; +import { getGatewayModelIdForPurpose } from "@/lib/gateway/models"; import { buildGatewayProviderOptions, createGatewayLanguageModel, getGatewayAttributionHeaders, -} from "@/lib/ai/gateway-provider-options"; +} from "@/lib/gateway/provider-options"; const MAX_TITLE_LENGTH = 60; diff --git a/src/app/generate/page.tsx b/src/app/generate/page.tsx index fd3bace8..9158299c 100644 --- a/src/app/generate/page.tsx +++ b/src/app/generate/page.tsx @@ -12,7 +12,7 @@ import { WorkspaceProvider } from "@/contexts/WorkspaceContext"; import { MobileWarning } from "@/components/ui/MobileWarning"; import { FloatingWorkspaceCards } from "@/components/background/FloatingWorkspaceCards"; import { ATTACHMENTS_SESSION_KEY } from "@/contexts/HomeAttachmentsContext"; -import { matchesWebSearchStreamToolName } from "@/lib/ai/chat-tool-names"; +import { matchesWebSearchStreamToolName } from "@/lib/autogen/web-search"; const PROGRESS_LABELS: Record = { understanding: "Understanding content...", diff --git a/src/app/globals.css b/src/app/globals.css index 3adf9f04..975e2010 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -218,14 +218,6 @@ } } -/* Prevent CSS smooth scrolling from interfering with assistant-ui's programmatic - scrollTo calls. The library uses scrollTo({ behavior: "auto" }) which defers - to this CSS property — smooth animation races with DOM updates during streaming, - causing missed/janky scroll positions. See: assistant-ui/assistant-ui#3648 */ -.aui-thread-viewport { - scroll-behavior: auto; -} - body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; } diff --git a/src/components/ai-elements/inline-citation.tsx b/src/components/ai-elements/inline-citation.tsx deleted file mode 100644 index 78021bcd..00000000 --- a/src/components/ai-elements/inline-citation.tsx +++ /dev/null @@ -1,200 +0,0 @@ -"use client"; - -import type { ComponentProps, ReactNode } from "react"; -import { ExternalLink } from "lucide-react"; - -import { Badge } from "@/components/ui/badge"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { cn } from "@/lib/utils"; - -export type InlineCitationProps = ComponentProps<"span">; - -export const InlineCitation = ({ - className, - ...props -}: InlineCitationProps) => ( - -); - -export type InlineCitationTextProps = ComponentProps<"span">; - -export const InlineCitationText = ({ - className, - ...props -}: InlineCitationTextProps) => ( - -); - -export type InlineCitationCardProps = ComponentProps; - -export const InlineCitationCard = (props: InlineCitationCardProps) => ( - -); - -export type InlineCitationCardTriggerProps = ComponentProps & { - sources: string[]; - /** Shown when sources is empty (e.g. during streaming) */ - fallbackLabel?: string; -}; - -function getBadgeLabel(sources: string[], fallbackLabel: string): ReactNode { - if (!sources[0]) return fallbackLabel; - try { - const url = new URL(sources[0]); - return ( - <> - {url.hostname}{" "} - {sources.length > 1 && `+${sources.length - 1}`} - - ); - } catch { - return fallbackLabel; - } -} - -export const InlineCitationCardTrigger = ({ - sources, - fallbackLabel = "unknown", - className, - ...props -}: InlineCitationCardTriggerProps) => ( - - - {getBadgeLabel(sources, fallbackLabel)} - - -); - -export type InlineCitationCardBodyProps = ComponentProps<"div">; - -export const InlineCitationCardBody = ({ - className, - ...props -}: InlineCitationCardBodyProps) => ( - -); - -export type InlineCitationSourceProps = ComponentProps<"div"> & { - title?: string; - url?: string; - /** When set (and no url), makes content clickable (e.g. open workspace document) */ - onClick?: () => void; -}; - -export const InlineCitationSource = ({ - title, - url, - onClick, - className, - children, - ...props -}: InlineCitationSourceProps) => { - const content = ( - <> - {title && ( -

{title}

- )} - {url && ( -

{url}

- )} - {children} - - ); - - const clickable = url || onClick; - const baseClasses = cn( - "space-y-1 block w-full text-left min-h-0", - clickable && "cursor-pointer transition-colors hover:bg-muted/50 rounded-md" - ); - - if (url) { - return ( - )} - > - {content} - - ); - } - if (onClick) { - return ( - - ); - } - return ( -
- {content} -
- ); -}; - -export type InlineCitationQuoteProps = ComponentProps<"blockquote">; - -export const InlineCitationQuote = ({ - children, - className, - ...props -}: InlineCitationQuoteProps) => ( -
- {children} -
-); - -function extractDomain(url: string): string { - try { - return new URL(url).hostname.replace(/^www\./, ""); - } catch { - return url; - } -} - -/** - * SurfSense-style URL citation: clickable badge with domain, opens in new tab. - * Used for [citation:https://...] refs. - */ -export function UrlCitation({ url }: { url: string }) { - const domain = extractDomain(url); - return ( - - - {domain} - - ); -} diff --git a/src/components/assistant-ui/AIFeedbackDialog.tsx b/src/components/assistant-ui/AIFeedbackDialog.tsx deleted file mode 100644 index 2d501151..00000000 --- a/src/components/assistant-ui/AIFeedbackDialog.tsx +++ /dev/null @@ -1,86 +0,0 @@ -"use client"; - -import { useState, useCallback } from "react"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; -import { posthog } from "@/lib/posthog-client"; -import { toast } from "sonner"; - -interface AIFeedbackDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function AIFeedbackDialog({ open, onOpenChange }: AIFeedbackDialogProps) { - const [feedback, setFeedback] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleSubmit = useCallback(() => { - setIsSubmitting(true); - - const trimmedFeedback = feedback.trim(); - - posthog.capture("ai_debug_feedback_submitted", { - feedback_text: trimmedFeedback, - }); - - toast.success("Feedback submitted—thank you!"); - setFeedback(""); - onOpenChange(false); - setIsSubmitting(false); - }, [feedback, onOpenChange]); - - const handleOpenChange = useCallback( - (nextOpen: boolean) => { - if (!nextOpen && !isSubmitting) { - setFeedback(""); - } - onOpenChange(nextOpen); - }, - [onOpenChange, isSubmitting] - ); - - return ( - - - - Report AI Issue - - What went wrong with the AI? Your feedback helps us improve. - - - -
-