aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorDhravya Shah <[email protected]>2025-08-16 18:58:33 -0700
committerGitHub <[email protected]>2025-08-16 18:58:33 -0700
commit91d957f478fa91a2e7487486f82fcc48f6184719 (patch)
tree3f870c04b3dce315bba1b21aa2da158494e71774 /apps
parentMerge pull request #355 from supermemoryai/archive (diff)
parentNew Version of Supermemory Consumer App (diff)
downloadsupermemory-91d957f478fa91a2e7487486f82fcc48f6184719.tar.xz
supermemory-91d957f478fa91a2e7487486f82fcc48f6184719.zip
Merge pull request #366 from supermemoryai/mahesh/supermemory-new
Diffstat (limited to 'apps')
-rw-r--r--apps/backend/.gitignore35
-rw-r--r--apps/backend/README.md8
-rw-r--r--apps/backend/drizzle.config.prod.ts10
-rw-r--r--apps/backend/drizzle.config.ts18
-rw-r--r--apps/backend/drizzle/0000_odd_impossible_man.sql189
-rw-r--r--apps/backend/drizzle/0001_seed-types.sql3
-rw-r--r--apps/backend/drizzle/0002_skinny_princess_powerful.sql2
-rw-r--r--apps/backend/drizzle/0003_luxuriant_annihilus.sql3
-rw-r--r--apps/backend/drizzle/0004_early_rick_jones.sql19
-rw-r--r--apps/backend/drizzle/0005_create-access-types.sql2
-rw-r--r--apps/backend/drizzle/0006_wandering_grandmaster.sql3
-rw-r--r--apps/backend/drizzle/0007_fantastic_serpent_society.sql1
-rw-r--r--apps/backend/drizzle/0008_add-notion.sql2
-rw-r--r--apps/backend/drizzle/0009_milky_sleepwalker.sql1
-rw-r--r--apps/backend/drizzle/0010_heavy_preak.sql7
-rw-r--r--apps/backend/drizzle/0011_new_liz_osborn.sql1
-rw-r--r--apps/backend/drizzle/0012_small_mystique.sql2
-rw-r--r--apps/backend/drizzle/0013_sharp_hemingway.sql1
-rw-r--r--apps/backend/drizzle/0014_mighty_the_captain.sql2
-rw-r--r--apps/backend/drizzle/0015_perpetual_mauler.sql1
-rw-r--r--apps/backend/drizzle/0016_good_deathbird.sql7
-rw-r--r--apps/backend/drizzle/0018_past_inertia.sql7
-rw-r--r--apps/backend/drizzle/0019_vengeful_marten_broadcloak.sql1
-rw-r--r--apps/backend/drizzle/0020_opposite_steel_serpent.sql2
-rw-r--r--apps/backend/drizzle/meta/0000_snapshot.json1060
-rw-r--r--apps/backend/drizzle/meta/0001_snapshot.json1060
-rw-r--r--apps/backend/drizzle/meta/0002_snapshot.json1067
-rw-r--r--apps/backend/drizzle/meta/0003_snapshot.json1095
-rw-r--r--apps/backend/drizzle/meta/0004_snapshot.json1173
-rw-r--r--apps/backend/drizzle/meta/0005_snapshot.json1173
-rw-r--r--apps/backend/drizzle/meta/0006_snapshot.json1189
-rw-r--r--apps/backend/drizzle/meta/0007_snapshot.json1168
-rw-r--r--apps/backend/drizzle/meta/0008_snapshot.json1168
-rw-r--r--apps/backend/drizzle/meta/0009_snapshot.json1175
-rw-r--r--apps/backend/drizzle/meta/0010_snapshot.json1175
-rw-r--r--apps/backend/drizzle/meta/0011_snapshot.json1181
-rw-r--r--apps/backend/drizzle/meta/0012_snapshot.json1188
-rw-r--r--apps/backend/drizzle/meta/0013_snapshot.json1194
-rw-r--r--apps/backend/drizzle/meta/0014_snapshot.json1207
-rw-r--r--apps/backend/drizzle/meta/0015_snapshot.json1207
-rw-r--r--apps/backend/drizzle/meta/0016_snapshot.json1222
-rw-r--r--apps/backend/drizzle/meta/0017_snapshot.json1222
-rw-r--r--apps/backend/drizzle/meta/0018_snapshot.json1222
-rw-r--r--apps/backend/drizzle/meta/0019_snapshot.json1228
-rw-r--r--apps/backend/drizzle/meta/0020_snapshot.json1127
-rw-r--r--apps/backend/drizzle/meta/_journal.json146
-rw-r--r--apps/backend/package.json36
-rw-r--r--apps/backend/public/favicon.icobin15406 -> 0 bytes
-rw-r--r--apps/backend/public/output.css853
-rw-r--r--apps/backend/scripts/migrate.ts37
-rw-r--r--apps/backend/src/auth.ts151
-rw-r--r--apps/backend/src/components/landing.tsx234
-rw-r--r--apps/backend/src/errors/baseError.ts45
-rw-r--r--apps/backend/src/errors/results.ts31
-rw-r--r--apps/backend/src/globals.css3
-rw-r--r--apps/backend/src/index.tsx255
-rw-r--r--apps/backend/src/providers.ts19
-rw-r--r--apps/backend/src/routes/actions.ts1171
-rw-r--r--apps/backend/src/routes/integrations.ts180
-rw-r--r--apps/backend/src/routes/memories.ts293
-rw-r--r--apps/backend/src/routes/spaces.ts709
-rw-r--r--apps/backend/src/routes/user.ts193
-rw-r--r--apps/backend/src/types.ts79
-rw-r--r--apps/backend/src/utils/chunkers.ts116
-rw-r--r--apps/backend/src/utils/cipher.ts79
-rw-r--r--apps/backend/src/utils/extractDocumentContent.ts87
-rw-r--r--apps/backend/src/utils/extractor.ts50
-rw-r--r--apps/backend/src/utils/fetchers.ts143
-rw-r--r--apps/backend/src/utils/notion.ts239
-rw-r--r--apps/backend/src/utils/tweetsToThreads.ts108
-rw-r--r--apps/backend/src/utils/typeDecider.ts41
-rw-r--r--apps/backend/src/workflow/index.ts217
-rw-r--r--apps/backend/tailwind.config.js9
-rw-r--r--apps/backend/tsconfig.json16
-rw-r--r--apps/backend/types.d.ts3
-rw-r--r--apps/backend/wrangler.toml55
-rw-r--r--apps/docs/README.md32
-rw-r--r--apps/docs/api-reference/endpoints/connect/connect-app.mdx10
-rw-r--r--apps/docs/api-reference/endpoints/connect/connection-information.mdx5
-rw-r--r--apps/docs/api-reference/endpoints/memory-management/delete-delete.mdx3
-rw-r--r--apps/docs/api-reference/endpoints/memory-management/post-add.mdx59
-rw-r--r--apps/docs/api-reference/endpoints/memory-management/put-update.mdx7
-rw-r--r--apps/docs/api-reference/endpoints/search/get-fastsearch.mdx5
-rw-r--r--apps/docs/api-reference/endpoints/search/post-search.mdx12
-rw-r--r--apps/docs/api-reference/endpoints/settings.mdx3
-rw-r--r--apps/docs/changelog/overview.mdx12
-rw-r--r--apps/docs/essentials/metadata-filtering.mdx78
-rw-r--r--apps/docs/essentials/pricing.mdx47
-rw-r--r--apps/docs/favicon.pngbin23616 -> 0 bytes
-rw-r--r--apps/docs/image.pngbin79915 -> 0 bytes
-rw-r--r--apps/docs/images/checks-passed.pngbin160724 -> 0 bytes
-rw-r--r--apps/docs/images/hero-dark.svg32
-rw-r--r--apps/docs/images/hero-light.svg32
-rw-r--r--apps/docs/images/setup/1.pngbin1017363 -> 0 bytes
-rw-r--r--apps/docs/images/setup/2.pngbin653266 -> 0 bytes
-rw-r--r--apps/docs/images/setup/3.pngbin660688 -> 0 bytes
-rw-r--r--apps/docs/introduction.mdx156
-rw-r--r--apps/docs/logo/dark.svg3
-rw-r--r--apps/docs/logo/light.svg3
-rw-r--r--apps/docs/mint.json75
-rw-r--r--apps/docs/openapi.json625
-rw-r--r--apps/docs/quickstart.mdx59
-rw-r--r--apps/docs/self-hosting.mdx94
-rw-r--r--apps/docs/snippets/snippet-intro.mdx1
-rw-r--r--apps/extension/.gitignore27
-rw-r--r--apps/extension/README.md3
-rw-r--r--apps/extension/components.json21
-rw-r--r--apps/extension/css/globals.css59
-rw-r--r--apps/extension/extension-env.d.ts9
-rw-r--r--apps/extension/images/icon/128.pngbin14307 -> 0 bytes
-rw-r--r--apps/extension/images/icon/16.pngbin543 -> 0 bytes
-rw-r--r--apps/extension/images/icon/32.pngbin1462 -> 0 bytes
-rw-r--r--apps/extension/images/icon/48.pngbin2836 -> 0 bytes
-rw-r--r--apps/extension/images/icon/favicon.icobin15406 -> 0 bytes
-rw-r--r--apps/extension/lib/utils.ts20
-rw-r--r--apps/extension/manifest.json37
-rw-r--r--apps/extension/package.json38
-rw-r--r--apps/extension/postcss.config.js6
-rw-r--r--apps/extension/public/globals.css1632
-rw-r--r--apps/extension/public/logo.svg11
-rw-r--r--apps/extension/scripts/content.tsx64
-rw-r--r--apps/extension/scripts/css.tsx1584
-rw-r--r--apps/extension/src/background.ts225
-rw-r--r--apps/extension/src/content.tsx350
-rw-r--r--apps/extension/src/helpers.ts65
-rw-r--r--apps/extension/src/twitter.constants.ts32
-rw-r--r--apps/extension/src/twitter.ts470
-rw-r--r--apps/extension/tailwind.config.cjs50
-rw-r--r--apps/extension/tsconfig.json22
-rw-r--r--apps/extension/ui/hooks/use-spaces.tsx147
-rw-r--r--apps/extension/ui/shadcn/badge.tsx36
-rw-r--r--apps/extension/ui/shadcn/button.tsx57
-rw-r--r--apps/extension/ui/shadcn/command.tsx151
-rw-r--r--apps/extension/ui/shadcn/dialog.tsx122
-rw-r--r--apps/extension/ui/shadcn/popover.tsx31
-rw-r--r--apps/extension/ui/spaces-selector.tsx217
-rw-r--r--apps/web/.env.example2
-rw-r--r--apps/web/.gitignore45
-rw-r--r--apps/web/.prettierrc19
-rw-r--r--apps/web/README.md48
-rw-r--r--apps/web/app/(auth)/login/page.tsx12
-rw-r--r--apps/web/app/api/emails/welcome/route.tsx19
-rw-r--r--apps/web/app/components/Chat.tsx329
-rw-r--r--apps/web/app/components/ChatInputForm.tsx502
-rw-r--r--apps/web/app/components/Histories.tsx42
-rw-r--r--apps/web/app/components/Landing/Feature.tsx157
-rw-r--r--apps/web/app/components/Landing/Footer.tsx142
-rw-r--r--apps/web/app/components/Landing/Hero.tsx299
-rw-r--r--apps/web/app/components/Landing/Note.tsx73
-rw-r--r--apps/web/app/components/Landing/Private.tsx62
-rw-r--r--apps/web/app/components/Landing/index.tsx17
-rw-r--r--apps/web/app/components/Landing/plus-grid.tsx88
-rw-r--r--apps/web/app/components/Navbar.tsx155
-rw-r--r--apps/web/app/components/Reminders.tsx354
-rw-r--r--apps/web/app/components/Suggestions.tsx43
-rw-r--r--apps/web/app/components/editor/plate-editor.tsx20
-rw-r--r--apps/web/app/components/editor/plate-types.ts221
-rw-r--r--apps/web/app/components/editor/plugins/ai-plugins.tsx195
-rw-r--r--apps/web/app/components/editor/plugins/align-plugin.ts17
-rw-r--r--apps/web/app/components/editor/plugins/autoformat-plugin.ts273
-rw-r--r--apps/web/app/components/editor/plugins/basic-nodes-plugins.tsx14
-rw-r--r--apps/web/app/components/editor/plugins/block-menu-plugins.ts14
-rw-r--r--apps/web/app/components/editor/plugins/block-selection-plugins.ts23
-rw-r--r--apps/web/app/components/editor/plugins/comments-plugin.tsx24
-rw-r--r--apps/web/app/components/editor/plugins/copilot-plugins.ts60
-rw-r--r--apps/web/app/components/editor/plugins/delete-plugins.ts20
-rw-r--r--apps/web/app/components/editor/plugins/dnd-plugins.tsx22
-rw-r--r--apps/web/app/components/editor/plugins/editor-plugins.tsx100
-rw-r--r--apps/web/app/components/editor/plugins/exit-break-plugin.ts28
-rw-r--r--apps/web/app/components/editor/plugins/fixed-toolbar-plugin.tsx17
-rw-r--r--apps/web/app/components/editor/plugins/floating-toolbar-plugin.tsx17
-rw-r--r--apps/web/app/components/editor/plugins/indent-list-plugins.ts57
-rw-r--r--apps/web/app/components/editor/plugins/line-height-plugin.ts15
-rw-r--r--apps/web/app/components/editor/plugins/link-plugin.tsx9
-rw-r--r--apps/web/app/components/editor/plugins/media-plugins.tsx30
-rw-r--r--apps/web/app/components/editor/plugins/mention-plugin.ts7
-rw-r--r--apps/web/app/components/editor/plugins/reset-block-type-plugin.ts63
-rw-r--r--apps/web/app/components/editor/plugins/soft-break-plugin.ts30
-rw-r--r--apps/web/app/components/editor/plugins/table-plugin.ts9
-rw-r--r--apps/web/app/components/editor/plugins/toc-plugin.ts10
-rw-r--r--apps/web/app/components/editor/transforms.ts206
-rw-r--r--apps/web/app/components/editor/use-chat.tsx270
-rw-r--r--apps/web/app/components/editor/use-create-editor.tsx137
-rw-r--r--apps/web/app/components/editor/writing-playground.tsx242
-rw-r--r--apps/web/app/components/gradients/gradient1.pngbin420630 -> 0 bytes
-rw-r--r--apps/web/app/components/icons/IntegrationIcons.tsx70
-rw-r--r--apps/web/app/components/icons/Logo.tsx14
-rw-r--r--apps/web/app/components/markdown/codeblock.tsx40
-rw-r--r--apps/web/app/components/markdown/renderer.tsx10
-rw-r--r--apps/web/app/components/memories/AddMemory.tsx583
-rw-r--r--apps/web/app/components/memories/CSVUploadModal.tsx267
-rw-r--r--apps/web/app/components/memories/Integrations.tsx697
-rw-r--r--apps/web/app/components/memories/MarkdownUploadModal.tsx294
-rw-r--r--apps/web/app/components/memories/MemoriesPage.tsx423
-rw-r--r--apps/web/app/components/memories/SharedCard.tsx892
-rw-r--r--apps/web/app/components/memories/SpacesSelector.tsx134
-rw-r--r--apps/web/app/components/plate-ui/ai-chat-editor.tsx54
-rw-r--r--apps/web/app/components/plate-ui/ai-leaf.tsx24
-rw-r--r--apps/web/app/components/plate-ui/ai-menu-items.tsx301
-rw-r--r--apps/web/app/components/plate-ui/ai-menu.tsx165
-rw-r--r--apps/web/app/components/plate-ui/ai-toolbar-button.tsx30
-rw-r--r--apps/web/app/components/plate-ui/align-dropdown-menu.tsx75
-rw-r--r--apps/web/app/components/plate-ui/avatar.tsx19
-rw-r--r--apps/web/app/components/plate-ui/block-context-menu.tsx206
-rw-r--r--apps/web/app/components/plate-ui/block-selection.tsx42
-rw-r--r--apps/web/app/components/plate-ui/blockquote-element.tsx22
-rw-r--r--apps/web/app/components/plate-ui/button.tsx57
-rw-r--r--apps/web/app/components/plate-ui/calendar.tsx68
-rw-r--r--apps/web/app/components/plate-ui/caption.tsx48
-rw-r--r--apps/web/app/components/plate-ui/checkbox.tsx26
-rw-r--r--apps/web/app/components/plate-ui/code-block-combobox.tsx202
-rw-r--r--apps/web/app/components/plate-ui/code-block-element.css404
-rw-r--r--apps/web/app/components/plate-ui/code-block-element.tsx39
-rw-r--r--apps/web/app/components/plate-ui/code-leaf.tsx24
-rw-r--r--apps/web/app/components/plate-ui/code-line-element.tsx10
-rw-r--r--apps/web/app/components/plate-ui/code-syntax-leaf.tsx20
-rw-r--r--apps/web/app/components/plate-ui/color-constants.ts436
-rw-r--r--apps/web/app/components/plate-ui/color-dropdown-menu-items.tsx111
-rw-r--r--apps/web/app/components/plate-ui/color-dropdown-menu.tsx58
-rw-r--r--apps/web/app/components/plate-ui/color-input.tsx31
-rw-r--r--apps/web/app/components/plate-ui/color-picker.tsx78
-rw-r--r--apps/web/app/components/plate-ui/colors-custom.tsx73
-rw-r--r--apps/web/app/components/plate-ui/column-element.tsx34
-rw-r--r--apps/web/app/components/plate-ui/column-group-element.tsx186
-rw-r--r--apps/web/app/components/plate-ui/command.tsx88
-rw-r--r--apps/web/app/components/plate-ui/comment-avatar.tsx22
-rw-r--r--apps/web/app/components/plate-ui/comment-create-form.tsx37
-rw-r--r--apps/web/app/components/plate-ui/comment-item.tsx74
-rw-r--r--apps/web/app/components/plate-ui/comment-leaf.tsx49
-rw-r--r--apps/web/app/components/plate-ui/comment-more-dropdown.tsx44
-rw-r--r--apps/web/app/components/plate-ui/comment-reply-items.tsx22
-rw-r--r--apps/web/app/components/plate-ui/comment-resolve-button.tsx31
-rw-r--r--apps/web/app/components/plate-ui/comment-toolbar-button.tsx20
-rw-r--r--apps/web/app/components/plate-ui/comment-value.tsx34
-rw-r--r--apps/web/app/components/plate-ui/comments-popover.tsx63
-rw-r--r--apps/web/app/components/plate-ui/context-menu.tsx229
-rw-r--r--apps/web/app/components/plate-ui/cursor-overlay.tsx64
-rw-r--r--apps/web/app/components/plate-ui/date-element.tsx85
-rw-r--r--apps/web/app/components/plate-ui/dialog.tsx62
-rw-r--r--apps/web/app/components/plate-ui/draggable.tsx187
-rw-r--r--apps/web/app/components/plate-ui/dropdown-menu.tsx240
-rw-r--r--apps/web/app/components/plate-ui/editor.tsx114
-rw-r--r--apps/web/app/components/plate-ui/emoji-picker-navigation.tsx105
-rw-r--r--apps/web/app/components/plate-ui/emoji-picker-preview.tsx73
-rw-r--r--apps/web/app/components/plate-ui/emoji-picker-search-and-clear.tsx46
-rw-r--r--apps/web/app/components/plate-ui/emoji-picker-search-bar.tsx33
-rw-r--r--apps/web/app/components/plate-ui/emoji-picker.tsx79
-rw-r--r--apps/web/app/components/plate-ui/emoji-toolbar-dropdown.tsx29
-rw-r--r--apps/web/app/components/plate-ui/excalidraw-element.tsx30
-rw-r--r--apps/web/app/components/plate-ui/fixed-toolbar-buttons.tsx147
-rw-r--r--apps/web/app/components/plate-ui/fixed-toolbar.tsx10
-rw-r--r--apps/web/app/components/plate-ui/floating-toolbar-buttons.tsx91
-rw-r--r--apps/web/app/components/plate-ui/floating-toolbar.tsx83
-rw-r--r--apps/web/app/components/plate-ui/ghost-text.tsx37
-rw-r--r--apps/web/app/components/plate-ui/heading-element.tsx40
-rw-r--r--apps/web/app/components/plate-ui/highlight-leaf.tsx19
-rw-r--r--apps/web/app/components/plate-ui/hr-element.tsx32
-rw-r--r--apps/web/app/components/plate-ui/image-element.tsx80
-rw-r--r--apps/web/app/components/plate-ui/image-preview.tsx149
-rw-r--r--apps/web/app/components/plate-ui/indent-fire-marker.tsx22
-rw-r--r--apps/web/app/components/plate-ui/indent-list-toolbar-button.tsx35
-rw-r--r--apps/web/app/components/plate-ui/indent-todo-marker.tsx41
-rw-r--r--apps/web/app/components/plate-ui/indent-todo-toolbar-button.tsx23
-rw-r--r--apps/web/app/components/plate-ui/indent-toolbar-button.tsx21
-rw-r--r--apps/web/app/components/plate-ui/inline-combobox.tsx405
-rw-r--r--apps/web/app/components/plate-ui/input.tsx25
-rw-r--r--apps/web/app/components/plate-ui/insert-dropdown-menu.tsx261
-rw-r--r--apps/web/app/components/plate-ui/kbd-leaf.tsx22
-rw-r--r--apps/web/app/components/plate-ui/line-height-dropdown-menu.tsx55
-rw-r--r--apps/web/app/components/plate-ui/link-element.tsx33
-rw-r--r--apps/web/app/components/plate-ui/link-floating-toolbar.tsx162
-rw-r--r--apps/web/app/components/plate-ui/link-toolbar-button.tsx29
-rw-r--r--apps/web/app/components/plate-ui/mark-toolbar-button.tsx24
-rw-r--r--apps/web/app/components/plate-ui/media-embed-element.tsx132
-rw-r--r--apps/web/app/components/plate-ui/media-popover.tsx98
-rw-r--r--apps/web/app/components/plate-ui/media-toolbar-button.tsx28
-rw-r--r--apps/web/app/components/plate-ui/mention-element.tsx63
-rw-r--r--apps/web/app/components/plate-ui/mention-input-element.tsx146
-rw-r--r--apps/web/app/components/plate-ui/mode-dropdown-menu.tsx92
-rw-r--r--apps/web/app/components/plate-ui/more-dropdown-menu.tsx101
-rw-r--r--apps/web/app/components/plate-ui/outdent-toolbar-button.tsx21
-rw-r--r--apps/web/app/components/plate-ui/paragraph-element.tsx22
-rw-r--r--apps/web/app/components/plate-ui/placeholder.tsx54
-rw-r--r--apps/web/app/components/plate-ui/plate-element.tsx26
-rw-r--r--apps/web/app/components/plate-ui/popover.tsx31
-rw-r--r--apps/web/app/components/plate-ui/resizable.tsx66
-rw-r--r--apps/web/app/components/plate-ui/separator.tsx25
-rw-r--r--apps/web/app/components/plate-ui/slash-input-element.tsx242
-rw-r--r--apps/web/app/components/plate-ui/table-cell-element.tsx152
-rw-r--r--apps/web/app/components/plate-ui/table-dropdown-menu.tsx160
-rw-r--r--apps/web/app/components/plate-ui/table-element.tsx330
-rw-r--r--apps/web/app/components/plate-ui/table-row-element.tsx24
-rw-r--r--apps/web/app/components/plate-ui/toc-element.tsx65
-rw-r--r--apps/web/app/components/plate-ui/toggle-element.tsx44
-rw-r--r--apps/web/app/components/plate-ui/toggle-toolbar-button.tsx25
-rw-r--r--apps/web/app/components/plate-ui/toolbar.tsx157
-rw-r--r--apps/web/app/components/plate-ui/tooltip.tsx86
-rw-r--r--apps/web/app/components/plate-ui/turn-into-dropdown-menu.tsx165
-rw-r--r--apps/web/app/components/plate-ui/with-draggables.tsx148
-rw-r--r--apps/web/app/components/skeletons/HistoriesSkeleton.tsx26
-rw-r--r--apps/web/app/components/skeletons/SuggestionsSkeleton.tsx17
-rw-r--r--apps/web/app/components/theme-button.tsx15
-rw-r--r--apps/web/app/components/twitter/avatar-img.tsx9
-rw-r--r--apps/web/app/components/twitter/icons/icons.module.css9
-rw-r--r--apps/web/app/components/twitter/icons/index.ts3
-rw-r--r--apps/web/app/components/twitter/icons/verified-business.tsx48
-rw-r--r--apps/web/app/components/twitter/icons/verified-government.tsx13
-rw-r--r--apps/web/app/components/twitter/icons/verified.tsx9
-rw-r--r--apps/web/app/components/twitter/quoted-tweet/quoted-tweet-body.module.css9
-rw-r--r--apps/web/app/components/twitter/quoted-tweet/quoted-tweet-body.tsx13
-rw-r--r--apps/web/app/components/twitter/quoted-tweet/quoted-tweet-container.module.css19
-rw-r--r--apps/web/app/components/twitter/quoted-tweet/quoted-tweet-container.tsx18
-rw-r--r--apps/web/app/components/twitter/quoted-tweet/quoted-tweet-header.module.css38
-rw-r--r--apps/web/app/components/twitter/quoted-tweet/quoted-tweet-header.tsx37
-rw-r--r--apps/web/app/components/twitter/quoted-tweet/quoted-tweet.tsx16
-rw-r--r--apps/web/app/components/twitter/render-tweet.tsx198
-rw-r--r--apps/web/app/components/twitter/tweet-body.module.css8
-rw-r--r--apps/web/app/components/twitter/tweet-container.module.css25
-rw-r--r--apps/web/app/components/twitter/tweet-header.module.css96
-rw-r--r--apps/web/app/components/twitter/tweet-in-reply-to.module.css13
-rw-r--r--apps/web/app/components/twitter/tweet-link.module.css10
-rw-r--r--apps/web/app/components/twitter/tweet-media-video.module.css70
-rw-r--r--apps/web/app/components/twitter/tweet-media-video.tsx97
-rw-r--r--apps/web/app/components/twitter/tweet-media.module.css53
-rw-r--r--apps/web/app/components/twitter/tweet-media.tsx87
-rw-r--r--apps/web/app/components/twitter/verified-badge.module.css10
-rw-r--r--apps/web/app/components/twitter/verified-badge.tsx33
-rw-r--r--apps/web/app/components/ui/Loader.tsx19
-rw-r--r--apps/web/app/components/ui/avatar.tsx48
-rw-r--r--apps/web/app/components/ui/badge.tsx36
-rw-r--r--apps/web/app/components/ui/button.tsx59
-rw-r--r--apps/web/app/components/ui/card.tsx76
-rw-r--r--apps/web/app/components/ui/checkbox.tsx28
-rw-r--r--apps/web/app/components/ui/combobox.tsx102
-rw-r--r--apps/web/app/components/ui/command.tsx143
-rw-r--r--apps/web/app/components/ui/credenza.tsx146
-rw-r--r--apps/web/app/components/ui/dialog.tsx102
-rw-r--r--apps/web/app/components/ui/drawer.tsx116
-rw-r--r--apps/web/app/components/ui/dropdown-menu.tsx203
-rw-r--r--apps/web/app/components/ui/input.tsx22
-rw-r--r--apps/web/app/components/ui/label.tsx24
-rw-r--r--apps/web/app/components/ui/popover.tsx31
-rw-r--r--apps/web/app/components/ui/select.tsx150
-rw-r--r--apps/web/app/components/ui/switch.tsx27
-rw-r--r--apps/web/app/components/ui/tabs.tsx53
-rw-r--r--apps/web/app/components/ui/textarea.tsx23
-rw-r--r--apps/web/app/config/integrations.tsx203
-rw-r--r--apps/web/app/config/util.ts11
-rw-r--r--apps/web/app/entry.client.tsx15
-rw-r--r--apps/web/app/entry.server.tsx57
-rw-r--r--apps/web/app/icon.pngbin0 -> 590 bytes
-rw-r--r--apps/web/app/layout.tsx69
-rw-r--r--apps/web/app/lib/auth/authMiddleware.ts45
-rw-r--r--apps/web/app/lib/constants/pastelColors.tsx9
-rw-r--r--apps/web/app/lib/constants/typeIcons.tsx57
-rw-r--r--apps/web/app/lib/env.server.ts19
-rw-r--r--apps/web/app/lib/environment.ts15
-rw-r--r--apps/web/app/lib/hooks/use-auto-scroll.ts62
-rw-r--r--apps/web/app/lib/hooks/use-chat-stream.ts85
-rw-r--r--apps/web/app/lib/hooks/use-debounce.ts18
-rw-r--r--apps/web/app/lib/hooks/use-fetcher-with-promise.tsx44
-rw-r--r--apps/web/app/lib/hooks/use-is-touch-device.ts26
-rw-r--r--apps/web/app/lib/hooks/use-keyboard.tsx101
-rw-r--r--apps/web/app/lib/hooks/use-live-transcript.tsx132
-rw-r--r--apps/web/app/lib/hooks/use-media-query.tsx19
-rw-r--r--apps/web/app/lib/hooks/use-memories.tsx276
-rw-r--r--apps/web/app/lib/hooks/use-mounted.ts11
-rw-r--r--apps/web/app/lib/hooks/use-spaces.tsx135
-rw-r--r--apps/web/app/lib/hooks/use-text-overflow.ts22
-rw-r--r--apps/web/app/lib/hooks/use-upload-file.ts30
-rw-r--r--apps/web/app/lib/misc.ts20
-rw-r--r--apps/web/app/lib/stripe.constants.ts39
-rw-r--r--apps/web/app/lib/stripe.ts70
-rw-r--r--apps/web/app/lib/theme-provider.tsx133
-rw-r--r--apps/web/app/lib/theme.server.ts28
-rw-r--r--apps/web/app/lib/types/memory.ts12
-rw-r--r--apps/web/app/lib/types/safety.ts8
-rw-r--r--apps/web/app/lib/utils.ts6
-rw-r--r--apps/web/app/lib/utils/metadata.ts5
-rw-r--r--apps/web/app/lib/utils/tweet.ts22
-rw-r--r--apps/web/app/manifest.ts20
-rw-r--r--apps/web/app/not-found.tsx27
-rw-r--r--apps/web/app/opengraph-image.pngbin0 -> 536316 bytes
-rw-r--r--apps/web/app/page.tsx681
-rw-r--r--apps/web/app/ref/[code]/page.tsx227
-rw-r--r--apps/web/app/ref/page.tsx58
-rw-r--r--apps/web/app/root.tsx191
-rw-r--r--apps/web/app/routes/_index.tsx250
-rw-r--r--apps/web/app/routes/action.chat.tsx60
-rw-r--r--apps/web/app/routes/action.set-theme.tsx23
-rw-r--r--apps/web/app/routes/action.sign-out.tsx10
-rw-r--r--apps/web/app/routes/action.upload.tsx69
-rw-r--r--apps/web/app/routes/callback.ts5
-rw-r--r--apps/web/app/routes/chat.$chatId.tsx55
-rw-r--r--apps/web/app/routes/chat.new.tsx42
-rw-r--r--apps/web/app/routes/content.$contentid.tsx277
-rw-r--r--apps/web/app/routes/editor.tsx13
-rw-r--r--apps/web/app/routes/extension.ts7
-rw-r--r--apps/web/app/routes/home.tsx5
-rw-r--r--apps/web/app/routes/onboarding.add.tsx245
-rw-r--r--apps/web/app/routes/onboarding.import.tsx171
-rw-r--r--apps/web/app/routes/onboarding.index.tsx91
-rw-r--r--apps/web/app/routes/onboarding.privacy.tsx92
-rw-r--r--apps/web/app/routes/pay.stripe.callback.tsx21
-rw-r--r--apps/web/app/routes/pay.stripe.tsx93
-rw-r--r--apps/web/app/routes/pay.stripe.webhook.tsx69
-rw-r--r--apps/web/app/routes/pitch.index.tsx93
-rw-r--r--apps/web/app/routes/pricing.index.tsx253
-rw-r--r--apps/web/app/routes/privacy.tsx71
-rw-r--r--apps/web/app/routes/ref.tsx25
-rw-r--r--apps/web/app/routes/signin.tsx9
-rw-r--r--apps/web/app/routes/space.$spaceId.tsx388
-rw-r--r--apps/web/app/routes/space.($spaceId).invitation.tsx123
-rw-r--r--apps/web/app/routes/tos.tsx57
-rw-r--r--apps/web/app/sonner.css656
-rw-r--r--apps/web/app/tailwind.css253
-rw-r--r--apps/web/app/types/css.d.ts9
-rw-r--r--apps/web/app/types/stripe.d.ts10
-rw-r--r--apps/web/app/waitlist/page.tsx218
-rw-r--r--apps/web/assets/logo.svg11
-rw-r--r--apps/web/biome.json10
-rw-r--r--apps/web/button.tsx58
-rw-r--r--apps/web/components.json38
-rw-r--r--apps/web/components/connect-ai-modal.tsx219
-rw-r--r--apps/web/components/create-project-dialog.tsx119
-rw-r--r--apps/web/components/glass-menu-effect.tsx37
-rw-r--r--apps/web/components/install-prompt.tsx118
-rw-r--r--apps/web/components/memory-list-view.tsx802
-rw-r--r--apps/web/components/menu.tsx618
-rw-r--r--apps/web/components/project-selector.tsx566
-rw-r--r--apps/web/components/referral-upgrade-modal.tsx290
-rw-r--r--apps/web/components/spinner.tsx8
-rw-r--r--apps/web/components/text-shimmer.tsx57
-rw-r--r--apps/web/components/tour.tsx413
-rw-r--r--apps/web/components/views/add-memory.tsx1425
-rw-r--r--apps/web/components/views/billing.tsx261
-rw-r--r--apps/web/components/views/chat/chat-messages.tsx322
-rw-r--r--apps/web/components/views/chat/index.tsx124
-rw-r--r--apps/web/components/views/connections-tab-content.tsx382
-rw-r--r--apps/web/components/views/mcp/index.tsx311
-rw-r--r--apps/web/components/views/mcp/installation-dialog-content.tsx79
-rw-r--r--apps/web/components/views/profile.tsx266
-rw-r--r--apps/web/components/views/projects.tsx742
-rw-r--r--apps/web/env.d.ts8
-rw-r--r--apps/web/eslint.config.mjs145
-rw-r--r--apps/web/functions/[[path]].ts6
-rw-r--r--apps/web/functions/notion.ts0
-rw-r--r--apps/web/globals.css2
-rw-r--r--apps/web/hooks/use-project-mutations.ts89
-rw-r--r--apps/web/hooks/use-project-name.ts26
-rw-r--r--apps/web/hooks/use-resize-observer.ts23
-rw-r--r--apps/web/instrumentation-client.ts24
-rw-r--r--apps/web/lib/analytics.ts44
-rw-r--r--apps/web/lib/mobile-panel-context.tsx32
-rw-r--r--apps/web/lib/tour-constants.ts23
-rw-r--r--apps/web/lib/view-mode-context.tsx96
-rw-r--r--apps/web/load-context.ts9
-rw-r--r--apps/web/middleware.ts50
-rw-r--r--apps/web/next.config.ts60
-rw-r--r--apps/web/open-next.config.ts6
-rw-r--r--apps/web/package.json264
-rw-r--r--apps/web/postcss.config.js6
-rw-r--r--apps/web/postcss.config.mjs5
-rw-r--r--apps/web/public/_headers8
-rw-r--r--apps/web/public/_routes.json16
-rw-r--r--apps/web/public/android-chrome-192x192.pngbin26861 -> 0 bytes
-rw-r--r--apps/web/public/android-chrome-512x512.pngbin94061 -> 0 bytes
-rw-r--r--apps/web/public/apple-touch-icon.pngbin23616 -> 0 bytes
-rw-r--r--apps/web/public/favicon-16x16.pngbin601 -> 0 bytes
-rw-r--r--apps/web/public/favicon-32x32.pngbin1545 -> 0 bytes
-rw-r--r--apps/web/public/favicon.icobin15406 -> 0 bytes
-rw-r--r--apps/web/public/images/login-carousel-1.pngbin0 -> 395589 bytes
-rw-r--r--apps/web/public/images/login-carousel-2.pngbin0 -> 269962 bytes
-rw-r--r--apps/web/public/images/logo.pngbin0 -> 102803 bytes
-rw-r--r--apps/web/public/images/sidebar.pngbin0 -> 4777067 bytes
-rw-r--r--apps/web/public/landing-page.jpegbin0 -> 694533 bytes
-rw-r--r--apps/web/public/mcp-supported-tools/claude.pngbin0 -> 54846 bytes
-rw-r--r--apps/web/public/mcp-supported-tools/cline.pngbin0 -> 25126 bytes
-rw-r--r--apps/web/public/mcp-supported-tools/cursor.pngbin0 -> 104215 bytes
-rw-r--r--apps/web/public/mcp-supported-tools/gemini-cli.pngbin0 -> 21598 bytes
-rw-r--r--apps/web/public/mcp-supported-tools/vscode.pngbin0 -> 70640 bytes
-rw-r--r--apps/web/public/og-image.pngbin36164 -> 0 bytes
-rw-r--r--apps/web/public/product-of-the-day.pngbin79406 -> 0 bytes
-rw-r--r--apps/web/public/siri.webpbin10208 -> 0 bytes
-rw-r--r--apps/web/public/site.webmanifest1
-rw-r--r--apps/web/server/index.ts420
-rw-r--r--apps/web/server/proxy.ts42
-rw-r--r--apps/web/stores/chat.ts182
-rw-r--r--apps/web/stores/highlights.ts32
-rw-r--r--apps/web/stores/index.ts78
-rw-r--r--apps/web/tailwind.config.ts92
-rw-r--r--apps/web/tsconfig.json44
-rw-r--r--apps/web/vite.config.ts81
-rw-r--r--apps/web/worker-configuration.d.ts33
-rw-r--r--apps/web/wrangler.jsonc31
-rw-r--r--apps/web/wrangler.toml27
496 files changed, 9522 insertions, 63236 deletions
diff --git a/apps/backend/.gitignore b/apps/backend/.gitignore
deleted file mode 100644
index e4b00d87..00000000
--- a/apps/backend/.gitignore
+++ /dev/null
@@ -1,35 +0,0 @@
-# prod
-dist/
-.dev.vars
-*.vars
-
-# dev
-.yarn/
-!.yarn/releases
-.vscode/*
-!.vscode/launch.json
-!.vscode/*.code-snippets
-.idea/workspace.xml
-.idea/usage.statistics.xml
-.idea/shelf
-
-# deps
-node_modules/
-.wrangler
-
-# env
-.env
-.env.production
-.dev.vars
-
-# logs
-logs/
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-# misc
-.DS_Store
diff --git a/apps/backend/README.md b/apps/backend/README.md
deleted file mode 100644
index cc58e962..00000000
--- a/apps/backend/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-```
-npm install
-npm run dev
-```
-
-```
-npm run deploy
-```
diff --git a/apps/backend/drizzle.config.prod.ts b/apps/backend/drizzle.config.prod.ts
deleted file mode 100644
index 4707bf25..00000000
--- a/apps/backend/drizzle.config.prod.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { defineConfig } from "drizzle-kit";
-
-export default defineConfig({
- dialect: "postgresql",
- schema: "../../packages/db",
- out: "./drizzle",
- dbCredentials: {
- url: process.env.PROD_DATABASE_URL!,
- },
-});
diff --git a/apps/backend/drizzle.config.ts b/apps/backend/drizzle.config.ts
deleted file mode 100644
index 3f198a9f..00000000
--- a/apps/backend/drizzle.config.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { config } from "dotenv";
-import { defineConfig } from "drizzle-kit";
-import process from "process";
-
-config();
-
-if (process.env.NODE_ENV !== "production" && !process.env.DATABASE_URL) {
- throw new Error("DATABASE_URL is not set");
-}
-
-export default defineConfig({
- dialect: "postgresql",
- schema: "../../packages/db",
- out: "./drizzle",
- dbCredentials: {
- url: process.env.DATABASE_URL!,
- },
-});
diff --git a/apps/backend/drizzle/0000_odd_impossible_man.sql b/apps/backend/drizzle/0000_odd_impossible_man.sql
deleted file mode 100644
index 0a4b660c..00000000
--- a/apps/backend/drizzle/0000_odd_impossible_man.sql
+++ /dev/null
@@ -1,189 +0,0 @@
-CREATE EXTENSION IF NOT EXISTS vectorscale CASCADE;
-
-CREATE TABLE IF NOT EXISTS "chat_threads" (
- "id" bigserial PRIMARY KEY NOT NULL,
- "uuid" varchar(36) NOT NULL,
- "firstMessage" text NOT NULL,
- "user_id" integer NOT NULL,
- "messages" jsonb NOT NULL,
- "created_at" timestamp with time zone DEFAULT now() NOT NULL,
- CONSTRAINT "chat_threads_uuid_unique" UNIQUE("uuid")
-);
---> statement-breakpoint
-CREATE TABLE IF NOT EXISTS "chunks" (
- "id" serial PRIMARY KEY NOT NULL,
- "document_id" integer NOT NULL,
- "text_content" text,
- "order_in_document" integer NOT NULL,
- "embeddings" vector(1536),
- "metadata" jsonb,
- "created_at" timestamp with time zone DEFAULT now() NOT NULL,
- "updated_at" timestamp with time zone DEFAULT now() NOT NULL
-);
---> statement-breakpoint
-CREATE TABLE IF NOT EXISTS "content_to_space" (
- "content_id" integer NOT NULL,
- "space_id" integer NOT NULL
-);
---> statement-breakpoint
-CREATE TABLE IF NOT EXISTS "document_type" (
- "type" text PRIMARY KEY NOT NULL
-);
---> statement-breakpoint
-CREATE TABLE IF NOT EXISTS "documents" (
- "id" bigserial PRIMARY KEY NOT NULL,
- "uuid" varchar(36) NOT NULL,
- "url" text,
- "created_at" timestamp with time zone DEFAULT now() NOT NULL,
- "updated_at" timestamp with time zone,
- "type" text NOT NULL,
- "title" text,
- "description" text,
- "og_image" text,
- "raw" text,
- "user_id" integer NOT NULL,
- "content" text,
- CONSTRAINT "documents_uuid_unique" UNIQUE("uuid")
-);
---> statement-breakpoint
-CREATE TABLE IF NOT EXISTS "job" (
- "id" serial PRIMARY KEY NOT NULL,
- "user_id" integer NOT NULL,
- "url" text NOT NULL,
- "status" text NOT NULL,
- "attempts" integer DEFAULT 0 NOT NULL,
- "lastAttemptAt" timestamp with time zone,
- "error" text,
- "created_at" timestamp with time zone,
- "updated_at" timestamp with time zone
-);
---> statement-breakpoint
-CREATE TABLE IF NOT EXISTS "space_access" (
- "space_id" integer,
- "user_email" varchar(512),
- "status" text
-);
---> statement-breakpoint
-CREATE TABLE IF NOT EXISTS "space_access_status" (
- "status" text PRIMARY KEY NOT NULL
-);
---> statement-breakpoint
-CREATE TABLE IF NOT EXISTS "space_members" (
- "spaceId" integer NOT NULL,
- "user_id" integer NOT NULL
-);
---> statement-breakpoint
-CREATE TABLE IF NOT EXISTS "spaces" (
- "id" bigserial PRIMARY KEY NOT NULL,
- "uuid" varchar(36) NOT NULL,
- "name" text NOT NULL,
- "created_at" timestamp DEFAULT now() NOT NULL,
- "updated_at" timestamp DEFAULT now() NOT NULL,
- "ownerId" integer NOT NULL,
- "is_public" boolean DEFAULT false NOT NULL,
- CONSTRAINT "spaces_uuid_unique" UNIQUE("uuid")
-);
---> statement-breakpoint
-CREATE TABLE IF NOT EXISTS "users" (
- "id" serial PRIMARY KEY NOT NULL,
- "uuid" varchar(36) NOT NULL,
- "email" text NOT NULL,
- "first_name" text,
- "last_name" text,
- "email_verified" boolean DEFAULT false NOT NULL,
- "profile_picture_url" text,
- "telegram_id" varchar(255),
- "has_onboarded" integer DEFAULT 0 NOT NULL,
- "created_at" timestamp DEFAULT now() NOT NULL,
- "updated_at" timestamp DEFAULT now() NOT NULL,
- CONSTRAINT "users_uuid_unique" UNIQUE("uuid"),
- CONSTRAINT "users_email_unique" UNIQUE("email")
-);
---> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "chat_threads" ADD CONSTRAINT "chat_threads_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
---> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "chunks" ADD CONSTRAINT "chunks_document_id_documents_id_fk" FOREIGN KEY ("document_id") REFERENCES "public"."documents"("id") ON DELETE cascade ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
---> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "content_to_space" ADD CONSTRAINT "content_to_space_content_id_documents_id_fk" FOREIGN KEY ("content_id") REFERENCES "public"."documents"("id") ON DELETE cascade ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
---> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "content_to_space" ADD CONSTRAINT "content_to_space_space_id_spaces_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."spaces"("id") ON DELETE cascade ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
---> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "documents" ADD CONSTRAINT "documents_type_document_type_type_fk" FOREIGN KEY ("type") REFERENCES "public"."document_type"("type") ON DELETE no action ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
---> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "documents" ADD CONSTRAINT "documents_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE restrict ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
---> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "job" ADD CONSTRAINT "job_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
---> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "space_access" ADD CONSTRAINT "space_access_space_id_spaces_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."spaces"("id") ON DELETE cascade ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
---> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "space_access" ADD CONSTRAINT "space_access_status_space_access_status_status_fk" FOREIGN KEY ("status") REFERENCES "public"."space_access_status"("status") ON DELETE no action ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
---> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "space_members" ADD CONSTRAINT "space_members_spaceId_users_id_fk" FOREIGN KEY ("spaceId") REFERENCES "public"."users"("id") ON DELETE restrict ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
---> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "space_members" ADD CONSTRAINT "space_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE restrict ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
---> statement-breakpoint
-CREATE INDEX IF NOT EXISTS "chat_threads_user_idx" ON "chat_threads" USING btree ("user_id");--> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "chunk_id_idx" ON "chunks" USING btree ("id");--> statement-breakpoint
-CREATE INDEX IF NOT EXISTS "chunk_document_id_idx" ON "chunks" USING btree ("document_id");--> statement-breakpoint
-CREATE INDEX IF NOT EXISTS "embeddingIndex" ON "chunks" USING diskann ("embeddings" vector_cosine_ops);--> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "content_id_space_id_unique" ON "content_to_space" USING btree ("content_id","space_id");--> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "document_id_idx" ON "documents" USING btree ("id");--> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "document_uuid_idx" ON "documents" USING btree ("uuid");--> statement-breakpoint
-CREATE INDEX IF NOT EXISTS "document_type_idx" ON "documents" USING btree ("type");--> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "document_url_user_id_idx" ON "documents" USING btree ("url","user_id");--> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "user_id_url_idx" ON "job" USING btree ("user_id","url");--> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "space_id_user_email_idx" ON "space_access" USING btree ("space_id","user_email");--> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "space_members_space_user_idx" ON "space_members" USING btree ("spaceId","user_id");--> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "spaces_id_idx" ON "spaces" USING btree ("id");--> statement-breakpoint
-CREATE INDEX IF NOT EXISTS "spaces_owner_id_idx" ON "spaces" USING btree ("ownerId");--> statement-breakpoint
-CREATE INDEX IF NOT EXISTS "spaces_name_idx" ON "spaces" USING btree ("name");--> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "users_id_idx" ON "users" USING btree ("id");--> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "users_uuid_idx" ON "users" USING btree ("uuid");--> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "users_email_idx" ON "users" USING btree ("email");--> statement-breakpoint
-CREATE INDEX IF NOT EXISTS "users_name_idx" ON "users" USING btree ("first_name","last_name");--> statement-breakpoint
-CREATE INDEX IF NOT EXISTS "users_created_at_idx" ON "users" USING btree ("created_at");--> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "users_telegram_id_idx" ON "users" USING btree ("telegram_id"); \ No newline at end of file
diff --git a/apps/backend/drizzle/0001_seed-types.sql b/apps/backend/drizzle/0001_seed-types.sql
deleted file mode 100644
index 3c409b92..00000000
--- a/apps/backend/drizzle/0001_seed-types.sql
+++ /dev/null
@@ -1,3 +0,0 @@
--- Custom SQL migration file, put you code below! --
-INSERT INTO "document_type" (type) VALUES ('tweet'), ('page'), ('note') ON CONFLICT DO NOTHING;
-INSERT INTO "document_type" (type) VALUES ('document') ON CONFLICT DO NOTHING; \ No newline at end of file
diff --git a/apps/backend/drizzle/0002_skinny_princess_powerful.sql b/apps/backend/drizzle/0002_skinny_princess_powerful.sql
deleted file mode 100644
index c826811f..00000000
--- a/apps/backend/drizzle/0002_skinny_princess_powerful.sql
+++ /dev/null
@@ -1,2 +0,0 @@
--- Active: 1732249624784@@_@5432@supermemorymain
-ALTER TABLE "documents" ADD COLUMN "is_successfully_processed" boolean DEFAULT false; \ No newline at end of file
diff --git a/apps/backend/drizzle/0003_luxuriant_annihilus.sql b/apps/backend/drizzle/0003_luxuriant_annihilus.sql
deleted file mode 100644
index fef1b6b1..00000000
--- a/apps/backend/drizzle/0003_luxuriant_annihilus.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-ALTER TABLE "documents" ALTER COLUMN "is_successfully_processed" DROP NOT NULL;--> statement-breakpoint
-ALTER TABLE "space_access" ADD COLUMN "access_type" text DEFAULT 'read' NOT NULL;--> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "document_raw_user_idx" ON "documents" USING btree ("raw","user_id");--> statement-breakpoint \ No newline at end of file
diff --git a/apps/backend/drizzle/0004_early_rick_jones.sql b/apps/backend/drizzle/0004_early_rick_jones.sql
deleted file mode 100644
index b26c0ffc..00000000
--- a/apps/backend/drizzle/0004_early_rick_jones.sql
+++ /dev/null
@@ -1,19 +0,0 @@
-CREATE TABLE IF NOT EXISTS "saved_spaces" (
- "user_id" integer NOT NULL,
- "space_id" integer NOT NULL,
- "saved_at" timestamp DEFAULT now() NOT NULL
-);
---> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "saved_spaces" ADD CONSTRAINT "saved_spaces_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
---> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "saved_spaces" ADD CONSTRAINT "saved_spaces_space_id_spaces_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."spaces"("id") ON DELETE cascade ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
---> statement-breakpoint
-CREATE UNIQUE INDEX IF NOT EXISTS "saved_spaces_user_space_idx" ON "saved_spaces" USING btree ("user_id","space_id"); \ No newline at end of file
diff --git a/apps/backend/drizzle/0005_create-access-types.sql b/apps/backend/drizzle/0005_create-access-types.sql
deleted file mode 100644
index 29095454..00000000
--- a/apps/backend/drizzle/0005_create-access-types.sql
+++ /dev/null
@@ -1,2 +0,0 @@
--- Custom SQL migration file, put you code below! --
-INSERT INTO "space_access_status" (status) VALUES ('pending'), ('accepted'), ('rejected') ON CONFLICT DO NOTHING; \ No newline at end of file
diff --git a/apps/backend/drizzle/0006_wandering_grandmaster.sql b/apps/backend/drizzle/0006_wandering_grandmaster.sql
deleted file mode 100644
index 0a0066d8..00000000
--- a/apps/backend/drizzle/0006_wandering_grandmaster.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-CREATE TABLE IF NOT EXISTS "waitlist" (
- "email" varchar(512) PRIMARY KEY NOT NULL
-);
diff --git a/apps/backend/drizzle/0007_fantastic_serpent_society.sql b/apps/backend/drizzle/0007_fantastic_serpent_society.sql
deleted file mode 100644
index c25c55a3..00000000
--- a/apps/backend/drizzle/0007_fantastic_serpent_society.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP INDEX IF EXISTS "document_url_user_id_idx"; \ No newline at end of file
diff --git a/apps/backend/drizzle/0008_add-notion.sql b/apps/backend/drizzle/0008_add-notion.sql
deleted file mode 100644
index 17cd0206..00000000
--- a/apps/backend/drizzle/0008_add-notion.sql
+++ /dev/null
@@ -1,2 +0,0 @@
--- Custom SQL migration file, put you code below! --
-INSERT INTO "document_type" (type) VALUES ('notion') ON CONFLICT DO NOTHING; \ No newline at end of file
diff --git a/apps/backend/drizzle/0009_milky_sleepwalker.sql b/apps/backend/drizzle/0009_milky_sleepwalker.sql
deleted file mode 100644
index d32443c1..00000000
--- a/apps/backend/drizzle/0009_milky_sleepwalker.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE "waitlist" ADD COLUMN "created_at" timestamp with time zone DEFAULT now() NOT NULL; \ No newline at end of file
diff --git a/apps/backend/drizzle/0010_heavy_preak.sql b/apps/backend/drizzle/0010_heavy_preak.sql
deleted file mode 100644
index 003d62ae..00000000
--- a/apps/backend/drizzle/0010_heavy_preak.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-ALTER TABLE "documents" DROP CONSTRAINT "documents_user_id_users_id_fk";
---> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "documents" ADD CONSTRAINT "documents_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
diff --git a/apps/backend/drizzle/0011_new_liz_osborn.sql b/apps/backend/drizzle/0011_new_liz_osborn.sql
deleted file mode 100644
index b5679196..00000000
--- a/apps/backend/drizzle/0011_new_liz_osborn.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE "documents" ADD COLUMN "error_message" text; \ No newline at end of file
diff --git a/apps/backend/drizzle/0012_small_mystique.sql b/apps/backend/drizzle/0012_small_mystique.sql
deleted file mode 100644
index e89fb98a..00000000
--- a/apps/backend/drizzle/0012_small_mystique.sql
+++ /dev/null
@@ -1,2 +0,0 @@
--- Active: 1732308352274@@127.0.0.1@5432@supermemorydhravya
-ALTER TABLE "users" ADD COLUMN "last_api_key_generated_at" timestamp DEFAULT now(); \ No newline at end of file
diff --git a/apps/backend/drizzle/0013_sharp_hemingway.sql b/apps/backend/drizzle/0013_sharp_hemingway.sql
deleted file mode 100644
index 1f295677..00000000
--- a/apps/backend/drizzle/0013_sharp_hemingway.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE "documents" ADD COLUMN "content_hash" text; \ No newline at end of file
diff --git a/apps/backend/drizzle/0014_mighty_the_captain.sql b/apps/backend/drizzle/0014_mighty_the_captain.sql
deleted file mode 100644
index 40e7b1b8..00000000
--- a/apps/backend/drizzle/0014_mighty_the_captain.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE "users" ADD COLUMN "stripe_customer_id" text;--> statement-breakpoint
-ALTER TABLE "users" ADD COLUMN "tier" text DEFAULT 'free' NOT NULL; \ No newline at end of file
diff --git a/apps/backend/drizzle/0015_perpetual_mauler.sql b/apps/backend/drizzle/0015_perpetual_mauler.sql
deleted file mode 100644
index ec419523..00000000
--- a/apps/backend/drizzle/0015_perpetual_mauler.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE "chunks" ALTER COLUMN "embeddings" SET DATA TYPE vector(768); \ No newline at end of file
diff --git a/apps/backend/drizzle/0016_good_deathbird.sql b/apps/backend/drizzle/0016_good_deathbird.sql
deleted file mode 100644
index 7c6505de..00000000
--- a/apps/backend/drizzle/0016_good_deathbird.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-ALTER TABLE "chunks" ALTER COLUMN "embeddings" SET DATA TYPE vector(768);--> statement-breakpoint
-CREATE INDEX IF NOT EXISTS "documents_search_idx" ON "documents" USING gin ((
- setweight(to_tsvector('english', coalesce("content", '')),'A') ||
- setweight(to_tsvector('english', coalesce("title", '')),'B') ||
- setweight(to_tsvector('english', coalesce("description", '')),'C') ||
- setweight(to_tsvector('english', coalesce("url", '')),'D')
- )); \ No newline at end of file
diff --git a/apps/backend/drizzle/0018_past_inertia.sql b/apps/backend/drizzle/0018_past_inertia.sql
deleted file mode 100644
index a7d2e321..00000000
--- a/apps/backend/drizzle/0018_past_inertia.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-DROP INDEX IF EXISTS "documents_search_idx";--> statement-breakpoint
-CREATE INDEX IF NOT EXISTS "documents_search_idx" ON "documents" USING gin ((
- setweight(to_tsvector('english', coalesce("content", '')),'A') ||
- setweight(to_tsvector('english', coalesce("title", '')),'B') ||
- setweight(to_tsvector('english', coalesce("description", '')),'C') ||
- setweight(to_tsvector('english', coalesce("url", '')),'D')
- )); \ No newline at end of file
diff --git a/apps/backend/drizzle/0019_vengeful_marten_broadcloak.sql b/apps/backend/drizzle/0019_vengeful_marten_broadcloak.sql
deleted file mode 100644
index 7bed7a59..00000000
--- a/apps/backend/drizzle/0019_vengeful_marten_broadcloak.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE "documents" ADD COLUMN "metadata" jsonb; \ No newline at end of file
diff --git a/apps/backend/drizzle/0020_opposite_steel_serpent.sql b/apps/backend/drizzle/0020_opposite_steel_serpent.sql
deleted file mode 100644
index 3b0e2042..00000000
--- a/apps/backend/drizzle/0020_opposite_steel_serpent.sql
+++ /dev/null
@@ -1,2 +0,0 @@
--- Active: 1732308352274@@127.0.0.1@5432@supermemorydhravya
-DROP TABLE "job"; \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0000_snapshot.json b/apps/backend/drizzle/meta/0000_snapshot.json
deleted file mode 100644
index 4d7bf98d..00000000
--- a/apps/backend/drizzle/meta/0000_snapshot.json
+++ /dev/null
@@ -1,1060 +0,0 @@
-{
- "id": "a40bb20a-1c12-4e15-a7fc-cf46e9d4507f",
- "prevId": "00000000-0000-0000-0000-000000000000",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "diskann",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_url_user_id_idx": {
- "name": "document_url_user_id_idx",
- "columns": [
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0001_snapshot.json b/apps/backend/drizzle/meta/0001_snapshot.json
deleted file mode 100644
index 3c02b35d..00000000
--- a/apps/backend/drizzle/meta/0001_snapshot.json
+++ /dev/null
@@ -1,1060 +0,0 @@
-{
- "id": "67f177d5-9303-4ae0-ad66-e04d0624dab1",
- "prevId": "a40bb20a-1c12-4e15-a7fc-cf46e9d4507f",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "columnsFrom": [
- "user_id"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "columns": [
- "uuid"
- ],
- "nullsNotDistinct": false
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "diskann",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "columnsFrom": [
- "document_id"
- ],
- "tableTo": "documents",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "columnsFrom": [
- "content_id"
- ],
- "tableTo": "documents",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "columnsFrom": [
- "space_id"
- ],
- "tableTo": "spaces",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "document_url_user_id_idx": {
- "name": "document_url_user_id_idx",
- "columns": [
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "columnsFrom": [
- "type"
- ],
- "tableTo": "document_type",
- "columnsTo": [
- "type"
- ],
- "onUpdate": "no action",
- "onDelete": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "columnsFrom": [
- "user_id"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "restrict"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "columns": [
- "uuid"
- ],
- "nullsNotDistinct": false
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "columnsFrom": [
- "user_id"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "columnsFrom": [
- "space_id"
- ],
- "tableTo": "spaces",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "columnsFrom": [
- "status"
- ],
- "tableTo": "space_access_status",
- "columnsTo": [
- "status"
- ],
- "onUpdate": "no action",
- "onDelete": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "columnsFrom": [
- "spaceId"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "restrict"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "columnsFrom": [
- "user_id"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "restrict"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "columns": [
- "uuid"
- ],
- "nullsNotDistinct": false
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "columns": [
- "uuid"
- ],
- "nullsNotDistinct": false
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "columns": [
- "email"
- ],
- "nullsNotDistinct": false
- }
- }
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0002_snapshot.json b/apps/backend/drizzle/meta/0002_snapshot.json
deleted file mode 100644
index c36c66eb..00000000
--- a/apps/backend/drizzle/meta/0002_snapshot.json
+++ /dev/null
@@ -1,1067 +0,0 @@
-{
- "id": "be517863-31ba-4bf3-9a57-208d29723161",
- "prevId": "67f177d5-9303-4ae0-ad66-e04d0624dab1",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "diskann",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_url_user_id_idx": {
- "name": "document_url_user_id_idx",
- "columns": [
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0003_snapshot.json b/apps/backend/drizzle/meta/0003_snapshot.json
deleted file mode 100644
index a1730bb5..00000000
--- a/apps/backend/drizzle/meta/0003_snapshot.json
+++ /dev/null
@@ -1,1095 +0,0 @@
-{
- "id": "bce9d9c5-68a2-417c-a9f8-454734bf47fa",
- "prevId": "be517863-31ba-4bf3-9a57-208d29723161",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_url_user_id_idx": {
- "name": "document_url_user_id_idx",
- "columns": [
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0004_snapshot.json b/apps/backend/drizzle/meta/0004_snapshot.json
deleted file mode 100644
index acc4663d..00000000
--- a/apps/backend/drizzle/meta/0004_snapshot.json
+++ /dev/null
@@ -1,1173 +0,0 @@
-{
- "id": "6f9ba02f-28e8-4a11-8606-847dbc2ad43f",
- "prevId": "bce9d9c5-68a2-417c-a9f8-454734bf47fa",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_url_user_id_idx": {
- "name": "document_url_user_id_idx",
- "columns": [
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0005_snapshot.json b/apps/backend/drizzle/meta/0005_snapshot.json
deleted file mode 100644
index 72fa7e54..00000000
--- a/apps/backend/drizzle/meta/0005_snapshot.json
+++ /dev/null
@@ -1,1173 +0,0 @@
-{
- "id": "5410ff1a-d1d0-4f47-b84e-a3507bd3f148",
- "prevId": "6f9ba02f-28e8-4a11-8606-847dbc2ad43f",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "columnsFrom": [
- "user_id"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "columns": [
- "uuid"
- ],
- "nullsNotDistinct": false
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "hnsw",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "columnsFrom": [
- "document_id"
- ],
- "tableTo": "documents",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "columnsFrom": [
- "content_id"
- ],
- "tableTo": "documents",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "columnsFrom": [
- "space_id"
- ],
- "tableTo": "spaces",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "document_url_user_id_idx": {
- "name": "document_url_user_id_idx",
- "columns": [
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "columnsFrom": [
- "type"
- ],
- "tableTo": "document_type",
- "columnsTo": [
- "type"
- ],
- "onUpdate": "no action",
- "onDelete": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "columnsFrom": [
- "user_id"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "restrict"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "columns": [
- "uuid"
- ],
- "nullsNotDistinct": false
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "columnsFrom": [
- "user_id"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "columnsFrom": [
- "user_id"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "columnsFrom": [
- "space_id"
- ],
- "tableTo": "spaces",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "columnsFrom": [
- "space_id"
- ],
- "tableTo": "spaces",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "columnsFrom": [
- "status"
- ],
- "tableTo": "space_access_status",
- "columnsTo": [
- "status"
- ],
- "onUpdate": "no action",
- "onDelete": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "columnsFrom": [
- "spaceId"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "restrict"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "columnsFrom": [
- "user_id"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "restrict"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "columns": [
- "uuid"
- ],
- "nullsNotDistinct": false
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "columns": [
- "uuid"
- ],
- "nullsNotDistinct": false
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "columns": [
- "email"
- ],
- "nullsNotDistinct": false
- }
- }
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0006_snapshot.json b/apps/backend/drizzle/meta/0006_snapshot.json
deleted file mode 100644
index e69b69cb..00000000
--- a/apps/backend/drizzle/meta/0006_snapshot.json
+++ /dev/null
@@ -1,1189 +0,0 @@
-{
- "id": "d1518064-adca-451e-a41f-f6902c26f3aa",
- "prevId": "5410ff1a-d1d0-4f47-b84e-a3507bd3f148",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_url_user_id_idx": {
- "name": "document_url_user_id_idx",
- "columns": [
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0007_snapshot.json b/apps/backend/drizzle/meta/0007_snapshot.json
deleted file mode 100644
index e91f60a8..00000000
--- a/apps/backend/drizzle/meta/0007_snapshot.json
+++ /dev/null
@@ -1,1168 +0,0 @@
-{
- "id": "912b42e8-2827-4572-a698-577ba751461e",
- "prevId": "d1518064-adca-451e-a41f-f6902c26f3aa",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0008_snapshot.json b/apps/backend/drizzle/meta/0008_snapshot.json
deleted file mode 100644
index b4cdeb49..00000000
--- a/apps/backend/drizzle/meta/0008_snapshot.json
+++ /dev/null
@@ -1,1168 +0,0 @@
-{
- "id": "6f307490-3b7f-4c61-aec9-f51b65aae436",
- "prevId": "912b42e8-2827-4572-a698-577ba751461e",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "columnsFrom": [
- "user_id"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "columns": [
- "uuid"
- ],
- "nullsNotDistinct": false
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "hnsw",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "columnsFrom": [
- "document_id"
- ],
- "tableTo": "documents",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "columnsFrom": [
- "content_id"
- ],
- "tableTo": "documents",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "columnsFrom": [
- "space_id"
- ],
- "tableTo": "spaces",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "columnsFrom": [
- "type"
- ],
- "tableTo": "document_type",
- "columnsTo": [
- "type"
- ],
- "onUpdate": "no action",
- "onDelete": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "columnsFrom": [
- "user_id"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "restrict"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "columns": [
- "uuid"
- ],
- "nullsNotDistinct": false
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "columnsFrom": [
- "user_id"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "columnsFrom": [
- "user_id"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "columnsFrom": [
- "space_id"
- ],
- "tableTo": "spaces",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "columnsFrom": [
- "space_id"
- ],
- "tableTo": "spaces",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "cascade"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "columnsFrom": [
- "status"
- ],
- "tableTo": "space_access_status",
- "columnsTo": [
- "status"
- ],
- "onUpdate": "no action",
- "onDelete": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "columnsFrom": [
- "spaceId"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "restrict"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "columnsFrom": [
- "user_id"
- ],
- "tableTo": "users",
- "columnsTo": [
- "id"
- ],
- "onUpdate": "no action",
- "onDelete": "restrict"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "columns": [
- "uuid"
- ],
- "nullsNotDistinct": false
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "with": {},
- "method": "btree",
- "concurrently": false
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "with": {},
- "method": "btree",
- "concurrently": false
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "columns": [
- "uuid"
- ],
- "nullsNotDistinct": false
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "columns": [
- "email"
- ],
- "nullsNotDistinct": false
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0009_snapshot.json b/apps/backend/drizzle/meta/0009_snapshot.json
deleted file mode 100644
index 33f2bbb5..00000000
--- a/apps/backend/drizzle/meta/0009_snapshot.json
+++ /dev/null
@@ -1,1175 +0,0 @@
-{
- "id": "b661c516-b919-46cf-81f3-6d7743acaf2f",
- "prevId": "6f307490-3b7f-4c61-aec9-f51b65aae436",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0010_snapshot.json b/apps/backend/drizzle/meta/0010_snapshot.json
deleted file mode 100644
index 3cb3360c..00000000
--- a/apps/backend/drizzle/meta/0010_snapshot.json
+++ /dev/null
@@ -1,1175 +0,0 @@
-{
- "id": "6fcd94f9-4308-4619-bc19-ec851edf8964",
- "prevId": "b661c516-b919-46cf-81f3-6d7743acaf2f",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0011_snapshot.json b/apps/backend/drizzle/meta/0011_snapshot.json
deleted file mode 100644
index b97e0826..00000000
--- a/apps/backend/drizzle/meta/0011_snapshot.json
+++ /dev/null
@@ -1,1181 +0,0 @@
-{
- "id": "e8a080b4-de5d-4015-945e-7bf5f461f16f",
- "prevId": "6fcd94f9-4308-4619-bc19-ec851edf8964",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- },
- "error_message": {
- "name": "error_message",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0012_snapshot.json b/apps/backend/drizzle/meta/0012_snapshot.json
deleted file mode 100644
index 829368fe..00000000
--- a/apps/backend/drizzle/meta/0012_snapshot.json
+++ /dev/null
@@ -1,1188 +0,0 @@
-{
- "id": "15bb0365-73b0-4c03-9aa8-6249e89a4afd",
- "prevId": "e8a080b4-de5d-4015-945e-7bf5f461f16f",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- },
- "error_message": {
- "name": "error_message",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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()"
- },
- "last_api_key_generated_at": {
- "name": "last_api_key_generated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false,
- "default": "now()"
- }
- },
- "indexes": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0013_snapshot.json b/apps/backend/drizzle/meta/0013_snapshot.json
deleted file mode 100644
index 686ad86a..00000000
--- a/apps/backend/drizzle/meta/0013_snapshot.json
+++ /dev/null
@@ -1,1194 +0,0 @@
-{
- "id": "8204e351-f30b-41a5-b071-c50bad426920",
- "prevId": "15bb0365-73b0-4c03-9aa8-6249e89a4afd",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- },
- "error_message": {
- "name": "error_message",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "content_hash": {
- "name": "content_hash",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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()"
- },
- "last_api_key_generated_at": {
- "name": "last_api_key_generated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false,
- "default": "now()"
- }
- },
- "indexes": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0014_snapshot.json b/apps/backend/drizzle/meta/0014_snapshot.json
deleted file mode 100644
index 6391612e..00000000
--- a/apps/backend/drizzle/meta/0014_snapshot.json
+++ /dev/null
@@ -1,1207 +0,0 @@
-{
- "id": "f5cef50a-50ec-4b49-99c2-4da3a0f6a098",
- "prevId": "8204e351-f30b-41a5-b071-c50bad426920",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- },
- "error_message": {
- "name": "error_message",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "content_hash": {
- "name": "content_hash",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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()"
- },
- "last_api_key_generated_at": {
- "name": "last_api_key_generated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false,
- "default": "now()"
- },
- "stripe_customer_id": {
- "name": "stripe_customer_id",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "tier": {
- "name": "tier",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'free'"
- }
- },
- "indexes": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0015_snapshot.json b/apps/backend/drizzle/meta/0015_snapshot.json
deleted file mode 100644
index d19dbfbb..00000000
--- a/apps/backend/drizzle/meta/0015_snapshot.json
+++ /dev/null
@@ -1,1207 +0,0 @@
-{
- "id": "8529db1b-2d33-49e0-a413-f517eae7e4e4",
- "prevId": "f5cef50a-50ec-4b49-99c2-4da3a0f6a098",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(7)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- },
- "error_message": {
- "name": "error_message",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "content_hash": {
- "name": "content_hash",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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()"
- },
- "last_api_key_generated_at": {
- "name": "last_api_key_generated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false,
- "default": "now()"
- },
- "stripe_customer_id": {
- "name": "stripe_customer_id",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "tier": {
- "name": "tier",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'free'"
- }
- },
- "indexes": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0016_snapshot.json b/apps/backend/drizzle/meta/0016_snapshot.json
deleted file mode 100644
index 2020c691..00000000
--- a/apps/backend/drizzle/meta/0016_snapshot.json
+++ /dev/null
@@ -1,1222 +0,0 @@
-{
- "id": "23a39e70-a9c2-44cd-a3fb-22b19efef79e",
- "prevId": "8529db1b-2d33-49e0-a413-f517eae7e4e4",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(768)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- },
- "error_message": {
- "name": "error_message",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "content_hash": {
- "name": "content_hash",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "documents_search_idx": {
- "name": "documents_search_idx",
- "columns": [
- {
- "expression": "(\n setweight(to_tsvector('english', coalesce(\"content\", '')),'A') ||\n setweight(to_tsvector('english', coalesce(\"title\", '')),'B') ||\n setweight(to_tsvector('english', coalesce(\"description\", '')),'C') ||\n setweight(to_tsvector('english', coalesce(\"url\", '')),'D')\n )",
- "asc": true,
- "isExpression": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "gin",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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()"
- },
- "last_api_key_generated_at": {
- "name": "last_api_key_generated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false,
- "default": "now()"
- },
- "stripe_customer_id": {
- "name": "stripe_customer_id",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "tier": {
- "name": "tier",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'free'"
- }
- },
- "indexes": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0017_snapshot.json b/apps/backend/drizzle/meta/0017_snapshot.json
deleted file mode 100644
index efd17477..00000000
--- a/apps/backend/drizzle/meta/0017_snapshot.json
+++ /dev/null
@@ -1,1222 +0,0 @@
-{
- "id": "f8d9da82-e3be-46a9-b52e-a70dc2d24505",
- "prevId": "23a39e70-a9c2-44cd-a3fb-22b19efef79e",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(768)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- },
- "error_message": {
- "name": "error_message",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "content_hash": {
- "name": "content_hash",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "documents_search_idx": {
- "name": "documents_search_idx",
- "columns": [
- {
- "expression": "(\n setweight(to_tsvector('english', coalesce(preprocess_for_text_search(\"content\"), '')),'A') ||\n setweight(to_tsvector('english', coalesce(preprocess_for_text_search(\"title\"), '')),'B') ||\n setweight(to_tsvector('english', coalesce(preprocess_for_text_search(\"description\"), '')),'C') ||\n setweight(to_tsvector('english', coalesce(preprocess_for_text_search(\"url\"), '')),'D')\n )",
- "asc": true,
- "isExpression": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "gin",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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()"
- },
- "last_api_key_generated_at": {
- "name": "last_api_key_generated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false,
- "default": "now()"
- },
- "stripe_customer_id": {
- "name": "stripe_customer_id",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "tier": {
- "name": "tier",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'free'"
- }
- },
- "indexes": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0018_snapshot.json b/apps/backend/drizzle/meta/0018_snapshot.json
deleted file mode 100644
index 6b11027b..00000000
--- a/apps/backend/drizzle/meta/0018_snapshot.json
+++ /dev/null
@@ -1,1222 +0,0 @@
-{
- "id": "af318033-5cb6-41bf-9f68-2ebd75bd0a47",
- "prevId": "f8d9da82-e3be-46a9-b52e-a70dc2d24505",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(768)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- },
- "error_message": {
- "name": "error_message",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "content_hash": {
- "name": "content_hash",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "documents_search_idx": {
- "name": "documents_search_idx",
- "columns": [
- {
- "expression": "(\n setweight(to_tsvector('english', coalesce(\"content\", '')),'A') ||\n setweight(to_tsvector('english', coalesce(\"title\", '')),'B') ||\n setweight(to_tsvector('english', coalesce(\"description\", '')),'C') ||\n setweight(to_tsvector('english', coalesce(\"url\", '')),'D')\n )",
- "asc": true,
- "isExpression": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "gin",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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()"
- },
- "last_api_key_generated_at": {
- "name": "last_api_key_generated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false,
- "default": "now()"
- },
- "stripe_customer_id": {
- "name": "stripe_customer_id",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "tier": {
- "name": "tier",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'free'"
- }
- },
- "indexes": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0019_snapshot.json b/apps/backend/drizzle/meta/0019_snapshot.json
deleted file mode 100644
index 7c75753c..00000000
--- a/apps/backend/drizzle/meta/0019_snapshot.json
+++ /dev/null
@@ -1,1228 +0,0 @@
-{
- "id": "0f7b4e6d-295d-481f-84be-6b31178aa1b6",
- "prevId": "af318033-5cb6-41bf-9f68-2ebd75bd0a47",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(768)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- },
- "error_message": {
- "name": "error_message",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "content_hash": {
- "name": "content_hash",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "documents_search_idx": {
- "name": "documents_search_idx",
- "columns": [
- {
- "expression": "(\n setweight(to_tsvector('english', coalesce(\"content\", '')),'A') ||\n setweight(to_tsvector('english', coalesce(\"title\", '')),'B') ||\n setweight(to_tsvector('english', coalesce(\"description\", '')),'C') ||\n setweight(to_tsvector('english', coalesce(\"url\", '')),'D')\n )",
- "asc": true,
- "isExpression": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "gin",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.job": {
- "name": "job",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "attempts": {
- "name": "attempts",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "lastAttemptAt": {
- "name": "lastAttemptAt",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "error": {
- "name": "error",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "user_id_url_idx": {
- "name": "user_id_url_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "url",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "job_user_id_users_id_fk": {
- "name": "job_user_id_users_id_fk",
- "tableFrom": "job",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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()"
- },
- "last_api_key_generated_at": {
- "name": "last_api_key_generated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false,
- "default": "now()"
- },
- "stripe_customer_id": {
- "name": "stripe_customer_id",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "tier": {
- "name": "tier",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'free'"
- }
- },
- "indexes": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/0020_snapshot.json b/apps/backend/drizzle/meta/0020_snapshot.json
deleted file mode 100644
index a6d491d4..00000000
--- a/apps/backend/drizzle/meta/0020_snapshot.json
+++ /dev/null
@@ -1,1127 +0,0 @@
-{
- "id": "d76a6f6e-ff3d-49d8-b8fe-a5d3f48924da",
- "prevId": "0f7b4e6d-295d-481f-84be-6b31178aa1b6",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.chat_threads": {
- "name": "chat_threads",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "firstMessage": {
- "name": "firstMessage",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "messages": {
- "name": "messages",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "chat_threads_user_idx": {
- "name": "chat_threads_user_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "chat_threads_user_id_users_id_fk": {
- "name": "chat_threads_user_id_users_id_fk",
- "tableFrom": "chat_threads",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_threads_uuid_unique": {
- "name": "chat_threads_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.chunks": {
- "name": "chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "document_id": {
- "name": "document_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "text_content": {
- "name": "text_content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "order_in_document": {
- "name": "order_in_document",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "embeddings": {
- "name": "embeddings",
- "type": "vector(768)",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "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": {
- "chunk_id_idx": {
- "name": "chunk_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "chunk_document_id_idx": {
- "name": "chunk_document_id_idx",
- "columns": [
- {
- "expression": "document_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "embeddingIndex": {
- "name": "embeddingIndex",
- "columns": [
- {
- "expression": "embeddings",
- "isExpression": false,
- "asc": true,
- "nulls": "last",
- "opclass": "vector_cosine_ops"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "hnsw",
- "with": {}
- }
- },
- "foreignKeys": {
- "chunks_document_id_documents_id_fk": {
- "name": "chunks_document_id_documents_id_fk",
- "tableFrom": "chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.content_to_space": {
- "name": "content_to_space",
- "schema": "",
- "columns": {
- "content_id": {
- "name": "content_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "content_id_space_id_unique": {
- "name": "content_id_space_id_unique",
- "columns": [
- {
- "expression": "content_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "content_to_space_content_id_documents_id_fk": {
- "name": "content_to_space_content_id_documents_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "documents",
- "columnsFrom": [
- "content_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "content_to_space_space_id_spaces_id_fk": {
- "name": "content_to_space_space_id_spaces_id_fk",
- "tableFrom": "content_to_space",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.document_type": {
- "name": "document_type",
- "schema": "",
- "columns": {
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "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": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "og_image": {
- "name": "og_image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "raw": {
- "name": "raw",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "is_successfully_processed": {
- "name": "is_successfully_processed",
- "type": "boolean",
- "primaryKey": false,
- "notNull": false,
- "default": false
- },
- "error_message": {
- "name": "error_message",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "content_hash": {
- "name": "content_hash",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {
- "document_id_idx": {
- "name": "document_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_uuid_idx": {
- "name": "document_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_type_idx": {
- "name": "document_type_idx",
- "columns": [
- {
- "expression": "type",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "document_raw_user_idx": {
- "name": "document_raw_user_idx",
- "columns": [
- {
- "expression": "raw",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "documents_search_idx": {
- "name": "documents_search_idx",
- "columns": [
- {
- "expression": "(\n setweight(to_tsvector('english', coalesce(\"content\", '')),'A') ||\n setweight(to_tsvector('english', coalesce(\"title\", '')),'B') ||\n setweight(to_tsvector('english', coalesce(\"description\", '')),'C') ||\n setweight(to_tsvector('english', coalesce(\"url\", '')),'D')\n )",
- "asc": true,
- "isExpression": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "gin",
- "with": {}
- }
- },
- "foreignKeys": {
- "documents_type_document_type_type_fk": {
- "name": "documents_type_document_type_type_fk",
- "tableFrom": "documents",
- "tableTo": "document_type",
- "columnsFrom": [
- "type"
- ],
- "columnsTo": [
- "type"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- },
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "documents_uuid_unique": {
- "name": "documents_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.saved_spaces": {
- "name": "saved_spaces",
- "schema": "",
- "columns": {
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "saved_at": {
- "name": "saved_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "saved_spaces_user_space_idx": {
- "name": "saved_spaces_user_space_idx",
- "columns": [
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "saved_spaces_user_id_users_id_fk": {
- "name": "saved_spaces_user_id_users_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "saved_spaces_space_id_spaces_id_fk": {
- "name": "saved_spaces_space_id_spaces_id_fk",
- "tableFrom": "saved_spaces",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access": {
- "name": "space_access",
- "schema": "",
- "columns": {
- "space_id": {
- "name": "space_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "user_email": {
- "name": "user_email",
- "type": "varchar(512)",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_type": {
- "name": "access_type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'read'"
- }
- },
- "indexes": {
- "space_id_user_email_idx": {
- "name": "space_id_user_email_idx",
- "columns": [
- {
- "expression": "space_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_access_space_id_spaces_id_fk": {
- "name": "space_access_space_id_spaces_id_fk",
- "tableFrom": "space_access",
- "tableTo": "spaces",
- "columnsFrom": [
- "space_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "space_access_status_space_access_status_status_fk": {
- "name": "space_access_status_space_access_status_status_fk",
- "tableFrom": "space_access",
- "tableTo": "space_access_status",
- "columnsFrom": [
- "status"
- ],
- "columnsTo": [
- "status"
- ],
- "onDelete": "no action",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_access_status": {
- "name": "space_access_status",
- "schema": "",
- "columns": {
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.space_members": {
- "name": "space_members",
- "schema": "",
- "columns": {
- "spaceId": {
- "name": "spaceId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {
- "space_members_space_user_idx": {
- "name": "space_members_space_user_idx",
- "columns": [
- {
- "expression": "spaceId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "user_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {
- "space_members_spaceId_users_id_fk": {
- "name": "space_members_spaceId_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "spaceId"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- },
- "space_members_user_id_users_id_fk": {
- "name": "space_members_user_id_users_id_fk",
- "tableFrom": "space_members",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "restrict",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "public.spaces": {
- "name": "spaces",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "bigserial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "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,
- "default": "now()"
- },
- "ownerId": {
- "name": "ownerId",
- "type": "integer",
- "primaryKey": false,
- "notNull": true
- },
- "is_public": {
- "name": "is_public",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- }
- },
- "indexes": {
- "spaces_id_idx": {
- "name": "spaces_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_owner_id_idx": {
- "name": "spaces_owner_id_idx",
- "columns": [
- {
- "expression": "ownerId",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "spaces_name_idx": {
- "name": "spaces_name_idx",
- "columns": [
- {
- "expression": "name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "spaces_uuid_unique": {
- "name": "spaces_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- }
- }
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "serial",
- "primaryKey": true,
- "notNull": true
- },
- "uuid": {
- "name": "uuid",
- "type": "varchar(36)",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true,
- "default": false
- },
- "profile_picture_url": {
- "name": "profile_picture_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "telegram_id": {
- "name": "telegram_id",
- "type": "varchar(255)",
- "primaryKey": false,
- "notNull": false
- },
- "has_onboarded": {
- "name": "has_onboarded",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "default": 0
- },
- "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()"
- },
- "last_api_key_generated_at": {
- "name": "last_api_key_generated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false,
- "default": "now()"
- },
- "stripe_customer_id": {
- "name": "stripe_customer_id",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "tier": {
- "name": "tier",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'free'"
- }
- },
- "indexes": {
- "users_id_idx": {
- "name": "users_id_idx",
- "columns": [
- {
- "expression": "id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_uuid_idx": {
- "name": "users_uuid_idx",
- "columns": [
- {
- "expression": "uuid",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_email_idx": {
- "name": "users_email_idx",
- "columns": [
- {
- "expression": "email",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_name_idx": {
- "name": "users_name_idx",
- "columns": [
- {
- "expression": "first_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- },
- {
- "expression": "last_name",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_created_at_idx": {
- "name": "users_created_at_idx",
- "columns": [
- {
- "expression": "created_at",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- },
- "users_telegram_id_idx": {
- "name": "users_telegram_id_idx",
- "columns": [
- {
- "expression": "telegram_id",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": true,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_uuid_unique": {
- "name": "users_uuid_unique",
- "nullsNotDistinct": false,
- "columns": [
- "uuid"
- ]
- },
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- }
- }
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "email": {
- "name": "email",
- "type": "varchar(512)",
- "primaryKey": true,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-} \ No newline at end of file
diff --git a/apps/backend/drizzle/meta/_journal.json b/apps/backend/drizzle/meta/_journal.json
deleted file mode 100644
index ebd7e43a..00000000
--- a/apps/backend/drizzle/meta/_journal.json
+++ /dev/null
@@ -1,146 +0,0 @@
-{
- "version": "7",
- "dialect": "postgresql",
- "entries": [
- {
- "idx": 0,
- "version": "7",
- "when": 1731880515136,
- "tag": "0000_odd_impossible_man",
- "breakpoints": true
- },
- {
- "idx": 1,
- "version": "7",
- "when": 1731880538346,
- "tag": "0001_seed-types",
- "breakpoints": true
- },
- {
- "idx": 2,
- "version": "7",
- "when": 1731970096987,
- "tag": "0002_skinny_princess_powerful",
- "breakpoints": true
- },
- {
- "idx": 3,
- "version": "7",
- "when": 1732290500234,
- "tag": "0003_luxuriant_annihilus",
- "breakpoints": true
- },
- {
- "idx": 4,
- "version": "7",
- "when": 1732291518161,
- "tag": "0004_early_rick_jones",
- "breakpoints": true
- },
- {
- "idx": 5,
- "version": "7",
- "when": 1732292492979,
- "tag": "0005_create-access-types",
- "breakpoints": true
- },
- {
- "idx": 6,
- "version": "7",
- "when": 1732308308233,
- "tag": "0006_wandering_grandmaster",
- "breakpoints": true
- },
- {
- "idx": 7,
- "version": "7",
- "when": 1732476625236,
- "tag": "0007_fantastic_serpent_society",
- "breakpoints": true
- },
- {
- "idx": 8,
- "version": "7",
- "when": 1732575691923,
- "tag": "0008_add-notion",
- "breakpoints": true
- },
- {
- "idx": 9,
- "version": "7",
- "when": 1732693411536,
- "tag": "0009_milky_sleepwalker",
- "breakpoints": true
- },
- {
- "idx": 10,
- "version": "7",
- "when": 1733037679877,
- "tag": "0010_heavy_preak",
- "breakpoints": true
- },
- {
- "idx": 11,
- "version": "7",
- "when": 1733177542412,
- "tag": "0011_new_liz_osborn",
- "breakpoints": true
- },
- {
- "idx": 12,
- "version": "7",
- "when": 1735033070115,
- "tag": "0012_small_mystique",
- "breakpoints": true
- },
- {
- "idx": 13,
- "version": "7",
- "when": 1736500817155,
- "tag": "0013_sharp_hemingway",
- "breakpoints": true
- },
- {
- "idx": 14,
- "version": "7",
- "when": 1736852938881,
- "tag": "0014_mighty_the_captain",
- "breakpoints": true
- },
- {
- "idx": 15,
- "version": "7",
- "when": 1737920848112,
- "tag": "0015_perpetual_mauler",
- "breakpoints": true
- },
- {
- "idx": 16,
- "version": "7",
- "when": 1739937938319,
- "tag": "0016_good_deathbird",
- "breakpoints": true
- },
- {
- "idx": 18,
- "version": "7",
- "when": 1739939254444,
- "tag": "0018_past_inertia",
- "breakpoints": true
- },
- {
- "idx": 19,
- "version": "7",
- "when": 1741025619581,
- "tag": "0019_vengeful_marten_broadcloak",
- "breakpoints": true
- },
- {
- "idx": 20,
- "version": "7",
- "when": 1741026944027,
- "tag": "0020_opposite_steel_serpent",
- "breakpoints": true
- }
- ]
-} \ No newline at end of file
diff --git a/apps/backend/package.json b/apps/backend/package.json
deleted file mode 100644
index 664b501d..00000000
--- a/apps/backend/package.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "name": "supermemory-backend",
- "scripts": {
- "dev": "bunx wrangler -v && wrangler dev",
- "deploy": "bunx wrangler deploy --minify",
- "generate-migration": "dotenv -- npx drizzle-kit generate",
- "migrate:local": "bun run ./scripts/migrate.ts",
- "migrate:prod": "NODE_ENV=production bun run ./scripts/migrate.ts",
- "tail": "bunx wrangler tail"
- },
- "dependencies": {
- "@ai-sdk/google": "^0.0.51",
- "@ai-sdk/openai": "^0.0.70",
- "@hono/swagger-ui": "^0.5.0",
- "@hono/zod-openapi": "^0.18.3",
- "@hono/zod-validator": "^0.4.1",
- "@supermemory/db": "workspace:*",
- "ai": "4.0.16",
- "compromise": "^14.14.2",
- "dotenv": "^16.4.5",
- "drizzle-kit": "^0.25.0",
- "drizzle-orm": "^0.34.1",
- "hono": "^4.6.4",
- "openai": "^4.68.4",
- "postgres": "^3.4.4",
- "uuid": "^11.0.1",
- "wrangler": "^3.111.0",
- "zod": "^3.23.8"
- },
- "devDependencies": {
- "@cloudflare/workers-types": "^4.20250124.3"
- },
- "overrides": {
- "iron-webcrypto": "^1.2.1"
- }
-}
diff --git a/apps/backend/public/favicon.ico b/apps/backend/public/favicon.ico
deleted file mode 100644
index 90d7aafc..00000000
--- a/apps/backend/public/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/apps/backend/public/output.css b/apps/backend/public/output.css
deleted file mode 100644
index 925ff374..00000000
--- a/apps/backend/public/output.css
+++ /dev/null
@@ -1,853 +0,0 @@
-*, ::before, ::after {
- --tw-border-spacing-x: 0;
- --tw-border-spacing-y: 0;
- --tw-translate-x: 0;
- --tw-translate-y: 0;
- --tw-rotate: 0;
- --tw-skew-x: 0;
- --tw-skew-y: 0;
- --tw-scale-x: 1;
- --tw-scale-y: 1;
- --tw-pan-x: ;
- --tw-pan-y: ;
- --tw-pinch-zoom: ;
- --tw-scroll-snap-strictness: proximity;
- --tw-gradient-from-position: ;
- --tw-gradient-via-position: ;
- --tw-gradient-to-position: ;
- --tw-ordinal: ;
- --tw-slashed-zero: ;
- --tw-numeric-figure: ;
- --tw-numeric-spacing: ;
- --tw-numeric-fraction: ;
- --tw-ring-inset: ;
- --tw-ring-offset-width: 0px;
- --tw-ring-offset-color: #fff;
- --tw-ring-color: rgb(59 130 246 / 0.5);
- --tw-ring-offset-shadow: 0 0 #0000;
- --tw-ring-shadow: 0 0 #0000;
- --tw-shadow: 0 0 #0000;
- --tw-shadow-colored: 0 0 #0000;
- --tw-blur: ;
- --tw-brightness: ;
- --tw-contrast: ;
- --tw-grayscale: ;
- --tw-hue-rotate: ;
- --tw-invert: ;
- --tw-saturate: ;
- --tw-sepia: ;
- --tw-drop-shadow: ;
- --tw-backdrop-blur: ;
- --tw-backdrop-brightness: ;
- --tw-backdrop-contrast: ;
- --tw-backdrop-grayscale: ;
- --tw-backdrop-hue-rotate: ;
- --tw-backdrop-invert: ;
- --tw-backdrop-opacity: ;
- --tw-backdrop-saturate: ;
- --tw-backdrop-sepia: ;
- --tw-contain-size: ;
- --tw-contain-layout: ;
- --tw-contain-paint: ;
- --tw-contain-style: ;
-}
-
-::backdrop {
- --tw-border-spacing-x: 0;
- --tw-border-spacing-y: 0;
- --tw-translate-x: 0;
- --tw-translate-y: 0;
- --tw-rotate: 0;
- --tw-skew-x: 0;
- --tw-skew-y: 0;
- --tw-scale-x: 1;
- --tw-scale-y: 1;
- --tw-pan-x: ;
- --tw-pan-y: ;
- --tw-pinch-zoom: ;
- --tw-scroll-snap-strictness: proximity;
- --tw-gradient-from-position: ;
- --tw-gradient-via-position: ;
- --tw-gradient-to-position: ;
- --tw-ordinal: ;
- --tw-slashed-zero: ;
- --tw-numeric-figure: ;
- --tw-numeric-spacing: ;
- --tw-numeric-fraction: ;
- --tw-ring-inset: ;
- --tw-ring-offset-width: 0px;
- --tw-ring-offset-color: #fff;
- --tw-ring-color: rgb(59 130 246 / 0.5);
- --tw-ring-offset-shadow: 0 0 #0000;
- --tw-ring-shadow: 0 0 #0000;
- --tw-shadow: 0 0 #0000;
- --tw-shadow-colored: 0 0 #0000;
- --tw-blur: ;
- --tw-brightness: ;
- --tw-contrast: ;
- --tw-grayscale: ;
- --tw-hue-rotate: ;
- --tw-invert: ;
- --tw-saturate: ;
- --tw-sepia: ;
- --tw-drop-shadow: ;
- --tw-backdrop-blur: ;
- --tw-backdrop-brightness: ;
- --tw-backdrop-contrast: ;
- --tw-backdrop-grayscale: ;
- --tw-backdrop-hue-rotate: ;
- --tw-backdrop-invert: ;
- --tw-backdrop-opacity: ;
- --tw-backdrop-saturate: ;
- --tw-backdrop-sepia: ;
- --tw-contain-size: ;
- --tw-contain-layout: ;
- --tw-contain-paint: ;
- --tw-contain-style: ;
-}
-
-/*
-! tailwindcss v3.4.15 | MIT License | https://tailwindcss.com
-*/
-
-/*
-1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
-2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
-*/
-
-*,
-::before,
-::after {
- box-sizing: border-box;
- /* 1 */
- border-width: 0;
- /* 2 */
- border-style: solid;
- /* 2 */
- border-color: #e5e7eb;
- /* 2 */
-}
-
-::before,
-::after {
- --tw-content: '';
-}
-
-/*
-1. Use a consistent sensible line-height in all browsers.
-2. Prevent adjustments of font size after orientation changes in iOS.
-3. Use a more readable tab size.
-4. Use the user's configured `sans` font-family by default.
-5. Use the user's configured `sans` font-feature-settings by default.
-6. Use the user's configured `sans` font-variation-settings by default.
-7. Disable tap highlights on iOS
-*/
-
-html,
-:host {
- line-height: 1.5;
- /* 1 */
- -webkit-text-size-adjust: 100%;
- /* 2 */
- -moz-tab-size: 4;
- /* 3 */
- -o-tab-size: 4;
- tab-size: 4;
- /* 3 */
- font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
- /* 4 */
- font-feature-settings: normal;
- /* 5 */
- font-variation-settings: normal;
- /* 6 */
- -webkit-tap-highlight-color: transparent;
- /* 7 */
-}
-
-/*
-1. Remove the margin in all browsers.
-2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
-*/
-
-body {
- margin: 0;
- /* 1 */
- line-height: inherit;
- /* 2 */
-}
-
-/*
-1. Add the correct height in Firefox.
-2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
-3. Ensure horizontal rules are visible by default.
-*/
-
-hr {
- height: 0;
- /* 1 */
- color: inherit;
- /* 2 */
- border-top-width: 1px;
- /* 3 */
-}
-
-/*
-Add the correct text decoration in Chrome, Edge, and Safari.
-*/
-
-abbr:where([title]) {
- -webkit-text-decoration: underline dotted;
- text-decoration: underline dotted;
-}
-
-/*
-Remove the default font size and weight for headings.
-*/
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- font-size: inherit;
- font-weight: inherit;
-}
-
-/*
-Reset links to optimize for opt-in styling instead of opt-out.
-*/
-
-a {
- color: inherit;
- text-decoration: inherit;
-}
-
-/*
-Add the correct font weight in Edge and Safari.
-*/
-
-b,
-strong {
- font-weight: bolder;
-}
-
-/*
-1. Use the user's configured `mono` font-family by default.
-2. Use the user's configured `mono` font-feature-settings by default.
-3. Use the user's configured `mono` font-variation-settings by default.
-4. Correct the odd `em` font sizing in all browsers.
-*/
-
-code,
-kbd,
-samp,
-pre {
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
- /* 1 */
- font-feature-settings: normal;
- /* 2 */
- font-variation-settings: normal;
- /* 3 */
- font-size: 1em;
- /* 4 */
-}
-
-/*
-Add the correct font size in all browsers.
-*/
-
-small {
- font-size: 80%;
-}
-
-/*
-Prevent `sub` and `sup` elements from affecting the line height in all browsers.
-*/
-
-sub,
-sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
-}
-
-sub {
- bottom: -0.25em;
-}
-
-sup {
- top: -0.5em;
-}
-
-/*
-1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
-2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
-3. Remove gaps between table borders by default.
-*/
-
-table {
- text-indent: 0;
- /* 1 */
- border-color: inherit;
- /* 2 */
- border-collapse: collapse;
- /* 3 */
-}
-
-/*
-1. Change the font styles in all browsers.
-2. Remove the margin in Firefox and Safari.
-3. Remove default padding in all browsers.
-*/
-
-button,
-input,
-optgroup,
-select,
-textarea {
- font-family: inherit;
- /* 1 */
- font-feature-settings: inherit;
- /* 1 */
- font-variation-settings: inherit;
- /* 1 */
- font-size: 100%;
- /* 1 */
- font-weight: inherit;
- /* 1 */
- line-height: inherit;
- /* 1 */
- letter-spacing: inherit;
- /* 1 */
- color: inherit;
- /* 1 */
- margin: 0;
- /* 2 */
- padding: 0;
- /* 3 */
-}
-
-/*
-Remove the inheritance of text transform in Edge and Firefox.
-*/
-
-button,
-select {
- text-transform: none;
-}
-
-/*
-1. Correct the inability to style clickable types in iOS and Safari.
-2. Remove default button styles.
-*/
-
-button,
-input:where([type='button']),
-input:where([type='reset']),
-input:where([type='submit']) {
- -webkit-appearance: button;
- /* 1 */
- background-color: transparent;
- /* 2 */
- background-image: none;
- /* 2 */
-}
-
-/*
-Use the modern Firefox focus style for all focusable elements.
-*/
-
-:-moz-focusring {
- outline: auto;
-}
-
-/*
-Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
-*/
-
-:-moz-ui-invalid {
- box-shadow: none;
-}
-
-/*
-Add the correct vertical alignment in Chrome and Firefox.
-*/
-
-progress {
- vertical-align: baseline;
-}
-
-/*
-Correct the cursor style of increment and decrement buttons in Safari.
-*/
-
-::-webkit-inner-spin-button,
-::-webkit-outer-spin-button {
- height: auto;
-}
-
-/*
-1. Correct the odd appearance in Chrome and Safari.
-2. Correct the outline style in Safari.
-*/
-
-[type='search'] {
- -webkit-appearance: textfield;
- /* 1 */
- outline-offset: -2px;
- /* 2 */
-}
-
-/*
-Remove the inner padding in Chrome and Safari on macOS.
-*/
-
-::-webkit-search-decoration {
- -webkit-appearance: none;
-}
-
-/*
-1. Correct the inability to style clickable types in iOS and Safari.
-2. Change font properties to `inherit` in Safari.
-*/
-
-::-webkit-file-upload-button {
- -webkit-appearance: button;
- /* 1 */
- font: inherit;
- /* 2 */
-}
-
-/*
-Add the correct display in Chrome and Safari.
-*/
-
-summary {
- display: list-item;
-}
-
-/*
-Removes the default spacing and border for appropriate elements.
-*/
-
-blockquote,
-dl,
-dd,
-h1,
-h2,
-h3,
-h4,
-h5,
-h6,
-hr,
-figure,
-p,
-pre {
- margin: 0;
-}
-
-fieldset {
- margin: 0;
- padding: 0;
-}
-
-legend {
- padding: 0;
-}
-
-ol,
-ul,
-menu {
- list-style: none;
- margin: 0;
- padding: 0;
-}
-
-/*
-Reset default styling for dialogs.
-*/
-
-dialog {
- padding: 0;
-}
-
-/*
-Prevent resizing textareas horizontally by default.
-*/
-
-textarea {
- resize: vertical;
-}
-
-/*
-1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
-2. Set the default placeholder color to the user's configured gray 400 color.
-*/
-
-input::-moz-placeholder, textarea::-moz-placeholder {
- opacity: 1;
- /* 1 */
- color: #9ca3af;
- /* 2 */
-}
-
-input::placeholder,
-textarea::placeholder {
- opacity: 1;
- /* 1 */
- color: #9ca3af;
- /* 2 */
-}
-
-/*
-Set the default cursor for buttons.
-*/
-
-button,
-[role="button"] {
- cursor: pointer;
-}
-
-/*
-Make sure disabled buttons don't get the pointer cursor.
-*/
-
-:disabled {
- cursor: default;
-}
-
-/*
-1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
-2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
- This can trigger a poorly considered lint error in some tools but is included by design.
-*/
-
-img,
-svg,
-video,
-canvas,
-audio,
-iframe,
-embed,
-object {
- display: block;
- /* 1 */
- vertical-align: middle;
- /* 2 */
-}
-
-/*
-Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
-*/
-
-img,
-video {
- max-width: 100%;
- height: auto;
-}
-
-/* Make elements with the HTML hidden attribute stay hidden by default */
-
-[hidden]:where(:not([hidden="until-found"])) {
- display: none;
-}
-
-.container {
- width: 100%;
-}
-
-@media (min-width: 640px) {
- .container {
- max-width: 640px;
- }
-}
-
-@media (min-width: 768px) {
- .container {
- max-width: 768px;
- }
-}
-
-@media (min-width: 1024px) {
- .container {
- max-width: 1024px;
- }
-}
-
-@media (min-width: 1280px) {
- .container {
- max-width: 1280px;
- }
-}
-
-@media (min-width: 1536px) {
- .container {
- max-width: 1536px;
- }
-}
-
-.mx-auto {
- margin-left: auto;
- margin-right: auto;
-}
-
-.mb-12 {
- margin-bottom: 3rem;
-}
-
-.mb-4 {
- margin-bottom: 1rem;
-}
-
-.mb-8 {
- margin-bottom: 2rem;
-}
-
-.mt-8 {
- margin-top: 2rem;
-}
-
-.block {
- display: block;
-}
-
-.flex {
- display: flex;
-}
-
-.table {
- display: table;
-}
-
-.grid {
- display: grid;
-}
-
-.hidden {
- display: none;
-}
-
-.transform {
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-.items-center {
- align-items: center;
-}
-
-.justify-center {
- justify-content: center;
-}
-
-.justify-between {
- justify-content: space-between;
-}
-
-.gap-8 {
- gap: 2rem;
-}
-
-.space-x-4 > :not([hidden]) ~ :not([hidden]) {
- --tw-space-x-reverse: 0;
- margin-right: calc(1rem * var(--tw-space-x-reverse));
- margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
-}
-
-.space-x-8 > :not([hidden]) ~ :not([hidden]) {
- --tw-space-x-reverse: 0;
- margin-right: calc(2rem * var(--tw-space-x-reverse));
- margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
-}
-
-.space-y-2 > :not([hidden]) ~ :not([hidden]) {
- --tw-space-y-reverse: 0;
- margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
- margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
-}
-
-.rounded-lg {
- border-radius: 0.5rem;
-}
-
-.border {
- border-width: 1px;
-}
-
-.border-t {
- border-top-width: 1px;
-}
-
-.border-gray-600 {
- --tw-border-opacity: 1;
- border-color: rgb(75 85 99 / var(--tw-border-opacity, 1));
-}
-
-.border-gray-800 {
- --tw-border-opacity: 1;
- border-color: rgb(31 41 55 / var(--tw-border-opacity, 1));
-}
-
-.bg-black\/50 {
- background-color: rgb(0 0 0 / 0.5);
-}
-
-.bg-blue-600 {
- --tw-bg-opacity: 1;
- background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1));
-}
-
-.bg-gray-900 {
- --tw-bg-opacity: 1;
- background-color: rgb(17 24 39 / var(--tw-bg-opacity, 1));
-}
-
-.bg-gray-900\/50 {
- background-color: rgb(17 24 39 / 0.5);
-}
-
-.p-6 {
- padding: 1.5rem;
-}
-
-.px-6 {
- padding-left: 1.5rem;
- padding-right: 1.5rem;
-}
-
-.px-8 {
- padding-left: 2rem;
- padding-right: 2rem;
-}
-
-.py-16 {
- padding-top: 4rem;
- padding-bottom: 4rem;
-}
-
-.py-2 {
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
-}
-
-.py-20 {
- padding-top: 5rem;
- padding-bottom: 5rem;
-}
-
-.py-3 {
- padding-top: 0.75rem;
- padding-bottom: 0.75rem;
-}
-
-.py-4 {
- padding-top: 1rem;
- padding-bottom: 1rem;
-}
-
-.pt-8 {
- padding-top: 2rem;
-}
-
-.text-center {
- text-align: center;
-}
-
-.text-2xl {
- font-size: 1.5rem;
- line-height: 2rem;
-}
-
-.text-3xl {
- font-size: 1.875rem;
- line-height: 2.25rem;
-}
-
-.text-4xl {
- font-size: 2.25rem;
- line-height: 2.5rem;
-}
-
-.text-lg {
- font-size: 1.125rem;
- line-height: 1.75rem;
-}
-
-.text-xl {
- font-size: 1.25rem;
- line-height: 1.75rem;
-}
-
-.font-bold {
- font-weight: 700;
-}
-
-.font-semibold {
- font-weight: 600;
-}
-
-.text-gray-300 {
- --tw-text-opacity: 1;
- color: rgb(209 213 219 / var(--tw-text-opacity, 1));
-}
-
-.text-gray-400 {
- --tw-text-opacity: 1;
- color: rgb(156 163 175 / var(--tw-text-opacity, 1));
-}
-
-.text-white {
- --tw-text-opacity: 1;
- color: rgb(255 255 255 / var(--tw-text-opacity, 1));
-}
-
-.filter {
- filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
-}
-
-.transition {
- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
-}
-
-.duration-300 {
- transition-duration: 300ms;
-}
-
-.hover\:bg-blue-700:hover {
- --tw-bg-opacity: 1;
- background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
-}
-
-.hover\:bg-gray-700:hover {
- --tw-bg-opacity: 1;
- background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
-}
-
-.hover\:bg-gray-800:hover {
- --tw-bg-opacity: 1;
- background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1));
-}
-
-.hover\:text-white:hover {
- --tw-text-opacity: 1;
- color: rgb(255 255 255 / var(--tw-text-opacity, 1));
-}
-
-@media (min-width: 768px) {
- .md\:flex {
- display: flex;
- }
-
- .md\:grid-cols-3 {
- grid-template-columns: repeat(3, minmax(0, 1fr));
- }
-
- .md\:grid-cols-4 {
- grid-template-columns: repeat(4, minmax(0, 1fr));
- }
-}
diff --git a/apps/backend/scripts/migrate.ts b/apps/backend/scripts/migrate.ts
deleted file mode 100644
index bb8bdcf8..00000000
--- a/apps/backend/scripts/migrate.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { config } from "dotenv";
-import { drizzle } from "drizzle-orm/postgres-js";
-import { migrate } from "drizzle-orm/postgres-js/migrator";
-import process from "node:process";
-import postgres from "postgres";
-
-config();
-
-const isProd = process.env.NODE_ENV === "production";
-const connectionString = isProd ? process.env.PROD_DATABASE_URL : process.env.DATABASE_URL;
-
-if (!connectionString) {
- throw new Error(`${isProd ? "PROD_DATABASE_URL" : "DATABASE_URL"} is not set`);
-}
-
-console.log("Connecting to:", connectionString.replace(/:[^:@]+@/, ":****@")); // Log sanitized connection string
-
-const migrationClient = postgres(connectionString, { max: 1 });
-
-async function main() {
- console.log("Running migrations...");
-
- try {
- const db = drizzle(migrationClient);
- await migrate(db, { migrationsFolder: "./drizzle" });
- console.log("Migrations completed!");
- } catch (error) {
- console.error("Migration failed:", error);
- } finally {
- await migrationClient.end();
- }
-}
-
-main().catch((err) => {
- console.error("Unexpected error:", err);
- process.exit(1);
-});
diff --git a/apps/backend/src/auth.ts b/apps/backend/src/auth.ts
deleted file mode 100644
index 0ceeb0c1..00000000
--- a/apps/backend/src/auth.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-import { Context, Next } from "hono";
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-import { and, database, eq, sql } from "@supermemory/db";
-import { User, users } from "@supermemory/db/schema";
-import { Env, Variables } from "./types";
-import { encrypt, decrypt } from "./utils/cipher";
-
-interface EncryptedData {
- userId: string;
- lastApiKeyGeneratedAt: string;
-}
-
-export const getApiKey = async (
- userId: string,
- lastApiKeyGeneratedAt: string,
- c: Context<{ Variables: Variables; Bindings: Env }>
-) => {
- const data = `${userId}-${lastApiKeyGeneratedAt}`;
- return "sm_" + (await encrypt(data, c.env.WORKOS_COOKIE_PASSWORD));
-};
-
-export const decryptApiKey = async (
- encryptedKey: string,
- c: Context<{ Variables: Variables; Bindings: Env }>
-): Promise<EncryptedData> => {
- const ourKey = encryptedKey.slice(3);
- const decrypted = await decrypt(ourKey, c.env.WORKOS_COOKIE_PASSWORD);
- const [userId, lastApiKeyGeneratedAt] = decrypted.split("-");
-
- return {
- userId,
- lastApiKeyGeneratedAt,
- };
-};
-
-export const auth = async (
- c: Context<{ Variables: Variables; Bindings: Env }>,
- next: Next
-) => {
- // Handle CORS preflight requests
- if (c.req.method === "OPTIONS") {
- return next()
- }
-
- // Set cache control headers
- c.header("Cache-Control", "private, no-cache, no-store, must-revalidate");
- c.header("Pragma", "no-cache");
- c.header("Expires", "0");
-
- let user: User | User[] | undefined;
-
- // Check for API key authentication first
- const authHeader = c.req.raw.headers.get("Authorization");
- if (authHeader?.startsWith("Bearer ")) {
- const apiKey = authHeader.slice(7);
- try {
- const { userId, lastApiKeyGeneratedAt } = await decryptApiKey(apiKey, c);
-
- // Look up user with matching id and lastApiKeyGeneratedAt
- user = await database(c.env.HYPERDRIVE.connectionString)
- .select()
- .from(users)
- .where(
- and(
- eq(users.uuid, userId)
- )
- )
- .limit(1);
-
- if (user && Array.isArray(user)) {
- user = user[0];
- if (user && user.lastApiKeyGeneratedAt?.getTime() === Number(lastApiKeyGeneratedAt)) {
- c.set("user", user);
- } else {
- return c.json({ error: "Invalid API key - user not found" }, 401);
- }
- }
- } catch (err) {
- console.error("API key authentication failed:", err);
- return c.json({ error: "Invalid API key format" }, 401);
- }
- }
-
- // If no user found via API key, try cookie authentication
- if (!user) {
- const cookies = c.req.raw.headers.get("Cookie");
- if (cookies) {
- // Fake remix context object. this just works.
- const context = {
- cloudflare: {
- env: c.env,
- },
- };
-
- const session = await getSessionFromRequest(c.req.raw, context);
- console.log("Session", session);
- c.set("session", session);
-
- if (session?.user?.id) {
- user = await database(c.env.HYPERDRIVE.connectionString)
- .select()
- .from(users)
- .where(eq(users.uuid, session.user.id))
- .limit(1);
-
- if ((!user || user.length === 0) && session?.user?.id) {
- const newUser = await database(c.env.HYPERDRIVE.connectionString)
- .insert(users)
- .values({
- uuid: session.user?.id,
- email: session.user?.email,
- firstName: session.user?.firstName,
- lastName: session.user?.lastName,
- createdAt: new Date(),
- updatedAt: new Date(),
- emailVerified: false,
- profilePictureUrl: session.user?.profilePictureUrl ?? "",
- })
- .returning()
- .onConflictDoUpdate({
- target: [users.email],
- set: {
- uuid: session.user.id,
- },
- });
-
- user = newUser[0];
- }
-
- user = Array.isArray(user) ? user[0] : user;
- c.set("user", user);
- console.log("User", user);
- }
- }
- }
-
- // Check if request requires authentication
- const isPublicSpaceRequest =
- c.req.url.includes("/v1/spaces/") || c.req.url.includes("/v1/memories");
-
- if (!isPublicSpaceRequest && !c.get("user")) {
- console.log("Unauthorized access to", c.req.url);
- if (authHeader) {
- return c.json({ error: "Invalid authentication credentials" }, 401);
- } else {
- return c.json({ error: "Authentication required" }, 401);
- }
- }
-
- return next();
-};
diff --git a/apps/backend/src/components/landing.tsx b/apps/backend/src/components/landing.tsx
deleted file mode 100644
index 87bbdb69..00000000
--- a/apps/backend/src/components/landing.tsx
+++ /dev/null
@@ -1,234 +0,0 @@
-import { html } from "hono/html";
-
-export function LandingPage() {
- return (
- <html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <link href="/output.css" rel="stylesheet" />
- <title>Supermemory API</title>
- </head>
- <body>
- <div className="gradient-dark">
- <header className="bg-gray-900/50 dot-pattern">
- <nav className="container mx-auto px-6 py-4">
- <div className="flex items-center justify-between">
- <div className="text-2xl font-bold text-white">
- Supermemory API
- </div>
- <div className="hidden md:flex space-x-8">
- <a
- href="#features"
- className="text-gray-300 hover:text-white"
- >
- Features
- </a>
- <a
- href="https://docs.supermemory.ai/"
- target="_blank"
- className="text-gray-300 hover:text-white"
- rel="noreferrer"
- >
- Documentation
- </a>
- </div>
- <a
- href="https://docs.supermemory.ai/"
- target="_blank"
- className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
- rel="noreferrer"
- >
- Get Started
- </a>
- </div>
- </nav>
-
- <div className="container mx-auto px-6 py-16 text-center">
- <h1 className="text-4xl font-bold text-white mb-4">
- The Modern API for Knowledge Management
- </h1>
- <p className="text-xl text-gray-300 mb-8">
- Build powerful search and AI applications with our flexible,
- production-ready API
- </p>
- <div className="flex justify-center space-x-4">
- <a
- href="https://docs.supermemory.ai/"
- target="_blank"
- className="bg-blue-600 text-white px-8 py-3 rounded-lg hover:bg-blue-700"
- rel="noreferrer"
- >
- Get Started Free
- </a>
- <a
- href="https://docs.supermemory.ai/"
- target="_blank"
- className="border border-gray-600 text-gray-300 px-8 py-3 rounded-lg hover:bg-gray-700"
- rel="noreferrer"
- >
- View Docs
- </a>
- </div>
- </div>
- </header>
-
- <section id="features" className="py-20 bg-gray-900/50 dot-pattern">
- <div className="container mx-auto px-6">
- <h2 className="text-3xl font-bold text-center text-white mb-12">
- Key Features
- </h2>
- <div className="grid md:grid-cols-3 gap-8">
- <div className="p-6 border border-gray-600 rounded-lg bg-gray-900 hover:bg-gray-800 transition duration-300">
- <h3 className="text-xl font-semibold mb-4 text-white">
- Battle-Tested RAG Stack
- </h3>
- <p className="text-gray-300">
- Production-ready retrieval augmented generation architecture
- for reliable and scalable information retrieval.
- </p>
- </div>
- <div className="p-6 border border-gray-600 rounded-lg bg-gray-900 hover:bg-gray-800 transition duration-300">
- <h3 className="text-xl font-semibold mb-4 text-white">
- Flexible LLM Integration
- </h3>
- <p className="text-gray-300">
- Use any LLM of your choice or operate in search-only mode
- for maximum flexibility and control.
- </p>
- </div>
- <div className="p-6 border border-gray-600 rounded-lg bg-gray-900 hover:bg-gray-800 transition duration-300">
- <h3 className="text-xl font-semibold mb-4 text-white">
- Advanced Access Control
- </h3>
- <p className="text-gray-300">
- Comprehensive collection filtering and permission management
- for secure data access.
- </p>
- </div>
- <div className="p-6 border border-gray-600 rounded-lg bg-gray-900 hover:bg-gray-800 transition duration-300">
- <h3 className="text-xl font-semibold mb-4 text-white">
- Seamless Data Import
- </h3>
- <p className="text-gray-300">
- Magic link import and platform synchronization for
- effortless data integration.
- </p>
- </div>
- <div className="p-6 border border-gray-600 rounded-lg bg-gray-900 hover:bg-gray-800 transition duration-300">
- <h3 className="text-xl font-semibold mb-4 text-white">
- Real-time Monitoring
- </h3>
- <p className="text-gray-300">
- Track and analyze memory usage patterns in real-time with
- detailed metrics.
- </p>
- </div>
- <div className="p-6 border border-gray-600 rounded-lg bg-gray-900 hover:bg-gray-800 transition duration-300">
- <h3 className="text-xl font-semibold mb-4 text-white">
- Easy Integration
- </h3>
- <p className="text-gray-300">
- Simple API endpoints that integrate seamlessly with your
- existing infrastructure.
- </p>
- </div>
- </div>
- </div>
- </section>
-
- <footer className="bg-black/50 dot-pattern">
- <div className="container mx-auto px-6">
- <div className="grid md:grid-cols-4 gap-8">
- <div>
- <h4 className="text-lg font-semibold mb-4">
- Supermemory API
- </h4>
- <p className="text-gray-400">
- Making memory management simple and efficient for developers
- worldwide.
- </p>
- </div>
- <div>
- <h4 className="text-lg font-semibold mb-4">Product</h4>
- <ul className="space-y-2 text-gray-400">
- <li>
- <a href="#features" className="hover:text-white">
- Features
- </a>
- </li>
- <li>
- <a
- href="https://docs.supermemory.ai/"
- target="_blank"
- className="hover:text-white"
- rel="noreferrer"
- >
- Documentation
- </a>
- </li>
- </ul>
- </div>
- <div>
- <h4 className="text-lg font-semibold mb-4">Connect</h4>
- <ul className="space-y-2 text-gray-400">
- <li>
- <a
- href="https://x.com/supermemoryai"
- target="_blank"
- className="hover:text-white"
- rel="noreferrer"
- >
- X (formerly Twitter)
- </a>
- </li>
- <li>
- <a
- href="https://github.com/supermemoryai"
- target="_blank"
- className="hover:text-white"
- rel="noreferrer"
- >
- GitHub
- </a>
- </li>
- <li>
- <a
- href="https://discord.gg/b3BgKWpbtR"
- target="_blank"
- className="hover:text-white"
- rel="noreferrer"
- >
- Discord
- </a>
- </li>
- </ul>
- </div>
- </div>
- <div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
- <p>&copy; 2024 Supermemory API. All rights reserved.</p>
- </div>
- </div>
- </footer>
-
- <style
- dangerouslySetInnerHTML={{
- __html: `
- .dot-pattern {
- background-image: radial-gradient(
- rgba(255, 255, 255, 0.1) 1px,
- transparent 1px
- );
- background-size: 24px 24px;
- }
- .gradient-dark {
- background: linear-gradient(to bottom right, rgb(17 24 39), rgb(0 0 0));
- }
- `,
- }}
- />
- </div>
- </body>
- </html>
- );
-}
diff --git a/apps/backend/src/errors/baseError.ts b/apps/backend/src/errors/baseError.ts
deleted file mode 100644
index bccc54df..00000000
--- a/apps/backend/src/errors/baseError.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-export class BaseHttpError extends Error {
- public status: number;
- public message: string;
-
- constructor(status: number, message: string) {
- super(message);
- this.status = status;
- this.message = message;
- Object.setPrototypeOf(this, new.target.prototype); // Restore prototype chain
- }
- }
-
-
- export class BaseError extends Error {
- type: string;
- message: string;
- source: string;
- ignoreLog: boolean;
-
- constructor(
- type: string,
- message?: string,
- source?: string,
- ignoreLog = false
- ) {
- super();
-
- Object.setPrototypeOf(this, new.target.prototype);
-
- this.type = type;
- this.message =
- message ??
- "An unknown error occurred. If this persists, please contact us.";
- this.source = source ?? "unspecified";
- this.ignoreLog = ignoreLog;
- }
-
- toJSON(): Record<PropertyKey, string> {
- return {
- type: this.type,
- message: this.message,
- source: this.source,
- };
- }
- } \ No newline at end of file
diff --git a/apps/backend/src/errors/results.ts b/apps/backend/src/errors/results.ts
deleted file mode 100644
index c5ab115b..00000000
--- a/apps/backend/src/errors/results.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { BaseError } from "./baseError";
-
-export type Result<T, E extends Error> =
- | { ok: true; value: T }
- | { ok: false; error: E };
-
-export const Ok = <T>(data: T): Result<T, never> => {
- return { ok: true, value: data };
-};
-
-export const Err = <E extends BaseError>(error: E): Result<never, E> => {
- return { ok: false, error };
-};
-
-export async function wrap<T, E extends BaseError>(
- p: Promise<T>,
- errorFactory: (err: Error, source: string) => E,
- source: string = "unspecified"
- ): Promise<Result<T, E>> {
- try {
- return Ok(await p);
- } catch (e) {
- return Err(errorFactory(e as Error, source));
- }
- }
-
-export function isErr<T, E extends Error>(
- result: Result<T, E>,
-): result is { ok: false; error: E } {
- return !result.ok;
-} \ No newline at end of file
diff --git a/apps/backend/src/globals.css b/apps/backend/src/globals.css
deleted file mode 100644
index b5c61c95..00000000
--- a/apps/backend/src/globals.css
+++ /dev/null
@@ -1,3 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
diff --git a/apps/backend/src/index.tsx b/apps/backend/src/index.tsx
deleted file mode 100644
index 4ccbb4c0..00000000
--- a/apps/backend/src/index.tsx
+++ /dev/null
@@ -1,255 +0,0 @@
-import { z } from "zod";
-import { type Context, Hono } from "hono";
-import { auth } from "./auth";
-import { logger } from "hono/logger";
-import { timing } from "hono/timing";
-import type { Env, Variables } from "./types";
-import { zValidator } from "@hono/zod-validator";
-import { database } from "@supermemory/db";
-import { waitlist } from "@supermemory/db/schema";
-import { cors } from "hono/cors";
-import { ContentWorkflow } from "./workflow";
-import { Resend } from "resend";
-import { LandingPage } from "./components/landing";
-import user from "./routes/user";
-import spacesRoute from "./routes/spaces";
-import actions from "./routes/actions";
-import memories from "./routes/memories";
-import integrations from "./routes/integrations";
-import { fromHono } from "chanfana";
-import {
- DurableObjectRateLimiter,
- DurableObjectStore,
-} from "@hono-rate-limiter/cloudflare";
-import type { ConfigType, GeneralConfigType, rateLimiter } from "hono-rate-limiter";
-
-// Create base Hono app first
-const honoApp = new Hono<{ Variables: Variables; Bindings: Env }>();
-
-const app = fromHono(honoApp);
-
-// Add all middleware and routes
-app.use("*", timing());
-app.use("*", logger());
-app.use(
- "*",
- cors({
- origin: [
- "http://localhost:3000",
- "https://supermemory.ai",
- "https://*.supermemory.ai",
- "https://*.supermemory.com",
- "https://supermemory.com",
- "chrome-extension://*",
- ],
- allowHeaders: ["*"],
- allowMethods: ["*"],
- credentials: true,
- exposeHeaders: ["*"],
- })
-);
-
-app.use("/v1/*", auth);
-app.use("/v1/*", (c, next) => {
- const user = c.get("user");
-
- if (c.env.NODE_ENV === "development") {
- return next();
- }
-
- // RATELIMITS
- const rateLimitConfig = {
- // Endpoints that bypass rate limiting
- excludedPaths: [
- "/v1/add",
- "/v1/chat",
- "/v1/suggested-learnings",
- "/v1/recommended-questions",
- ] as (string | RegExp)[],
-
- // Custom rate limits for specific endpoints
- customLimits: {
- notionImport: {
- paths: ["/v1/integrations/notion/import", "/v1/integrations/notion"],
- windowMs: 10 * 60 * 1000, // 10 minutes
- limit: 5, // 5 requests per 10 minutes
- },
- inviteSpace: {
- paths: [/^\v1\/spaces\/[^/]+\/invite$/],
- windowMs: 60 * 1000, // 1 minute
- limit: 5, // 5 requests per minute
- },
- } as Record<
- string,
- { paths: (string | RegExp)[]; windowMs: number; limit: number }
- >,
-
- default: {
- windowMs: 60 * 1000, // 1 minute
- limit: 100, // 100 requests per minute
- },
-
- common: {
- standardHeaders: "draft-6",
- keyGenerator: (c: Context) =>
- `${user?.uuid ?? c.req.header("cf-connecting-ip")}-${new Date().getDate()}`, // day so that limit gets reset every day
- store: new DurableObjectStore({ namespace: c.env.RATE_LIMITER }),
- } as GeneralConfigType<ConfigType>,
- };
-
- if (
- c.req.path &&
- rateLimitConfig.excludedPaths.some((path) =>
- typeof path === "string" ? c.req.path === path : path.test(c.req.path)
- )
- ) {
- return next();
- }
-
- // Check for custom rate limits
- for (const [_, config] of Object.entries(rateLimitConfig.customLimits)) {
- if (
- config.paths.some((path) =>
- typeof path === "string" ? c.req.path === path : path.test(c.req.path)
- )
- ) {
- return rateLimiter({
- windowMs: config.windowMs,
- limit: config.limit,
- ...rateLimitConfig.common,
- })(c as any, next);
- }
- }
-
- // Apply default rate limit
- return rateLimiter({
- windowMs: rateLimitConfig.default.windowMs,
- limit: rateLimitConfig.default.limit,
- ...rateLimitConfig.common,
- })(c as any, next);
-});
-
-app.get("/", (c) => {
- return c.html(<LandingPage />);
-});
-
-// TEMPORARY REDIRECT
-app.all("/api/*", async (c) => {
- // Get the full URL and path
- const url = new URL(c.req.url);
- const path = url.pathname;
- const newPath = path.replace("/api", "/v1");
-
- // Preserve query parameters and build target URL
- const redirectUrl = `https://api.supermemory.ai${newPath}${url.search}`;
-
- // Use c.redirect() for a proper redirect
- return c.redirect(redirectUrl);
-});
-
-app.route("/v1/user", user);
-app.route("/v1/spaces", spacesRoute);
-app.route("/v1", actions);
-app.route("/v1/integrations", integrations);
-app.route("/v1/memories", memories);
-
-app.get("/v1/session", (c) => {
- const user = c.get("user");
-
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- return c.json({
- user,
- });
-});
-
-app.post(
- "/waitlist",
- zValidator(
- "json",
- z.object({ email: z.string().email(), token: z.string() })
- ),
- async (c) => {
- const { email, token } = c.req.valid("json");
-
- const address = c.req.raw.headers.get("CF-Connecting-IP");
-
- const idempotencyKey = crypto.randomUUID();
- const url = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
- const firstResult = await fetch(url, {
- body: JSON.stringify({
- secret: c.env.TURNSTILE_SECRET_KEY,
- response: token,
- remoteip: address,
- idempotency_key: idempotencyKey,
- }),
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- });
-
- const firstOutcome = (await firstResult.json()) as { success: boolean };
-
- if (!firstOutcome.success) {
- console.info("Turnstile verification failed", firstOutcome);
- return c.json(
- { error: "Turnstile verification failed" },
- 439 as StatusCode
- );
- }
-
- const resend = new Resend(c.env.RESEND_API_KEY);
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- const ip =
- c.req.header("cf-connecting-ip") ||
- `${c.req.raw.cf?.asn}-${c.req.raw.cf?.country}-${c.req.raw.cf?.city}-${c.req.raw.cf?.region}-${c.req.raw.cf?.postalCode}`;
-
- const { success } = await c.env.EMAIL_LIMITER.limit({ key: ip });
-
- if (!success) {
- return c.json({ error: "Rate limit exceeded" }, 429);
- }
-
- const message = `Supermemory started as a side project a few months ago when I built it as a hackathon project.
- <br></br>
- you guys loved it too much. like wayy too much. it was embarrassing, because this was not it - it was nothing but a hackathon project.
- <br></br>
- I launched on github too. <a href="https://git.new/memory">https://github.com/supermemoryai/supermemory</a>, and we were somehow one of the fastest growing open source repositories in Q3 2024.
- <br></br><br></br>
- So, it's time to make this good. My vision is to make supermemory the best memory tool on the internet.
- `;
-
- try {
- await db.insert(waitlist).values({ email });
- await resend.emails.send({
- from: "Dhravya From Supermemory <[email protected]>",
- to: email,
- subject: "You're in the waitlist - A personal note from Dhravya",
- html: `<p>Hi. I'm Dhravya. I'm building Supermemory to help people remember everything.<br></br> ${message} <br></br><br></br>I'll be in touch when we launch! Till then, just reply to this email if you wanna talk :)<br></br>If you want to follow me on X, here's my handle: <a href='https://x.com/dhravyashah'>@dhravyashah</a><br></br><br></br>- Dhravya</p>`,
- });
- } catch (e) {
- console.error(e);
- return c.json({ error: "Failed to add to waitlist" }, 400);
- }
-
- return c.json({ success: true });
- }
-);
-
-app.onError((err, c) => {
- console.error(err);
- return c.json({ error: "Internal server error" }, 500);
-});
-
-export default {
- fetch: app.fetch,
-};
-
-export { ContentWorkflow, DurableObjectRateLimiter };
-
-export type AppType = typeof app;
diff --git a/apps/backend/src/providers.ts b/apps/backend/src/providers.ts
deleted file mode 100644
index ed9644a3..00000000
--- a/apps/backend/src/providers.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { createOpenAI, OpenAIProvider } from "@ai-sdk/openai";
-import { createGoogleGenerativeAI } from "@ai-sdk/google";
-import { Env } from "./types";
-
-export function openai(
- env: Env,
- apiKey?: string
-): ReturnType<typeof createOpenAI> {
- return createOpenAI({
- apiKey: apiKey || env.OPEN_AI_API_KEY,
- baseURL: "https://gateway.ai.cloudflare.com/v1/47c2b4d598af9d423c06fc9f936226d5/supermemory/openai"
- });
-}
-
-export function google(securityKey: string) {
- return createGoogleGenerativeAI({
- apiKey: securityKey,
- });
-}
diff --git a/apps/backend/src/routes/actions.ts b/apps/backend/src/routes/actions.ts
deleted file mode 100644
index 581e7787..00000000
--- a/apps/backend/src/routes/actions.ts
+++ /dev/null
@@ -1,1171 +0,0 @@
-import { Hono } from "hono";
-import { Variables, Env, recommendedQuestionsSchema } from "../types";
-import { zValidator } from "@hono/zod-validator";
-import { z } from "zod";
-import {
- AISDKError,
- convertToCoreMessages,
- embed,
- generateObject,
- InvalidPromptError,
- Message,
- smoothStream,
- StreamData,
- streamText,
- TextPart,
-} from "ai";
-import {
- chatThreads,
- documents,
- chunk,
- spaces as spaceInDb,
- spaceAccess,
- type Space,
- contentToSpace,
-} from "@supermemory/db/schema";
-import { google, openai } from "../providers";
-import { randomId } from "@supermemory/shared";
-import {
- and,
- cosineDistance,
- database,
- desc,
- eq,
- exists,
- inArray,
- or,
- sql,
-} from "@supermemory/db";
-import { typeDecider } from "../utils/typeDecider";
-import { isErr, Ok } from "../errors/results";
-import { fromHono } from "chanfana";
-
-const actions = fromHono(new Hono<{ Variables: Variables; Bindings: Env }>())
- .post(
- "/chat",
- zValidator(
- "json",
- z.object({
- messages: z.array(z.any()).min(1, "At least one message is required"),
- threadId: z.string().optional(),
- })
- ),
- async (c) => {
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const { messages, threadId } = await c.req.valid("json");
-
- const unfilteredCoreMessages = convertToCoreMessages(
- (messages as Message[])
- .filter((m) => m.content.length > 0)
- .map((m) => ({
- ...m,
- content:
- m.content +
- (m.annotations
- ? `<context>${JSON.stringify(m.annotations)}</context>`
- : ""),
- experimental_attachments:
- m.experimental_attachments?.length &&
- m.experimental_attachments?.length > 0
- ? m.experimental_attachments
- : (m.data as { files: [] })?.files,
- }))
- );
-
- const coreMessages = unfilteredCoreMessages.filter(
- (message) => message.content.length > 0
- );
-
- const db = database(c.env.HYPERDRIVE.connectionString);
- const { initLogger, wrapAISDKModel } = await import("braintrust");
-
- // Initialize clients and loggers
- const logger = initLogger({
- projectName: "supermemory",
- apiKey: c.env.BRAINTRUST_API_KEY,
- });
-
- const googleClient = wrapAISDKModel(
- openai(c.env).chat("gpt-4o-mini-2024-07-18")
- );
-
- // Get last user message and generate embedding in parallel with thread creation
- let lastUserMessage = coreMessages.findLast((i) => i.role === "user");
- const queryText =
- typeof lastUserMessage?.content === "string"
- ? lastUserMessage.content
- : lastUserMessage?.content.map((c) => (c as TextPart).text).join("");
-
- if (!queryText || queryText.length === 0) {
- return c.json({ error: "Empty query" }, 400);
- }
-
- // Run embedding generation and thread creation in parallel
- const [{ data: embedding }, thread] = await Promise.all([
- c.env.AI.run("@cf/baai/bge-base-en-v1.5", { text: queryText }),
- !threadId
- ? db
- .insert(chatThreads)
- .values({
- firstMessage: messages[0].content,
- userId: user.id,
- uuid: randomId(),
- messages: coreMessages,
- })
- .returning()
- : null,
- ]);
-
- const threadUuid = threadId || thread?.[0].uuid;
-
- if (!embedding) {
- return c.json({ error: "Failed to generate embedding" }, 500);
- }
-
- try {
- const data = new StreamData();
-
- // Pre-compute the vector similarity expression
- const vectorSimilarity = sql<number>`1 - (embeddings <=> ${JSON.stringify(embedding[0])}::vector)`;
-
- // Get matching chunks with document info
- const matchingChunks = await db
- .select({
- chunkId: chunk.id,
- documentId: chunk.documentId,
- textContent: chunk.textContent,
- orderInDocument: chunk.orderInDocument,
- metadata: chunk.metadata,
- similarity: vectorSimilarity,
- // Document fields
- docId: documents.id,
- docUuid: documents.uuid,
- docContent: documents.content,
- docType: documents.type,
- docUrl: documents.url,
- docTitle: documents.title,
- docDescription: documents.description,
- docOgImage: documents.ogImage,
- })
- .from(chunk)
- .innerJoin(documents, eq(chunk.documentId, documents.id))
- .where(
- and(eq(documents.userId, user.id), sql`${vectorSimilarity} > 0.3`)
- )
- .orderBy(desc(vectorSimilarity))
- .limit(25);
-
- // Get unique document IDs from matching chunks
- const uniqueDocIds = [
- ...new Set(matchingChunks.map((c) => c.documentId)),
- ];
-
- // Fetch all chunks for these documents to get context
- const contextChunks = await db
- .select({
- id: chunk.id,
- documentId: chunk.documentId,
- textContent: chunk.textContent,
- orderInDocument: chunk.orderInDocument,
- metadata: chunk.metadata,
- })
- .from(chunk)
- .where(inArray(chunk.documentId, uniqueDocIds))
- .orderBy(chunk.documentId, chunk.orderInDocument);
-
- // Group chunks by document
- const chunksByDocument = new Map<number, typeof contextChunks>();
- for (const chunk of contextChunks) {
- const docChunks = chunksByDocument.get(chunk.documentId) || [];
- docChunks.push(chunk);
- chunksByDocument.set(chunk.documentId, docChunks);
- }
-
- // Create context with surrounding chunks
- const contextualResults = matchingChunks.map((match) => {
- const docChunks = chunksByDocument.get(match.documentId) || [];
- const matchIndex = docChunks.findIndex((c) => c.id === match.chunkId);
-
- // Get surrounding chunks (2 before and 2 after for more context)
- const start = Math.max(0, matchIndex - 2);
- const end = Math.min(docChunks.length, matchIndex + 3);
- const relevantChunks = docChunks.slice(start, end);
-
- return {
- id: match.docId,
- title: match.docTitle,
- description: match.docDescription,
- url: match.docUrl,
- type: match.docType,
- content: relevantChunks.map((c) => c.textContent).join("\n"),
- similarity: Number(match.similarity.toFixed(4)),
- chunks: relevantChunks.map((c) => ({
- id: c.id,
- content: c.textContent,
- orderInDocument: c.orderInDocument,
- metadata: c.metadata,
- isMatch: c.id === match.chunkId,
- })),
- };
- });
-
- // Sort by similarity and take top results
- const topResults = contextualResults
- .sort((a, b) => b.similarity - a.similarity)
- .slice(0, 10);
-
- data.appendMessageAnnotation(topResults);
-
- if (lastUserMessage) {
- lastUserMessage.content =
- typeof lastUserMessage.content === "string"
- ? lastUserMessage.content +
- `<context>${JSON.stringify(topResults)}</context>`
- : [
- ...lastUserMessage.content,
- {
- type: "text",
- text: `<context>${JSON.stringify(topResults)}</context>`,
- },
- ];
- coreMessages[coreMessages.length - 1] = lastUserMessage;
- }
-
- const result = await streamText({
- model: googleClient,
- experimental_providerMetadata: {
- metadata: { userId: user.id, chatThreadId: threadUuid ?? "" },
- },
- experimental_transform: smoothStream(),
- messages: [
- {
- role: "system",
- content: `You are a knowledgeable and helpful AI assistant for Supermemory, a personal knowledge management app. Your goal is to help users explore and understand their saved content.
-
- Key guidelines:
- - Maintain natural, engaging conversation while seamlessly incorporating relevant information from the user's knowledge base
- - Build on previous messages in the conversation to provide coherent, contextual responses
- - Be concise but thorough, focusing on the most relevant details
- - When appropriate, make connections between different pieces of information
- - If you're not sure about something, be honest and say so
- - Feel free to ask clarifying questions if needed
- - Make it easy to read for the user!
- - Use markdown to format your responses but dont make your answers TOO long include any and all information related to context in the response if possible.
- - only talk about the context if the right answer is in the context.
- - You are Supermemory - a personal knowledge management app.
- - You are built by Dhravya Shah (https://dhravya.dev). And the supermemory team (https://supermemory.ai).
-
- The user's saved content is provided in <context> tags. Use this information naturally without explicitly referencing it.`,
- },
- ...coreMessages,
- ],
- async onFinish(completion) {
- try {
- if (lastUserMessage) {
- lastUserMessage.content =
- typeof lastUserMessage.content === "string"
- ? lastUserMessage.content.replace(
- /<context>[\s\S]*?<\/context>/g,
- ""
- )
- : lastUserMessage.content.filter(
- (part) =>
- !(
- part.type === "text" &&
- part.text.startsWith("<context>")
- )
- );
- coreMessages[coreMessages.length - 1] = lastUserMessage;
- }
-
- const newMessages = [
- ...coreMessages,
- {
- role: "assistant",
- content:
- completion.text +
- `<context>[${JSON.stringify(topResults)}]</context>`,
- },
- ];
-
- if (threadUuid) {
- await db
- .update(chatThreads)
- .set({ messages: newMessages })
- .where(eq(chatThreads.uuid, threadUuid));
- }
- } catch (error) {
- console.error("Failed to update thread:", error);
- } finally {
- await data.close();
- }
- },
- });
-
- return result.toDataStreamResponse({
- headers: {
- "Supermemory-Thread-Uuid": threadUuid ?? "",
- "Content-Type": "text/x-unknown",
- "content-encoding": "identity",
- "transfer-encoding": "chunked",
- },
- data,
- });
- } catch (error) {
- console.error("Chat error:", error);
-
- if (error instanceof InvalidPromptError) {
- return c.json(
- { error: "Invalid prompt - please rephrase your message" },
- 400
- );
- }
-
- if ((error as AISDKError).cause === "ECONNREFUSED") {
- return c.json({ error: "Database connection failed" }, 503);
- }
-
- return c.json(
- {
- error: "An unexpected error occurred",
- details:
- c.env.NODE_ENV === "development"
- ? (error as Error).message
- : undefined,
- },
- 500
- );
- }
- }
- )
- .get(
- "/chat/:threadUuid",
- zValidator(
- "param",
- z.object({
- threadUuid: z.string(),
- })
- ),
- async (c) => {
- const user = c.get("user");
- const threadUuid = c.req.valid("param").threadUuid;
-
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const chatHistory = await database(c.env.HYPERDRIVE.connectionString)
- .select()
- .from(chatThreads)
- .where(
- and(eq(chatThreads.userId, user.id), eq(chatThreads.uuid, threadUuid))
- );
-
- if (!chatHistory) {
- return c.json({ error: "Chat history not found" }, 404);
- }
-
- return c.json({ chatHistory: chatHistory[0].messages });
- }
- )
- .get("/recommended-questions", async (c) => {
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
- const recentDocuments = await db
- .select()
- .from(documents)
- .where(eq(documents.userId, user.id))
- .orderBy(sql`RANDOM()`)
- .limit(3);
-
- if (recentDocuments.length === 0) {
- return c.json({ questions: [] });
- }
-
- const cachedQuestions = await c.env.MD_CACHE.get(`rq:${user.id}`);
- if (cachedQuestions) {
- const randomQuestions = JSON.parse(cachedQuestions)
- .questions.sort(() => Math.random() - 0.5)
- .slice(0, 3);
- return c.json({ questions: randomQuestions });
- }
-
- const { initLogger, wrapAISDKModel } = await import("braintrust");
-
- const logger = initLogger({
- projectName: "supermemory",
- apiKey: c.env.BRAINTRUST_API_KEY,
- });
-
- const model = wrapAISDKModel(openai(c.env).chat("gpt-4o-mini-2024-07-18"));
-
- const aiResponse = await generateObject({
- schema: z.object({
- questions: recommendedQuestionsSchema,
- }),
- model,
- prompt: `You are helping generate search suggestions for a user's personal knowledge base.
-
- Generate 10 specific, focused questions based on the following documents. The questions should:
- - Be highly specific and reference concrete details from the documents
- - Focus on key insights, important facts, or interesting relationships
- - Be phrased naturally, as if the user is trying to recall something they learned
- - Be 2-8 words long
- - Not include generic questions that could apply to any document
-
- Documents:
- ${recentDocuments.map((d) => d.content).join("\n\n")}`,
- });
-
- await c.env.MD_CACHE.put(
- `rq:${user.id}`,
- JSON.stringify(aiResponse.object),
- {
- // 3 hours
- expirationTtl: 10800,
- }
- );
-
- const questions = aiResponse.object.questions;
- const randomQuestions = questions
- .sort(() => Math.random() - 0.5)
- .slice(0, 3);
- return c.json({ questions: randomQuestions });
- })
- .get("/suggested-learnings", async (c) => {
- const user = await c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- // Try to get from cache first
- const cacheKey = `sl:${user.id}`;
- const cached = await c.env.MD_CACHE.get(cacheKey);
- if (cached) {
- return c.json({
- suggestedLearnings: JSON.parse(cached) as { [x: string]: string },
- });
- }
-
- // Get random sample of user's documents that are well-distributed
- const recentLearnings = await db
- .select()
- .from(documents)
- .where(eq(documents.userId, user.id))
- // Use random() to get distributed sample
- .orderBy(sql`RANDOM()`)
- .limit(7);
-
- if (recentLearnings.length === 0 || recentLearnings.length < 3) {
- return c.json({ suggestedLearnings: [] });
- }
-
- // for each document, i want to generate a list of
- // small markdown tweet-like things that the user might want to remember about
- const suggestedLearnings = await Promise.all(
- recentLearnings.map(async (document) => {
- const model = openai(c.env).chat("gpt-4o-mini-2024-07-18");
- const prompt = `Generate a concise topic recall card for this document. The card should:
- - Have a clear title that captures the main topic
- - based on when the document was saved, include a brief "Last (week/month/...), you saved notes on..." intro (do something different every time.)
- - List 2-3 key points from the content in simple bullet points
- - Keep the total length under 280 characters
- - Focus on the core concepts worth remembering
- - Be in markdown format
- - if you don't have a good suggestions, just skip that document.
-
- Here's the document content: ${document.content}, Document saved at: ${document.updatedAt}, Today's date: ${new Date().toLocaleDateString()}`;
- const response = await generateObject({
- schema: z.object({
- [document.uuid]: z.string(),
- }),
- // @ts-ignore
- model,
- prompt,
- });
- return response.object;
- })
- );
-
- // Cache the results
- await c.env.MD_CACHE.put(cacheKey, JSON.stringify(suggestedLearnings), {
- expirationTtl: 60 * 60 * 3, // 3 hours
- });
-
- return c.json({ suggestedLearnings });
- })
- .post(
- "/search",
- zValidator(
- "json",
- z.object({
- query: z.string().min(1, "Search query cannot be empty"),
- limit: z.number().min(1).max(50).default(10),
- threshold: z.number().min(0).max(1).default(0),
- spaces: z.array(z.string()).optional(),
- })
- ),
- async (c) => {
- const { query, limit, threshold, spaces } = c.req.valid("json");
- const user = c.get("user");
-
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- if (spaces && spaces.length > 0) {
- const spaceDetails = await Promise.all(
- spaces.map(async (spaceId) => {
- const space = await db
- .select()
- .from(spaceInDb)
- .where(eq(spaceInDb.uuid, spaceId))
- .limit(1);
-
- if (space.length === 0) return null;
- return {
- id: space[0].id,
- ownerId: space[0].ownerId,
- uuid: space[0].uuid,
- };
- })
- );
-
- // Filter out any null values and check permissions
- const validSpaces = spaceDetails.filter(
- (s): s is NonNullable<typeof s> => s !== null
- );
- const unauthorized = validSpaces.filter((s) => s.ownerId !== user.id);
-
- if (unauthorized.length > 0) {
- return c.json(
- {
- error: "Space permission denied",
- details: unauthorized.map((s) => s.uuid).join(", "),
- },
- 403
- );
- }
-
- // Replace UUIDs with IDs for the database query
- spaces.splice(
- 0,
- spaces.length,
- ...validSpaces.map((s) => s.id.toString())
- );
- }
-
- try {
- // Generate embedding for the search query
- const embeddings = await c.env.AI.run("@cf/baai/bge-base-en-v1.5", {
- text: query,
- });
-
- if (!embeddings.data) {
- return c.json(
- { error: "Failed to generate embedding for query" },
- 500
- );
- }
-
- // Pre-compute the vector similarity expression
- const vectorSimilarity = sql<number>`1 - (embeddings <=> ${JSON.stringify(embeddings.data[0])}::vector)`;
-
- // Get matching chunks
- const results = await db
- .select({
- chunkId: chunk.id,
- documentId: chunk.documentId,
- textContent: chunk.textContent,
- orderInDocument: chunk.orderInDocument,
- metadata: chunk.metadata,
- similarity: vectorSimilarity,
- // Document fields
- docUuid: documents.uuid,
- docContent: documents.content,
- docType: documents.type,
- docUrl: documents.url,
- docTitle: documents.title,
- docCreatedAt: documents.createdAt,
- docUpdatedAt: documents.updatedAt,
- docUserId: documents.userId,
- docDescription: documents.description,
- docOgImage: documents.ogImage,
- })
- .from(chunk)
- .innerJoin(documents, eq(chunk.documentId, documents.id))
- .where(
- and(
- eq(documents.userId, user.id),
- sql`${vectorSimilarity} > ${threshold}`,
- ...(spaces && spaces.length > 0
- ? [
- exists(
- db
- .select()
- .from(contentToSpace)
- .where(
- and(
- eq(contentToSpace.contentId, documents.id),
- inArray(
- contentToSpace.spaceId,
- spaces.map((spaceId) => Number(spaceId))
- )
- )
- )
- ),
- ]
- : [])
- )
- )
- .orderBy(desc(vectorSimilarity))
- .limit(limit);
-
- // Group results by document and take the best matching chunk
- const documentResults = new Map<number, (typeof results)[0]>();
- for (const result of results) {
- const existingResult = documentResults.get(result.documentId);
- if (
- !existingResult ||
- result.similarity > existingResult.similarity
- ) {
- documentResults.set(result.documentId, result);
- }
- }
-
- // Convert back to array and format response
- const finalResults = Array.from(documentResults.values())
- .sort((a, b) => b.similarity - a.similarity)
- .map((r) => ({
- id: r.documentId,
- uuid: r.docUuid,
- content: r.docContent,
- type: r.docType,
- url: r.docUrl,
- title: r.docTitle,
- createdAt: r.docCreatedAt,
- updatedAt: r.docUpdatedAt,
- userId: r.docUserId,
- description: r.docDescription,
- ogImage: r.docOgImage,
- similarity: Number(r.similarity.toFixed(4)),
- matchingChunk: {
- id: r.chunkId,
- content: r.textContent,
- orderInDocument: r.orderInDocument,
- metadata: r.metadata,
- },
- }));
-
- return c.json({ results: finalResults });
- } catch (error) {
- console.error("[Search Error]", error);
- return c.json(
- {
- error: "Search failed",
- details:
- c.env.NODE_ENV === "development"
- ? (error as Error).message
- : undefined,
- },
- 500
- );
- }
- }
- )
- .post(
- "/add",
- zValidator(
- "json",
- z.object({
- content: z.string().min(1, "Content cannot be empty"),
- spaces: z.array(z.string()).max(5).optional(),
- id: z.string().optional(),
- // any type of metadata. must be json serializable.
- metadata: z.any().optional(),
- images: z.array(z.string()).optional(),
- prefetched: z
- .object({
- contentToVectorize: z.string(),
- contentToSave: z.string(),
- title: z.string(),
- type: z.string(),
- description: z.string().optional(),
- ogImage: z.string().optional(),
- })
- .optional(),
- })
- ),
- async (c) => {
- const body = c.req.valid("json");
-
- const user = c.get("user");
-
- if (!user) {
- return c.json({ error: "You must be logged in to add content" }, 401);
- }
-
- // Do not perform seperate check for prefetched type, breaks tweet addition from extension
- const type = typeDecider(body.content);
-
- if (isErr(type)) {
- return c.json(
- {
- error: "Could not determine content type",
- details: type.error.message,
- },
- 400
- );
- }
-
- if (type.value === "page" && !body.content.startsWith("http")) {
- body.content = `https://${body.content}`;
- }
-
- const uuid = body.id ?? randomId();
- const contentId = `add-${user.id}-${uuid}`;
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- // Calculate document hash early to enable faster duplicate detection
- const content = body.prefetched?.contentToVectorize || body.content;
- const encoder = new TextEncoder();
- const data = encoder.encode(content);
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
- const hashArray = Array.from(new Uint8Array(hashBuffer));
- const documentHash = hashArray
- .map((b) => b.toString(16).padStart(2, "0"))
- .join("");
-
- // Check for duplicates using hash
- const existingDocs = await db
- .select()
- .from(documents)
- .where(
- and(
- eq(documents.userId, user.id),
- or(
- eq(documents.contentHash, documentHash),
- and(
- eq(documents.type, type.value),
- or(eq(documents.url, body.content), eq(documents.raw, content))
- )
- )
- )
- );
-
- if (existingDocs.length > 0) {
- return c.json(
- { error: `That ${type.value} already exists in your memories` },
- 409
- );
- }
-
- // Check space permissions if spaces are specified
- if (body.spaces && body.spaces.length > 0) {
- const spacePermissions = await Promise.all(
- body.spaces.map(async (spaceId) => {
- const space = await db
- .select()
- .from(spaceInDb)
- .where(eq(spaceInDb.uuid, spaceId))
- .limit(1);
-
- if (!space[0]) {
- // create a new space for the user with the given id
- const newSpace = await db
- .insert(spaceInDb)
- .values({
- uuid: spaceId,
- name: spaceId,
- isPublic: false,
- ownerId: user.id,
- createdAt: new Date(),
- updatedAt: new Date(),
- })
- .returning();
-
- return {
- spaceId: newSpace[0].id,
- allowed: true,
- error: null,
- };
- }
-
- const spaceData = space[0] as Space;
-
- // If public space, only owner can add content
- if (spaceData.isPublic) {
- return {
- spaceId,
- allowed: spaceData.ownerId === user.id,
- error:
- spaceData.ownerId !== user.id
- ? "Only space owner can add to public spaces"
- : null,
- };
- }
-
- // For private spaces, check if user is owner or in allowlist
- const spaceAccessCheck = await db
- .select()
- .from(spaceAccess)
- .where(
- and(
- eq(spaceAccess.spaceId, spaceData.id),
- eq(spaceAccess.userEmail, user.email),
- eq(spaceAccess.status, "accepted")
- )
- )
- .limit(1);
-
- return {
- spaceId,
- allowed:
- spaceData.ownerId === user.id || spaceAccessCheck.length > 0,
- error:
- spaceData.ownerId !== user.id && !spaceAccessCheck.length
- ? "Not authorized to add to this space"
- : null,
- };
- })
- );
-
- const unauthorized = spacePermissions.filter((p) => !p.allowed);
- if (unauthorized.length > 0) {
- return c.json(
- {
- error: "Space permission denied",
- details: unauthorized
- .map((u) => `${u.spaceId}: ${u.error}`)
- .join(", "),
- },
- 403
- );
- }
- }
-
- const isExternalContent = [
- "page",
- "tweet",
- "document",
- "notion",
- ].includes(type.value);
- const indexedUrl = isExternalContent
- ? body.content
- : `https://supermemory.ai/content/${contentId}`;
-
- // Insert into documents table with hash
- try {
- await db.insert(documents).values({
- uuid: contentId,
- userId: user.id,
- type: type.value,
- url: indexedUrl,
- title: body.prefetched?.title,
- description: body.prefetched?.description,
- ogImage: body.prefetched?.ogImage,
- contentHash: documentHash,
- raw:
- (body.prefetched ?? body.content) + "\n\n" + body.spaces?.join(" "),
- metadata: body.metadata,
- });
-
- await c.env.CONTENT_WORKFLOW.create({
- params: {
- userId: user.id,
- content: body.content,
- spaces: body.spaces,
- type: type.value,
- uuid: contentId,
- url: indexedUrl,
- prefetched: body.prefetched,
- },
- id: contentId,
- });
-
- return c.json({
- message: "Content added successfully",
- id: contentId,
- type: type.value,
- });
- } catch (error) {
- console.error("[Add Content Error]", error);
- return c.json({ error: "Failed to process content" }, 500);
- }
- }
- )
- .post(
- "/batch-add",
- zValidator(
- "json",
- z
- .object({
- urls: z
- .array(z.string())
- .min(1, "At least one URL is required")
- .optional(),
- contents: z
- .array(
- z.object({
- content: z.string(),
- title: z.string(),
- type: z.string(),
- })
- )
- .optional(),
- spaces: z.array(z.string()).max(5).optional(),
- })
- .refine((data) => data.urls || data.contents, {
- message: "Either urls or contents must be provided",
- })
- ),
- async (c) => {
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const { urls, contents, spaces } = await c.req.valid("json");
-
- // Check space permissions if spaces are specified
- if (spaces && spaces.length > 0) {
- const db = database(c.env.HYPERDRIVE.connectionString);
- const spacePermissions = await Promise.all(
- spaces.map(async (spaceId) => {
- const space = await db
- .select()
- .from(spaceInDb)
- .where(eq(spaceInDb.uuid, spaceId))
- .limit(1);
-
- if (!space[0]) {
- return { spaceId, allowed: false, error: "Space not found" };
- }
-
- const spaceData = space[0] as Space;
-
- // If public space, only owner can add content
- if (spaceData.isPublic) {
- return {
- spaceId,
- allowed: spaceData.ownerId === user.id,
- error:
- spaceData.ownerId !== user.id
- ? "Only space owner can add to public spaces"
- : null,
- };
- }
-
- // For private spaces, check if user is owner or in allowlist
- const spaceAccessCheck = await db
- .select()
- .from(spaceAccess)
- .where(
- and(
- eq(spaceAccess.spaceId, spaceData.id),
- eq(spaceAccess.userEmail, user.email),
- eq(spaceAccess.status, "accepted")
- )
- )
- .limit(1);
-
- return {
- spaceId,
- allowed:
- spaceData.ownerId === user.id || spaceAccessCheck.length > 0,
- error:
- spaceData.ownerId !== user.id && !spaceAccessCheck.length
- ? "Not authorized to add to this space"
- : null,
- };
- })
- );
-
- const unauthorized = spacePermissions.filter((p) => !p.allowed);
- if (unauthorized.length > 0) {
- return c.json(
- {
- error: "Space permission denied",
- details: unauthorized
- .map((u) => `${u.spaceId}: ${u.error}`)
- .join(", "),
- },
- 403
- );
- }
- }
-
- // Create a new ReadableStream for progress updates
- const encoder = new TextEncoder();
- const stream = new ReadableStream({
- async start(controller) {
- const db = database(c.env.HYPERDRIVE.connectionString);
- const items = urls || contents || [];
- const total = items.length;
- let processed = 0;
- let failed = 0;
- let succeeded = 0;
-
- const sendMessage = (data: any) => {
- const message = encoder.encode(`data: ${JSON.stringify(data)}\n\n`);
- controller.enqueue(message);
- };
-
- for (const item of items) {
- try {
- processed++;
-
- // Handle both URL and markdown content
- const content = typeof item === "string" ? item : item.content;
- const title = typeof item === "string" ? null : item.title;
- const type =
- typeof item === "string" ? typeDecider(item) : Ok(item.type);
-
- if (isErr(type)) {
- failed++;
- sendMessage({
- progress: Math.round((processed / total) * 100),
- status: "error",
- url: typeof item === "string" ? item : item.title,
- error: type.error.message,
- processed,
- total,
- succeeded,
- failed,
- });
- continue;
- }
-
- // Calculate document hash
- const encoder = new TextEncoder();
- const data = encoder.encode(content);
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
- const hashArray = Array.from(new Uint8Array(hashBuffer));
- const documentHash = hashArray
- .map((b) => b.toString(16).padStart(2, "0"))
- .join("");
-
- // Check for duplicates
- const existingDocs = await db
- .select()
- .from(documents)
- .where(
- and(
- eq(documents.userId, user.id),
- or(
- eq(documents.contentHash, documentHash),
- eq(documents.raw, content)
- )
- )
- );
-
- if (existingDocs.length > 0) {
- failed++;
- sendMessage({
- progress: Math.round((processed / total) * 100),
- status: "duplicate",
- title: typeof item === "string" ? item : item.title,
- processed,
- total,
- succeeded,
- failed,
- });
- continue;
- }
-
- const contentId = `add-${user.id}-${randomId()}`;
- const isExternalContent =
- typeof item === "string" &&
- ["page", "tweet", "document", "notion"].includes(type.value);
- const url = isExternalContent
- ? content
- : `https://supermemory.ai/content/${contentId}`;
-
- // Insert into documents table
- await db.insert(documents).values({
- uuid: contentId,
- userId: user.id,
- type: type.value,
- url,
- title,
- contentHash: documentHash,
- raw: content + "\n\n" + spaces?.join(" "),
- });
-
- // Create workflow for processing
- await c.env.CONTENT_WORKFLOW.create({
- params: {
- userId: user.id,
- content,
- spaces,
- type: type.value,
- uuid: contentId,
- url,
- },
- id: contentId,
- });
-
- succeeded++;
- sendMessage({
- progress: Math.round((processed / total) * 100),
- status: "success",
- title: typeof item === "string" ? item : item.title,
- processed,
- total,
- succeeded,
- failed,
- });
-
- // Add a small delay between requests
- await new Promise((resolve) => setTimeout(resolve, 100));
- } catch (error) {
- failed++;
- sendMessage({
- progress: Math.round((processed / total) * 100),
- status: "error",
- title: typeof item === "string" ? item : item.title,
- error: error instanceof Error ? error.message : "Unknown error",
- processed,
- total,
- succeeded,
- failed,
- });
- }
- }
-
- sendMessage({
- progress: 100,
- status: "complete",
- processed,
- total,
- succeeded,
- failed,
- });
- controller.close();
- },
- });
-
- return new Response(stream, {
- headers: {
- "Content-Type": "text/event-stream",
- "Cache-Control": "no-cache",
- Connection: "keep-alive",
- },
- });
- }
- );
-
-export default actions;
diff --git a/apps/backend/src/routes/integrations.ts b/apps/backend/src/routes/integrations.ts
deleted file mode 100644
index 9aa369ea..00000000
--- a/apps/backend/src/routes/integrations.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import { Hono } from "hono";
-import type { Env, Variables } from "../types";
-import { getDecryptedKV } from "encrypt-workers-kv";
-import { getAllNotionPageContents } from "../utils/notion";
-import { and, eq, or } from "@supermemory/db";
-import { documents } from "@supermemory/db/schema";
-import { database } from "@supermemory/db";
-import { fromHono } from "chanfana";
-
-const integrations = fromHono(
- new Hono<{ Variables: Variables; Bindings: Env }>()).get("/notion/import", async (c) => {
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- // Create SSE stream
- const stream = new TransformStream();
- const writer = stream.writable.getWriter();
- const encoder = new TextEncoder();
-
- // Create response first so client gets headers immediately
- const response = new Response(stream.readable, {
- headers: {
- "Content-Type": "text/event-stream",
- "Cache-Control": "no-cache",
- Connection: "keep-alive",
- // Required CORS headers for SSE
- "Access-Control-Allow-Origin": "*",
- "Access-Control-Allow-Credentials": "true",
- },
- });
-
- const sendMessage = async (data: Record<string, any>) => {
- // Proper SSE format requires "data: " prefix and double newline
- const formattedData = `data: ${JSON.stringify(data)}\n\n`;
- await writer.write(encoder.encode(formattedData));
- };
-
- // Start processing in background
- c.executionCtx.waitUntil(
- (async () => {
- try {
- // Send initial heartbeat
- await sendMessage({ type: "connected" });
-
- const token = await getDecryptedKV(
- c.env.ENCRYPTED_TOKENS,
- `${user.uuid}-notion`,
- `${c.env.WORKOS_COOKIE_PASSWORD}-${user.uuid}`
- );
-
- const stringToken = new TextDecoder().decode(token);
- if (!stringToken) {
- await sendMessage({ type: "error", error: "No token found" });
- await writer.close();
- return;
- }
-
- await sendMessage({ type: "progress", progress: 5 });
-
- // Fetch pages with progress updates
- const pages = await getAllNotionPageContents(
- stringToken,
- async (progress) => {
- // Map progress from 0-100 to 5-40 range
- const scaledProgress = Math.floor(5 + (progress * 35) / 100);
- await sendMessage({ type: "progress", progress: scaledProgress });
- }
- );
-
- await sendMessage({ type: "progress", progress: 40 });
-
- let processed = 0;
- const totalPages = pages.length;
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- for (const page of pages) {
- // Calculate document hash for duplicate detection
- const encoder = new TextEncoder();
- const data = encoder.encode(page.content);
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
- const hashArray = Array.from(new Uint8Array(hashBuffer));
- const documentHash = hashArray
- .map((b) => b.toString(16).padStart(2, "0"))
- .join("");
-
- // Check for duplicates using hash
- const existingDocs = await db
- .select()
- .from(documents)
- .where(
- and(
- eq(documents.userId, user.id),
- or(
- eq(documents.contentHash, documentHash),
- and(
- eq(documents.type, "notion"),
- or(
- eq(documents.url, page.url),
- eq(documents.raw, page.content)
- )
- )
- )
- )
- );
-
- if (existingDocs.length > 0) {
- await sendMessage({
- type: "warning",
- message: `Skipping duplicate page: ${page.title}`,
- });
- processed++;
- continue;
- }
-
- // Insert into documents table first
- try {
- await db.insert(documents).values({
- uuid: page.id,
- userId: user.id,
- type: "notion",
- url: page.url,
- title: page.title,
- contentHash: documentHash,
- raw: page.content,
- });
-
- await c.env.CONTENT_WORKFLOW.create({
- params: {
- userId: user.id,
- content: page.url,
- spaces: [],
- type: "notion",
- uuid: page.id,
- url: page.url,
- prefetched: {
- contentToVectorize: page.content,
- contentToSave: page.content,
- title: page.title,
- type: "notion",
- },
- createdAt: page.createdAt,
- },
- id: `${user.id}-${page.id}-${new Date().getTime()}`,
- });
-
- processed++;
- const progress = 50 + Math.floor((processed / totalPages) * 50);
- await sendMessage({ type: "progress", progress, page: page.title });
- } catch (error) {
- console.error(`Failed to process page ${page.title}:`, error);
- await sendMessage({
- type: "warning",
- message: `Failed to process page: ${page.title}`,
- error: error instanceof Error ? error.message : "Unknown error",
- });
- processed++;
- continue;
- }
- }
-
- await sendMessage({ type: "complete", progress: 100 });
- await writer.close();
- } catch (error) {
- console.error("Import error:", error);
- await sendMessage({
- type: "error",
- error: error instanceof Error ? error.message : "Import failed",
- });
- await writer.close();
- }
- })()
- );
-
- return response;
-});
-
-export default integrations;
diff --git a/apps/backend/src/routes/memories.ts b/apps/backend/src/routes/memories.ts
deleted file mode 100644
index f79da8ce..00000000
--- a/apps/backend/src/routes/memories.ts
+++ /dev/null
@@ -1,293 +0,0 @@
-import { Hono } from "hono";
-import { Variables, Env } from "../types";
-import { zValidator } from "@hono/zod-validator";
-import { z } from "zod";
-import {
- documents,
- spaces,
- spaceAccess,
- contentToSpace,
-} from "@supermemory/db/schema";
-import { and, database, desc, eq, or, sql, isNull } from "@supermemory/db";
-import { fromHono } from "chanfana";
-
-const memories = fromHono(new Hono<{ Variables: Variables; Bindings: Env }>())
- .get(
- "/",
- zValidator(
- "query",
- z.object({
- start: z.string().default("0").transform(Number),
- count: z.string().default("10").transform(Number),
- spaceId: z.string().optional(),
- })
- ),
- async (c) => {
- const { start, count, spaceId } = c.req.valid("query");
- const user = c.get("user");
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- console.log("Fetching memories with spaceId", spaceId);
- console.log(c.req.url);
- // If spaceId provided, verify access
- if (spaceId) {
- console.log("SpaceID provided", spaceId);
- const space = await db
- .select()
- .from(spaces)
- .where(eq(spaces.uuid, spaceId.split("---")[0]))
- .limit(1);
-
- if (!space[0]) {
- return c.json({ error: "Space not found" }, 404);
- }
-
- // Check access - allow if public, user owns the space, or has access through spaceAccess
- if (!space[0].isPublic && !user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- if (!space[0].isPublic && space[0].ownerId !== user?.id) {
- const access = await db
- .select()
- .from(spaceAccess)
- .where(
- and(
- eq(spaceAccess.spaceId, space[0].id),
- eq(spaceAccess.userEmail, user?.email ?? ""),
- eq(spaceAccess.status, "accepted")
- )
- )
- .limit(1);
-
- if (access.length === 0) {
- console.log("Unauthorized access to", c.req.url);
- return c.json({ error: "Unauthorized" }, 401);
- }
- }
-
- // Get documents for space
- const [items, totalResult] = await Promise.all([
- db
- .select({
- documents,
- })
- .from(documents)
- .innerJoin(
- contentToSpace,
- eq(documents.id, contentToSpace.contentId)
- )
- .where(eq(contentToSpace.spaceId, space[0].id))
- .orderBy(desc(documents.createdAt))
- .limit(count)
- .offset(start),
- db
- .select({
- total: sql<number>`count(*)`.as("total"),
- })
- .from(documents)
- .innerJoin(
- contentToSpace,
- eq(documents.id, contentToSpace.contentId)
- )
- .where(eq(contentToSpace.spaceId, space[0].id)),
- ]);
-
- const total = totalResult[0]?.total ?? 0;
-
- return c.json({
- items: items.map((item) => ({
- ...item.documents,
- id: item.documents.uuid,
- })),
- total,
- });
- }
-
- // Regular user memories endpoint
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- // Set cache control headers for 5 minutes
- c.header("Cache-Control", "private, max-age=300");
- c.header("Vary", "Cookie"); // Vary on Cookie since response depends on user
-
- // Generate ETag based on user ID, start, count
- const etag = `"${user.id}-${start}-${count}"`;
- c.header("ETag", etag);
-
- // Check if client has matching ETag
- const ifNoneMatch = c.req.header("If-None-Match");
- if (ifNoneMatch === etag) {
- return new Response(null, { status: 304 });
- }
-
- const [items, [{ total }]] = await Promise.all([
- db
- .select({
- documents: documents,
- })
- .from(documents)
- .leftJoin(contentToSpace, eq(documents.id, contentToSpace.contentId))
- .where(
- and(eq(documents.userId, user.id), isNull(contentToSpace.contentId))
- )
- .orderBy(desc(documents.createdAt))
- .limit(count)
- .offset(start),
- db
- .select({
- total: sql<number>`count(*)`.as("total"),
- })
- .from(documents)
- .leftJoin(contentToSpace, eq(documents.id, contentToSpace.contentId))
- .where(
- and(eq(documents.userId, user.id), isNull(contentToSpace.contentId))
- ),
- ]);
-
- return c.json({
- items: items.map((item) => ({
- ...item.documents,
- id: item.documents.uuid,
- })),
- total,
- });
- }
- )
- .get("/:id", zValidator("param", z.object({ id: z.string() })), async (c) => {
- const { id } = c.req.valid("param");
- const user = c.get("user");
-
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const memory = await database(c.env.HYPERDRIVE.connectionString)
- .select()
- .from(documents)
- .where(and(eq(documents.uuid, id), eq(documents.userId, user.id)))
- .limit(1);
-
- return c.json(memory[0]);
- })
- .delete(
- "/:id",
- zValidator("param", z.object({ id: z.string() })),
- async (c) => {
- const { id } = c.req.valid("param");
- const user = c.get("user");
-
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- let documentIdNum;
-
- try {
- documentIdNum = Number(id);
- } catch (e) {
- documentIdNum = null;
- }
-
- const doc = await db
- .select()
- .from(documents)
- .where(
- and(
- documentIdNum
- ? or(eq(documents.uuid, id), eq(documents.id, documentIdNum))
- : eq(documents.uuid, id),
- eq(documents.userId, user.id)
- )
- )
- .limit(1);
-
- if (!doc[0]) {
- return c.json({ error: "Document not found" }, 404);
- }
-
- const [document, contentToSpacei] = await Promise.all([
- db
- .delete(documents)
- .where(and(eq(documents.uuid, id), eq(documents.userId, user.id))),
- db
- .delete(contentToSpace)
- .where(eq(contentToSpace.contentId, doc[0].id)),
- ]);
-
- return c.json({ success: true });
- }
- )
- .post(
- "/batch-delete",
- zValidator(
- "json",
- z.object({
- ids: z.array(z.string()),
- })
- ),
- async (c) => {
- const { ids } = c.req.valid("json");
- const user = c.get("user");
-
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- try {
- // First get all valid documents that belong to the user
- const docs = await db
- .select()
- .from(documents)
- .where(
- and(
- eq(documents.userId, user.id),
- sql`${documents.uuid} = ANY(ARRAY[${ids}]::text[])`
- )
- );
-
- if (docs.length === 0) {
- return c.json({ error: "No valid documents found" }, 404);
- }
-
- const docIds = docs.map((doc) => doc.id);
-
- // Delete in transaction to ensure consistency
- await db.transaction(async (tx) => {
- await Promise.all([
- // Delete document entries
- tx
- .delete(documents)
- .where(
- and(
- eq(documents.userId, user.id),
- sql`${documents.uuid} = ANY(ARRAY[${ids}]::text[])`
- )
- ),
- // Delete space connections
- tx
- .delete(contentToSpace)
- .where(
- sql`${contentToSpace.contentId} = ANY(ARRAY[${docIds}]::int[])`
- ),
- ]);
- });
-
- return c.json({
- success: true,
- deletedCount: docs.length,
- });
- } catch (error) {
- console.error("Batch delete error:", error);
- return c.json({ error: "Failed to delete documents" }, 500);
- }
- }
- );
-
-export default memories;
diff --git a/apps/backend/src/routes/spaces.ts b/apps/backend/src/routes/spaces.ts
deleted file mode 100644
index e004ce78..00000000
--- a/apps/backend/src/routes/spaces.ts
+++ /dev/null
@@ -1,709 +0,0 @@
-import { Hono } from "hono";
-import { Env, Variables } from "../types";
-import { and, database, desc, eq, isNotNull, or, sql } from "@supermemory/db";
-import {
- contentToSpace,
- documents,
- savedSpaces,
- spaceAccess,
- spaces,
- users,
-} from "@supermemory/db/schema";
-import { zValidator } from "@hono/zod-validator";
-import { z } from "zod";
-import { randomId } from "@supermemory/shared";
-import { fromHono } from "chanfana";
-
-const spacesRoute = fromHono(
- new Hono<{ Variables: Variables; Bindings: Env }>())
- .get("/", async (c) => {
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- const [allSpaces, savedSpacesList, spaceOwners] = await Promise.all([
- db
- .select({
- id: spaces.id,
- uuid: spaces.uuid,
- name: sql<string>`REGEXP_REPLACE(${spaces.name}, E'[\\n\\r]+', ' ', 'g')`.as(
- "name"
- ),
- ownerId: spaces.ownerId,
- isPublic: spaces.isPublic,
- createdAt: spaces.createdAt,
- accessType: spaceAccess.accessType,
- })
- .from(spaces)
- .leftJoin(
- spaceAccess,
- and(
- eq(spaces.id, spaceAccess.spaceId),
- eq(spaceAccess.userEmail, user.email),
- eq(spaceAccess.status, "accepted")
- )
- )
- .where(or(eq(spaces.ownerId, user.id), isNotNull(spaceAccess.spaceId)))
- .orderBy(desc(spaces.createdAt)),
-
- db
- .select({
- spaceId: savedSpaces.spaceId,
- })
- .from(savedSpaces)
- .where(eq(savedSpaces.userId, user.id)),
-
- db
- .select({
- id: users.id,
- uuid: users.uuid,
- name: users.firstName,
- email: users.email,
- profileImage: users.profilePictureUrl,
- })
- .from(users)
- .innerJoin(spaces, eq(spaces.ownerId, users.id)),
- ]);
-
- const savedSpaceIds = new Set(savedSpacesList.map((s) => s.spaceId));
- const ownerMap = new Map(spaceOwners.map((owner) => [owner.id, owner]));
-
- const spacesWithDetails = allSpaces.map((space) => {
- const isOwner = space.ownerId === user.id;
- const owner = ownerMap.get(space.ownerId);
-
- return {
- ...space,
- favorited: savedSpaceIds.has(space.id),
- permissions: {
- canRead: space.isPublic || isOwner || space.accessType != null,
- canEdit: isOwner || space.accessType === "edit",
- isOwner,
- },
- owner: isOwner
- ? null
- : {
- id: owner?.uuid,
- name: owner?.name,
- email: owner?.email,
- profileImage: owner?.profileImage,
- },
- };
- });
-
- return c.json({ spaces: spacesWithDetails });
- })
- .get("/:spaceId", async (c) => {
- const user = c.get("user");
- const spaceId = c.req.param("spaceId");
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- const space = await db
- .select()
- .from(spaces)
- .where(eq(spaces.uuid, spaceId))
- .limit(1);
-
- if (!space[0]) {
- return c.json({ error: "Space not found" }, 404);
- }
-
- // For public spaces, anyone can read but only owner can edit
- if (space[0].isPublic) {
- const canEdit = user?.id === space[0].ownerId;
- return c.json({
- ...space[0],
- permissions: {
- canRead: true,
- canEdit,
- isOwner: space[0].ownerId === user?.id,
- isPublic: space[0].isPublic,
- },
- });
- }
-
- // For private spaces, require authentication
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- // Check if user is owner or has access via spaceAccess
- const isOwner = space[0].ownerId === user.id;
- let canEdit = isOwner;
- if (!isOwner) {
- const spaceAccessCheck = await db
- .select()
- .from(spaceAccess)
- .where(
- and(
- eq(spaceAccess.spaceId, space[0].id),
- eq(spaceAccess.userEmail, user.email),
- eq(spaceAccess.status, "accepted")
- )
- )
- .limit(1);
-
- if (spaceAccessCheck.length === 0) {
- return c.json({ error: "Access denied" }, 403);
- }
-
- canEdit = spaceAccessCheck[0].accessType === "edit";
- }
-
- return c.json({
- ...space[0],
- permissions: {
- canRead: true,
- canEdit,
- isOwner: space[0].ownerId === user.id,
- isPublic: space[0].isPublic,
- },
- });
- })
- .post(
- "/create",
- zValidator(
- "json",
- z.object({
- spaceName: z.string().min(1, "Space name cannot be empty").max(100),
- isPublic: z.boolean(), // keep this explicit please
- })
- ),
- async (c) => {
- const body = c.req.valid("json");
- const user = c.get("user");
-
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- if (body.spaceName.trim() === "<HOME>") {
- return c.json({ error: "Cannot create space with name <HOME>" }, 400);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
- const uuid = randomId();
-
- try {
- const space = await db
- .insert(spaces)
- .values({
- name: body.spaceName.trim(),
- ownerId: user.id,
- uuid,
- isPublic: body.isPublic,
- createdAt: new Date(),
- })
- .returning();
-
- return c.json({
- message: "Space created successfully",
- space: {
- uuid: space[0].uuid,
- name: space[0].name,
- ownerId: space[0].ownerId,
- isPublic: space[0].isPublic,
- createdAt: space[0].createdAt,
- },
- });
- } catch (error) {
- console.error("[Space Creation Error]", error);
- return c.json({ error: "Failed to create space" }, 500);
- }
- }
- )
- .post(
- "/:spaceId/favorite",
- zValidator(
- "param",
- z.object({
- spaceId: z.string(),
- })
- ),
- async (c) => {
- const user = c.get("user");
- const { spaceId } = c.req.valid("param");
-
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- // Get space details
- const space = await db
- .select()
- .from(spaces)
- .where(eq(spaces.uuid, spaceId))
- .limit(1);
-
- if (!space[0]) {
- return c.json({ error: "Space not found" }, 404);
- }
-
- // Check if it's user's own space
- if (space[0].ownerId === user.id) {
- return c.json({ error: "Cannot favorite your own space" }, 400);
- }
-
- try {
- await db.insert(savedSpaces).values({
- userId: user.id,
- spaceId: space[0].id,
- savedAt: new Date(),
- });
-
- return c.json({ message: "Space favorited successfully" });
- } catch (error) {
- if (
- error instanceof Error &&
- error.message.includes("saved_spaces_user_space_idx")
- ) {
- // Space is already favorited
- return c.json({ message: "Space already favorited" });
- }
- throw error;
- }
- }
- )
- .post(
- "/moveContent",
- zValidator(
- "json",
- z.object({
- spaceId: z.string(),
- documentId: z.string(),
- })
- ),
- async (c) => {
- const body = c.req.valid("json");
- const user = c.get("user");
- const { spaceId, documentId } = body;
-
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- try {
- await db.transaction(async (tx) => {
- // If moving to <HOME>, just remove all space connections
- if (spaceId === "<HOME>") {
- const doc = await tx
- .select()
- .from(documents)
- .where(eq(documents.uuid, documentId))
- .limit(1);
-
- if (!doc[0]) {
- return c.json({ error: "Document not found" }, 404);
- }
-
- await tx
- .delete(contentToSpace)
- .where(eq(contentToSpace.contentId, doc[0].id));
- return;
- }
-
- // Get space and document, verify space ownership
- const results = (
- await tx
- .select({
- spaceId: spaces.id,
- documentId: documents.id,
- ownerId: spaces.ownerId,
- spaceName: spaces.name,
- })
- .from(spaces)
- .innerJoin(
- documents,
- and(eq(spaces.uuid, spaceId), eq(documents.uuid, documentId))
- )
- .limit(1)
- )[0];
-
- if (!results) {
- return c.json({ error: "Space or document not found" }, 404);
- }
-
- if (results.ownerId !== user.id) {
- return c.json(
- { error: "Not authorized to modify this space" },
- 403
- );
- }
-
- // Delete existing space relations for this document
- await tx
- .delete(contentToSpace)
- .where(eq(contentToSpace.contentId, results.documentId));
-
- // Add new space relation
- await tx.insert(contentToSpace).values({
- contentId: results.documentId,
- spaceId: results.spaceId,
- });
- });
-
- return c.json({ success: true, spaceId });
- } catch (e) {
- console.error("Failed to move content to space:", e);
- return c.json(
- {
- error: "Failed to move content to space",
- details: e instanceof Error ? e.message : "Unknown error",
- },
- 500
- );
- }
- }
- )
- .post(
- "/addContent",
- zValidator(
- "json",
- z.object({
- spaceId: z.string(),
- documentId: z.string(),
- })
- ),
- async (c) => {
- const body = c.req.valid("json");
- const user = c.get("user");
- const { spaceId, documentId } = body;
-
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- try {
- await db.transaction(async (tx) => {
- // If adding to <HOME>, just remove all space connections
- if (spaceId === "<HOME>") {
- const doc = await tx
- .select()
- .from(documents)
- .where(eq(documents.uuid, documentId))
- .limit(1);
-
- if (!doc[0]) {
- return c.json({ error: "Document not found" }, 404);
- }
-
- await tx
- .delete(contentToSpace)
- .where(eq(contentToSpace.contentId, doc[0].id));
- return;
- }
-
- // Get space and document, verify space ownership
- const results = (
- await tx
- .select({
- spaceId: spaces.id,
- documentId: documents.id,
- ownerId: spaces.ownerId,
- })
- .from(spaces)
- .innerJoin(
- documents,
- and(eq(spaces.uuid, spaceId), eq(documents.uuid, documentId))
- )
- .limit(1)
- )[0];
-
- if (!results) {
- return c.json({ error: "Space or document not found" }, 404);
- }
-
- if (results.ownerId !== user.id) {
- return c.json(
- { error: "Not authorized to modify this space" },
- 403
- );
- }
-
- // Check if mapping already exists to avoid duplicates
- const existing = await tx
- .select()
- .from(contentToSpace)
- .where(
- and(
- eq(contentToSpace.contentId, results.documentId),
- eq(contentToSpace.spaceId, results.spaceId)
- )
- )
- .limit(1);
-
- if (existing.length > 0) {
- return c.json({ error: "Content already exists in space" }, 409);
- }
-
- await tx.insert(contentToSpace).values({
- contentId: results.documentId,
- spaceId: results.spaceId,
- });
- });
-
- return c.json({ success: true });
- } catch (e) {
- console.error("Failed to add content to space:", e);
- return c.json(
- {
- error: "Failed to add content to space",
- details: e instanceof Error ? e.message : "Unknown error",
- },
- 500
- );
- }
- }
- )
- .post(
- "/:spaceId/invite",
- zValidator(
- "json",
- z.object({
- email: z.string().email("Invalid email address"),
- accessType: z.enum(["read", "edit"], {
- errorMap: () => ({
- message: "Access type must be either 'read' or 'edit'",
- }),
- }),
- })
- ),
- async (c) => {
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const { spaceId } = c.req.param();
- const { email, accessType } = c.req.valid("json");
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- // Check if space exists and user has permission to invite
- const space = await db
- .select()
- .from(spaces)
- .where(eq(spaces.uuid, spaceId))
- .limit(1);
-
- if (space.length === 0) {
- return c.json({ error: "Space not found" }, 404);
- }
-
- // Only space owner can invite others
- if (space[0].ownerId !== user.id) {
- return c.json({ error: "Only space owner can invite users" }, 403);
- }
-
- // Check if invite already exists
- const existingInvite = await db
- .select()
- .from(spaceAccess)
- .where(
- and(
- eq(spaceAccess.spaceId, space[0].id),
- eq(spaceAccess.userEmail, email)
- )
- )
- .limit(1);
-
- if (existingInvite.length > 0) {
- return c.json(
- { error: "User already has access or pending invite" },
- 400
- );
- }
-
- // Create invite
- await db.insert(spaceAccess).values({
- spaceId: space[0].id,
- userEmail: email,
- accessType,
- status: "pending",
- });
-
- // TODO: send email to the user
-
- return c.json({ success: true });
- }
- )
- .get(
- "/:spaceId/invitation",
- zValidator("param", z.object({ spaceId: z.string() })),
- async (c) => {
- const { spaceId } = c.req.valid("param");
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- const space = await db
- .select()
- .from(spaces)
- .where(eq(spaces.uuid, spaceId))
- .limit(1);
-
- if (space.length === 0) {
- console.log("Space not found", spaceId);
- return c.json({ error: "Space not found" }, 401);
- }
-
- // Get pending invitation with access type
- const invitation = await db
- .select()
- .from(spaceAccess)
- .where(
- and(
- eq(spaceAccess.spaceId, space[0].id),
- eq(spaceAccess.userEmail, user.email),
- eq(spaceAccess.status, "pending")
- )
- )
- .limit(1);
-
- if (invitation.length === 0) {
- return c.json({ error: "No pending invitation found" }, 403);
- }
-
- return c.json({
- space: space[0],
- accessType: invitation[0].accessType,
- });
- }
- )
- .post(
- "/invites/:action",
- zValidator(
- "json",
- z.object({
- spaceId: z.string().min(5, "Invalid space ID format"),
- })
- ),
- async (c) => {
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- const { action } = c.req.param();
- if (action !== "accept" && action !== "reject") {
- return c.json({ error: "Invalid action" }, 400);
- }
-
- const { spaceId } = c.req.valid("json");
- console.log("space ID", spaceId);
-
- // Get space
- const space = await db
- .select()
- .from(spaces)
- .where(eq(spaces.uuid, spaceId))
- .limit(1);
-
- if (space.length === 0) {
- return c.json({ error: "Space not found" }, 404);
- }
-
- // Update invite status
- const updateResult = await db
- .update(spaceAccess)
- .set({ status: action === "accept" ? "accepted" : "rejected" })
- .where(
- and(
- eq(spaceAccess.spaceId, space[0].id),
- eq(spaceAccess.userEmail, user.email),
- eq(spaceAccess.status, "pending")
- )
- );
-
- if (updateResult.length === 0) {
- return c.json({ error: "No pending invite found" }, 404);
- }
-
- return c.json({ success: true });
- }
- )
- .patch(
- "/:spaceId",
- zValidator(
- "json",
- z.object({
- name: z.string().min(1, "Space name cannot be empty").max(100),
- })
- ),
- async (c) => {
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const { spaceId } = c.req.param();
- const { name } = c.req.valid("json");
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- // Get space and verify ownership
- const space = await db
- .select()
- .from(spaces)
- .where(eq(spaces.uuid, spaceId))
- .limit(1);
-
- if (space.length === 0) {
- return c.json({ error: "Space not found" }, 404);
- }
-
- if (space[0].ownerId !== user.id) {
- return c.json({ error: "Only space owner can edit space name" }, 403);
- }
-
- if (name.trim() === "<HOME>") {
- return c.json({ error: "Cannot use reserved name <HOME>" }, 400);
- }
-
- // Update space name
- await db
- .update(spaces)
- .set({ name: name.trim() })
- .where(eq(spaces.uuid, spaceId));
-
- return c.json({ success: true, name: name.trim() });
- }
- )
- .delete("/:spaceId", async (c) => {
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const { spaceId } = c.req.param();
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- const space = await db
- .select()
- .from(spaces)
- .where(eq(spaces.uuid, spaceId))
- .limit(1);
-
- if (space.length === 0) {
- return c.json({ error: "Space not found" }, 404);
- }
-
- await db.delete(spaces).where(eq(spaces.uuid, spaceId));
-
- return c.json({ success: true });
- });
-
-export default spacesRoute;
diff --git a/apps/backend/src/routes/user.ts b/apps/backend/src/routes/user.ts
deleted file mode 100644
index 4905989f..00000000
--- a/apps/backend/src/routes/user.ts
+++ /dev/null
@@ -1,193 +0,0 @@
-import { Hono } from "hono";
-import { Env, Variables } from "../types";
-import { and, database, desc, eq, isNotNull, or, sql } from "@supermemory/db";
-import {
- chatThreads,
- savedSpaces,
- spaceAccess,
- spaces,
- users,
-} from "@supermemory/db/schema";
-import { decryptApiKey, getApiKey } from "../auth";
-import { DurableObjectStore } from "@hono-rate-limiter/cloudflare";
-import { rateLimiter } from "hono-rate-limiter";
-import { fromHono } from "chanfana";
-
-const user = fromHono(new Hono<{ Variables: Variables; Bindings: Env }>())
- .get("/", (c) => {
- return c.json(c.get("user"));
- })
- .get("/spaces", async (c) => {
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- const [allSpaces, savedSpacesList, spaceOwners] = await Promise.all([
- db
- .select({
- id: spaces.id,
- uuid: spaces.uuid,
- name: sql<string>`REGEXP_REPLACE(${spaces.name}, E'[\\n\\r]+', ' ', 'g')`.as(
- "name"
- ),
- ownerId: spaces.ownerId,
- isPublic: spaces.isPublic,
- createdAt: spaces.createdAt,
- accessType: spaceAccess.accessType,
- })
- .from(spaces)
- .leftJoin(
- spaceAccess,
- and(
- eq(spaces.id, spaceAccess.spaceId),
- eq(spaceAccess.userEmail, user.email),
- eq(spaceAccess.status, "accepted")
- )
- )
- .where(or(eq(spaces.ownerId, user.id), isNotNull(spaceAccess.spaceId)))
- .orderBy(desc(spaces.createdAt)),
-
- db
- .select({
- spaceId: savedSpaces.spaceId,
- })
- .from(savedSpaces)
- .where(eq(savedSpaces.userId, user.id)),
-
- db
- .select({
- id: users.id,
- uuid: users.uuid,
- name: users.firstName,
- email: users.email,
- profileImage: users.profilePictureUrl,
- })
- .from(users)
- .innerJoin(spaces, eq(spaces.ownerId, users.id)),
- ]);
-
- const savedSpaceIds = new Set(savedSpacesList.map((s) => s.spaceId));
- const ownerMap = new Map(spaceOwners.map((owner) => [owner.id, owner]));
-
- const spacesWithDetails = allSpaces.map((space) => {
- const isOwner = space.ownerId === user.id;
- const owner = ownerMap.get(space.ownerId);
-
- return {
- ...space,
- favorited: savedSpaceIds.has(space.id),
- permissions: {
- canRead: space.isPublic || isOwner || space.accessType != null,
- canEdit: isOwner || space.accessType === "edit",
- isOwner,
- },
- owner: isOwner
- ? null
- : {
- id: owner?.uuid,
- name: owner?.name,
- email: owner?.email,
- profileImage: owner?.profileImage,
- },
- };
- });
-
- return c.json({ spaces: spacesWithDetails });
- })
- .get("/history", async (c) => {
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const history = await database(c.env.HYPERDRIVE.connectionString)
- .select()
- .from(chatThreads)
- .where(eq(chatThreads.userId, user.id))
- .orderBy(desc(chatThreads.createdAt))
- .limit(10);
-
- return c.json({ history });
- })
- .get("/invitations", async (c) => {
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- const invitations = await db
- .select({
- spaceAccess: spaceAccess,
- spaceUuid: spaces.uuid,
- spaceName: spaces.name,
- })
- .from(spaceAccess)
- .innerJoin(spaces, eq(spaceAccess.spaceId, spaces.id))
- .where(eq(spaceAccess.userEmail, user.email))
- .limit(100);
-
- return c.json({ invitations });
- })
- .get("/key", async (c) => {
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- // we need user.id and user.lastApiKeyGeneratedAt
- const lastApiKeyGeneratedAt = user.lastApiKeyGeneratedAt?.getTime();
- if (!lastApiKeyGeneratedAt) {
- return c.json({ error: "No API key generated" }, 400);
- }
-
- const key = await getApiKey(user.uuid, lastApiKeyGeneratedAt.toString(), c);
-
- const decrypted = await decryptApiKey(key, c);
- return c.json({ key, decrypted });
- })
- .post("/update", async (c) => {
- const user = c.get("user");
- if (!user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- const body = await c.req.json();
-
- // Only allow updating specific safe fields
- const allowedFields = {
- firstName: true,
- lastName: true,
- profilePictureUrl: true,
- hasOnboarded: true,
- };
-
- const updateData: Record<string, unknown> = {};
- for (const [key, value] of Object.entries(body)) {
- if (allowedFields[key as keyof typeof allowedFields]) {
- updateData[key] = value;
- }
- }
-
- if (Object.keys(updateData).length === 0) {
- return c.json({ error: "No valid fields to update" }, 400);
- }
-
- const db = database(c.env.HYPERDRIVE.connectionString);
-
- await db
- .update(users)
- .set({
- ...updateData,
- updatedAt: new Date(),
- })
- .where(eq(users.id, user.id));
-
- return c.json({ success: true });
- });
-
-export default user;
diff --git a/apps/backend/src/types.ts b/apps/backend/src/types.ts
deleted file mode 100644
index 0323a9e6..00000000
--- a/apps/backend/src/types.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { DurableObjectRateLimiter } from "@hono-rate-limiter/cloudflare";
-import { Session } from "@supermemory/authkit-remix-cloudflare/src/interfaces";
-import { User } from "@supermemory/db/schema";
-import { z } from "zod";
-
-export type Variables = {
- user: User | null;
- session: Session | null;
-};
-
-export type WorkflowParams = {
- userId: number;
- content: string;
- spaces?: string[];
- type: string;
- uuid: string;
- url?: string;
- prefetched?: {
- contentToVectorize: string;
- contentToSave: string;
- title: string;
- type: string;
- description: string;
- ogImage: string;
- };
- createdAt: string;
-};
-
-export type Env = {
- WORKOS_API_KEY: string;
- WORKOS_CLIENT_ID: string;
- WORKOS_COOKIE_PASSWORD: string;
- DATABASE_URL: string;
- CONTENT_WORKFLOW: Workflow;
- GEMINI_API_KEY: string;
- NODE_ENV: string;
- OPEN_AI_API_KEY: string;
- BRAINTRUST_API_KEY: string;
- RESEND_API_KEY: string;
- TURNSTILE_SECRET_KEY: string;
-
- MD_CACHE: KVNamespace;
- HYPERDRIVE: Hyperdrive;
- EMAIL_LIMITER: {
- limit: (params: { key: string }) => Promise<{ success: boolean }>;
- };
- ENCRYPTED_TOKENS: KVNamespace;
- RATE_LIMITER: DurableObjectNamespace<DurableObjectRateLimiter>;
- AI: Ai
-};
-
-export type JobData = {
- content: string;
- spaces?: Array<string>;
- user: number;
- type: string;
-};
-
-type BaseChunks = {
- type: "tweet" | "page" | "note" | "image";
-};
-
-export type PageOrNoteChunks = BaseChunks & {
- type: "page" | "note";
- chunks: string[];
-};
-
-export type Metadata = {
- media?: Array<string>;
- links?: Array<string>; // idk how ideal this is will figure out after plate js thing
-};
-
-export type SpaceStatus = {
- type: "inviting" | "invited" | "pending" | "accepted";
-};
-
-export const recommendedQuestionsSchema = z
- .array(z.string().max(200))
- .length(10);
diff --git a/apps/backend/src/utils/chunkers.ts b/apps/backend/src/utils/chunkers.ts
deleted file mode 100644
index ce345d29..00000000
--- a/apps/backend/src/utils/chunkers.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import nlp from "compromise";
-
-export default function chunkText(
- text: string,
- maxChunkSize: number,
- overlap: number = 0.2
-): string[] {
- // Pre-process text to remove excessive whitespace
- text = text.replace(/\s+/g, " ").trim();
-
- const sentences = nlp(text).sentences().out("array");
- const chunks: {
- text: string;
- start: number;
- end: number;
- metadata?: {
- position: string;
- context?: string;
- };
- }[] = [];
-
- let currentChunk: string[] = [];
- let currentSize = 0;
-
- for (let i = 0; i < sentences.length; i++) {
- const sentence = sentences[i].trim();
-
- // Skip empty sentences
- if (!sentence) continue;
-
- // If a single sentence is longer than maxChunkSize, split it
- if (sentence.length > maxChunkSize) {
- if (currentChunk.length > 0) {
- chunks.push({
- text: currentChunk.join(" "),
- start: i - currentChunk.length,
- end: i - 1,
- metadata: {
- position: `${i - currentChunk.length}-${i - 1}`,
- context: currentChunk[0].substring(0, 100), // First 100 chars for context
- },
- });
- currentChunk = [];
- currentSize = 0;
- }
-
- // Split long sentence into smaller chunks
- const words = sentence.split(" ");
- let tempChunk: string[] = [];
-
- for (const word of words) {
- if (tempChunk.join(" ").length + word.length > maxChunkSize) {
- chunks.push({
- text: tempChunk.join(" "),
- start: i,
- end: i,
- metadata: {
- position: `${i}`,
- context: "Split sentence",
- },
- });
- tempChunk = [];
- }
- tempChunk.push(word);
- }
-
- if (tempChunk.length > 0) {
- chunks.push({
- text: tempChunk.join(" "),
- start: i,
- end: i,
- metadata: {
- position: `${i}`,
- context: "Split sentence remainder",
- },
- });
- }
- continue;
- }
-
- currentChunk.push(sentence);
- currentSize += sentence.length;
-
- if (currentSize >= maxChunkSize) {
- const overlapSize = Math.floor(currentChunk.length * overlap);
- chunks.push({
- text: currentChunk.join(" "),
- start: i - currentChunk.length + 1,
- end: i,
- metadata: {
- position: `${i - currentChunk.length + 1}-${i}`,
- context: currentChunk[0].substring(0, 100),
- },
- });
-
- // Keep overlap sentences for next chunk
- currentChunk = currentChunk.slice(-overlapSize);
- currentSize = currentChunk.reduce((sum, s) => sum + s.length, 0);
- }
- }
-
- // Handle remaining sentences
- if (currentChunk.length > 0) {
- chunks.push({
- text: currentChunk.join(" "),
- start: sentences.length - currentChunk.length,
- end: sentences.length - 1,
- metadata: {
- position: `${sentences.length - currentChunk.length}-${sentences.length - 1}`,
- context: currentChunk[0].substring(0, 100),
- },
- });
- }
-
- return chunks.map((chunk) => chunk.text);
-}
diff --git a/apps/backend/src/utils/cipher.ts b/apps/backend/src/utils/cipher.ts
deleted file mode 100644
index 3ba2e905..00000000
--- a/apps/backend/src/utils/cipher.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-async function encrypt(data: string, key: string): Promise<string> {
- try {
- const encoder = new TextEncoder();
- const encodedData = encoder.encode(data);
-
- const baseForIv = encoder.encode(data + key);
- const ivHash = await crypto.subtle.digest('SHA-256', baseForIv);
- const iv = new Uint8Array(ivHash).slice(0, 12);
-
- const cryptoKey = await crypto.subtle.importKey(
- "raw",
- encoder.encode(key),
- { name: "AES-GCM", length: 256 },
- false,
- ["encrypt", "decrypt"]
- );
-
- const encrypted = await crypto.subtle.encrypt(
- { name: "AES-GCM", iv: new Uint8Array(iv).buffer as ArrayBuffer },
- cryptoKey,
- encodedData
- );
-
- const combined = new Uint8Array([...iv, ...new Uint8Array(encrypted)]);
-
- // Convert to base64 safely
- const base64 = Buffer.from(combined).toString("base64");
-
- // Make URL-safe
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
- } catch (err) {
- console.error("Encryption error:", err);
- throw err;
- }
-}
-
-async function decrypt(encryptedData: string, key: string): Promise<string> {
- try {
- // Restore base64 padding and convert URL-safe chars
- const base64 = encryptedData
- .replace(/-/g, "+")
- .replace(/_/g, "/")
- .padEnd(
- encryptedData.length + ((4 - (encryptedData.length % 4)) % 4),
- "="
- );
-
- // Use Buffer for safer base64 decoding
- const combined = Buffer.from(base64, "base64");
- const combinedArray = new Uint8Array(combined);
-
- // Extract the IV that was used for encryption
- const iv = combinedArray.slice(0, 12);
- const encrypted = combinedArray.slice(12);
-
- // Import the same key used for encryption
- const cryptoKey = await crypto.subtle.importKey(
- "raw",
- new TextEncoder().encode(key),
- { name: "AES-GCM", length: 256 },
- false,
- ["encrypt", "decrypt"]
- );
-
- // Use the extracted IV and key to decrypt
- const decrypted = await crypto.subtle.decrypt(
- { name: "AES-GCM", iv: new Uint8Array(iv).buffer as ArrayBuffer },
- cryptoKey,
- encrypted.buffer as ArrayBuffer
- );
-
- return new TextDecoder().decode(decrypted);
- } catch (err) {
- console.error("Decryption error:", err);
- throw err;
- }
-}
-
-export { encrypt, decrypt }; \ No newline at end of file
diff --git a/apps/backend/src/utils/extractDocumentContent.ts b/apps/backend/src/utils/extractDocumentContent.ts
deleted file mode 100644
index 8b7d9256..00000000
--- a/apps/backend/src/utils/extractDocumentContent.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import * as mammoth from "mammoth";
-import { NonRetryableError } from "cloudflare:workflows";
-import { resolvePDFJS } from 'pdfjs-serverless';
-
-interface DocumentContent {
- content: string;
- error?: string;
-}
-
-export const extractDocumentContent = async (
- url: string
-): Promise<DocumentContent> => {
- try {
- const fileExtension = url.split(".").pop()?.toLowerCase();
-
- if (!fileExtension) {
- throw new Error("Invalid file URL");
- }
-
- console.log("file", fileExtension);
-
- switch (fileExtension) {
- case "pdf":
- return await extractPdfContent(url);
- case "md":
- case "txt":
- return await extractTextContent(url);
- case "doc":
- case "docx":
- return await extractWordContent(url);
- default:
- throw new NonRetryableError(`Unsupported file type: ${fileExtension}`);
- }
- } catch (error) {
- return {
- content: "",
- error: error instanceof Error ? error.message : "Unknown error occurred",
- };
- }
-};
-
-async function extractPdfContent(url: string): Promise<DocumentContent> {
- try {
- const response = await fetch(url);
- const arrayBuffer = await response.arrayBuffer();
-
- // Initialize PDF.js with serverless compatibility
- const { getDocument } = await resolvePDFJS();
-
- // Load the PDF document
- const pdf = await getDocument({
- data: arrayBuffer,
- useSystemFonts: true,
- }).promise;
-
- let fullText = "";
-
- // Extract text from each page
- for (let i = 1; i <= pdf.numPages; i++) {
- const page = await pdf.getPage(i);
- const textContent = await page.getTextContent();
- const pageText = textContent.items.map((item: any) => item.str).join(" ");
- fullText += pageText + "\n";
- }
-
- return { content: fullText };
- } catch (error) {
- console.error("Error extracting PDF content:", error);
- return {
- content: "",
- error: error instanceof Error ? error.message : "Failed to extract PDF content",
- };
- }
-}
-
-async function extractTextContent(url: string): Promise<DocumentContent> {
- const response = await fetch(url);
- const text = await response.text();
- return { content: text };
-}
-
-async function extractWordContent(url: string): Promise<DocumentContent> {
- const response = await fetch(url);
- const arrayBuffer = await response.arrayBuffer();
- const result = await mammoth.extractRawText({ arrayBuffer });
- return { content: result.value };
-}
diff --git a/apps/backend/src/utils/extractor.ts b/apps/backend/src/utils/extractor.ts
deleted file mode 100644
index f033f8e1..00000000
--- a/apps/backend/src/utils/extractor.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { Env } from "../types";
-
-export const extractPageContent = async (content: string, env: Env) => {
- const resp = await fetch(`https://r.jina.ai/${content}`);
-
- if (!resp.ok) {
- throw new Error(
- `Failed to fetch ${content}: ${resp.statusText}` + (await resp.text())
- );
- }
-
- const metadataResp = await fetch(`https://md.dhr.wtf/metadata?url=${content}`);
-
- if (!metadataResp.ok) {
- throw new Error(
- `Failed to fetch metadata for ${content}: ${metadataResp.statusText}` +
- (await metadataResp.text())
- );
- }
-
- const metadata = await metadataResp.json() as {
- title?: string;
- description?: string;
- image?: string;
- favicon?: string;
- };
-
- const responseText = await resp.text();
-
- try {
- const json: {
- contentToVectorize: string;
- contentToSave: string;
- title?: string;
- description?: string;
- image?: string;
- favicon?: string;
- } = {
- contentToSave: responseText,
- contentToVectorize: responseText,
- title: metadata.title,
- description: metadata.description,
- image: metadata.image,
- favicon: metadata.favicon,
- };
- return json;
- } catch (e) {
- throw new Error(`Failed to parse JSON from ${content}: ${e}`);
- }
-};
diff --git a/apps/backend/src/utils/fetchers.ts b/apps/backend/src/utils/fetchers.ts
deleted file mode 100644
index 2329f48a..00000000
--- a/apps/backend/src/utils/fetchers.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-import { WorkflowStep } from "cloudflare:workers";
-import { isErr, Ok } from "../errors/results";
-import { typeDecider } from "./typeDecider";
-import { Env, WorkflowParams } from "../types";
-import { unrollTweets } from "./tweetsToThreads";
-import { Tweet } from "react-tweet/api";
-import { NonRetryableError } from "cloudflare:workflows";
-import { extractPageContent } from "./extractor";
-import { extractDocumentContent } from "./extractDocumentContent";
-
-export const fetchContent = async (
- params: WorkflowParams,
- env: Env,
- step: WorkflowStep
-) => {
- const type = typeDecider(params.content);
-
- if (isErr(type)) {
- throw type.error;
- }
-
- switch (type.value) {
- case "page":
- const pageContent = await step?.do(
- "extract page content",
- async () => await extractPageContent(params.content, env)
- );
- return {
- ...pageContent,
- type: "page",
- };
-
- case "tweet":
- const tweetUrl = new URL(params.content);
- tweetUrl.search = ""; // Remove all search params
- const tweetId = tweetUrl.pathname.split("/").pop();
-
- const rawBaseTweetContent = await step.do(
- "extract tweet content",
- async () => {
- const url = `https://cdn.syndication.twimg.com/tweet-result?id=${tweetId}&lang=en&features=tfw_timeline_list%3A%3Btfw_follower_count_sunset%3Atrue%3Btfw_tweet_edit_backend%3Aon%3Btfw_refsrc_session%3Aon%3Btfw_fosnr_soft_interventions_enabled%3Aon%3Btfw_show_birdwatch_pivots_enabled%3Aon%3Btfw_show_business_verified_badge%3Aon%3Btfw_duplicate_scribes_to_settings%3Aon%3Btfw_use_profile_image_shape_enabled%3Aon%3Btfw_show_blue_verified_badge%3Aon%3Btfw_legacy_timeline_sunset%3Atrue%3Btfw_show_gov_verified_badge%3Aon%3Btfw_show_business_affiliate_badge%3Aon%3Btfw_tweet_edit_frontend%3Aon&token=4c2mmul6mnh`;
-
- const resp = await fetch(url, {
- headers: {
- "User-Agent":
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
- Accept: "application/json",
- "Accept-Language": "en-US,en;q=0.5",
- "Accept-Encoding": "gzip, deflate, br",
- Connection: "keep-alive",
- "Upgrade-Insecure-Requests": "1",
- "Cache-Control": "max-age=0",
- TE: "Trailers",
- },
- });
-
- const data = (await resp.json()) as Tweet;
- return data;
- }
- );
-
- let tweetContent: {
- text: string;
- metadata: {
- media?: string[] | undefined;
- links?: string[] | undefined;
- };
- raw: string;
- };
- const unrolledTweetContent = {
- value: [rawBaseTweetContent],
- };
- if (true) {
- console.error("Can't get thread, reverting back to single tweet");
- tweetContent = {
- text: rawBaseTweetContent.text,
- metadata: {
- media: [
- ...(rawBaseTweetContent.photos?.map((url) => url.expandedUrl) ??
- []),
- ...(rawBaseTweetContent.video?.variants[0].src ?? []),
- ],
- },
- raw: `<raw>${JSON.stringify(rawBaseTweetContent)}</raw>`,
- };
- } else {
- tweetContent = {
- text: unrolledTweetContent.value
- .map((tweet) => tweet.text)
- .join("\n"),
- metadata: {
- media: unrolledTweetContent.value.flatMap((tweet) => [
- ...tweet.videos,
- ...tweet.images,
- ]),
- links: unrolledTweetContent.value.flatMap((tweet) => tweet.links),
- },
- raw: `<raw>${JSON.stringify(rawBaseTweetContent)}</raw>`,
- };
- }
-
- // make it the same type as the page content
- const pageContentType: Awaited<ReturnType<typeof extractPageContent>> & {
- type: string;
- } = {
- contentToVectorize:
- tweetContent.text +
- "\n\nMetadata for this tweet:\n" +
- JSON.stringify(tweetContent.metadata) +
- "\n\nRaw tweet data:\n" +
- tweetContent.raw,
- contentToSave: tweetContent.raw,
- title: "",
- description: JSON.stringify(tweetContent.metadata),
- image: "",
- favicon: "",
- type: "tweet",
- };
- return pageContentType;
- case "note":
- const noteContent = {
- contentToVectorize: params.content,
- // TODO: different when using platejs
- contentToSave: params.content,
- // title is the first 30 characters of the first line
- title: params.content.split("\n")[0].slice(0, 30),
- type: "note",
- };
- return noteContent;
- case "document":
- const documentContent = await step.do(
- "extract document content",
- async () => await extractDocumentContent(params.content)
- );
- return {
- contentToVectorize: documentContent.content,
- contentToSave: documentContent.content,
- type: "document",
- };
- default:
- throw new NonRetryableError("Unknown content type");
- }
-};
diff --git a/apps/backend/src/utils/notion.ts b/apps/backend/src/utils/notion.ts
deleted file mode 100644
index ebe559e1..00000000
--- a/apps/backend/src/utils/notion.ts
+++ /dev/null
@@ -1,239 +0,0 @@
-interface PageContent {
- content: string;
- url: string;
- title: string;
- id: string;
- createdAt: string;
-}
-
-interface NotionBlock {
- type: string;
- [key: string]: any;
-}
-
-interface SearchResponse {
- results: {
- id: string;
- object: string;
- url: string;
- created_time: string;
- properties: {
- title?: {
- title: Array<{
- plain_text: string;
- }>;
- };
- Name?: {
- title: Array<{
- plain_text: string;
- }>;
- };
- };
- }[];
- next_cursor: string | undefined;
- has_more: boolean;
-}
-
-interface BlockResponse {
- results: NotionBlock[];
- next_cursor: string | undefined;
- has_more: boolean;
-}
-
-export const getAllNotionPageContents = async (
- token: string,
- onProgress: (progress: number) => Promise<void>
-): Promise<PageContent[]> => {
- const pages: PageContent[] = [];
- const NOTION_API_VERSION = "2022-06-28";
- const BASE_URL = "https://api.notion.com/v1";
- const MAX_RETRIES = 3;
- const BATCH_SIZE = 10; // Number of concurrent requests
- const PAGE_SIZE = 100; // Number of pages to fetch per search request
-
- const delay = (ms: number) =>
- new Promise((resolve) => setTimeout(resolve, ms));
-
- const notionFetch = async (
- endpoint: string,
- options: RequestInit = {},
- retries = 0
- ): Promise<any> => {
- try {
- const response = await fetch(`${BASE_URL}${endpoint}`, {
- ...options,
- headers: {
- Authorization: `Bearer ${token}`,
- "Notion-Version": NOTION_API_VERSION,
- "Content-Type": "application/json",
- ...((options.headers || {}) as Record<string, string>),
- },
- });
-
- if (response.status === 429) {
- // Rate limit error
- const retryAfter = parseInt(response.headers.get("Retry-After") || "5");
- if (retries < MAX_RETRIES) {
- await delay(retryAfter * 1000);
- return notionFetch(endpoint, options, retries + 1);
- }
- }
-
- if (!response.ok) {
- const errorText = await response.text();
- throw new Error(
- `Notion API error: ${response.statusText}\n${errorText}`
- );
- }
-
- return response.json();
- } catch (error) {
- if (retries < MAX_RETRIES) {
- await delay(2000 * (retries + 1)); // Exponential backoff
- return notionFetch(endpoint, options, retries + 1);
- }
- throw error;
- }
- };
-
- const convertBlockToMarkdown = (block: NotionBlock): string => {
- switch (block.type) {
- case "paragraph":
- return (
- block.paragraph?.rich_text
- ?.map((text: any) => text.plain_text)
- .join("") || ""
- );
- case "heading_1":
- return `# ${block.heading_1?.rich_text
- ?.map((text: any) => text.plain_text)
- .join("")}\n`;
- case "heading_2":
- return `## ${block.heading_2?.rich_text
- ?.map((text: any) => text.plain_text)
- .join("")}\n`;
- case "heading_3":
- return `### ${block.heading_3?.rich_text
- ?.map((text: any) => text.plain_text)
- .join("")}\n`;
- case "bulleted_list_item":
- return `* ${block.bulleted_list_item?.rich_text
- ?.map((text: any) => text.plain_text)
- .join("")}\n`;
- case "numbered_list_item":
- return `1. ${block.numbered_list_item?.rich_text
- ?.map((text: any) => text.plain_text)
- .join("")}\n`;
- case "to_do":
- const checked = block.to_do?.checked ? "x" : " ";
- return `- [${checked}] ${block.to_do?.rich_text
- ?.map((text: any) => text.plain_text)
- .join("")}\n`;
- case "code":
- return `\`\`\`${block.code?.language || ""}\n${block.code?.rich_text
- ?.map((text: any) => text.plain_text)
- .join("")}\n\`\`\`\n`;
- case "quote":
- return `> ${block.quote?.rich_text
- ?.map((text: any) => text.plain_text)
- .join("")}\n`;
- default:
- return "";
- }
- };
-
- const getAllBlocks = async (pageId: string): Promise<NotionBlock[]> => {
- const blocks: NotionBlock[] = [];
- let cursor: string | undefined = undefined;
-
- do {
- const endpoint = `/blocks/${pageId}/children${
- cursor ? `?start_cursor=${cursor}` : ""
- }`;
- const response = (await notionFetch(endpoint)) as BlockResponse;
- blocks.push(...response.results);
- cursor = response.next_cursor;
- } while (cursor);
-
- return blocks;
- };
-
- try {
- let hasMore = true;
- let cursor: string | undefined = undefined;
- let allPages: SearchResponse["results"] = [];
-
- // First, collect all pages
- while (hasMore) {
- const searchResponse = (await notionFetch("/search", {
- method: "POST",
- body: JSON.stringify({
- filter: {
- value: "page",
- property: "object",
- },
- sort: {
- direction: "ascending",
- timestamp: "last_edited_time",
- },
- start_cursor: cursor,
- page_size: PAGE_SIZE,
- }),
- })) as SearchResponse;
-
- allPages = [...allPages, ...searchResponse.results];
- cursor = searchResponse.next_cursor;
- hasMore = searchResponse.has_more;
-
- // Report progress for page collection (0-30%)
- const progressPercent = (allPages.length / (allPages.length + searchResponse.results.length)) * 30;
- await onProgress(progressPercent);
- }
-
- // Process pages in parallel batches
- for (let i = 0; i < allPages.length; i += BATCH_SIZE) {
- const batch = allPages.slice(i, i + BATCH_SIZE);
- const batchResults = await Promise.all(
- batch.map(async (page) => {
- try {
- const blocks = await getAllBlocks(page.id);
- const pageContent = {
- content: blocks.map(convertBlockToMarkdown).join("\n"),
- url: page.url || `https://notion.so/${page.id.replace(/-/g, "")}`,
- title:
- page.properties?.Name?.title?.[0]?.plain_text ||
- page.properties?.title?.title?.[0]?.plain_text ||
- "Untitled",
- id: page.id,
- createdAt: page.created_time,
- };
- return pageContent.content.length > 10 ? pageContent : null;
- } catch (error) {
- console.error(`Error processing page ${page.id}:`, error);
- return null;
- }
- })
- );
-
- pages.push(
- ...batchResults.filter(
- (result): result is PageContent => result !== null
- )
- );
-
- // Report progress for page processing (30-100%)
- const progressPercent = 30 + ((i + BATCH_SIZE) / allPages.length) * 70;
- await onProgress(Math.min(progressPercent, 100));
-
- // Add a small delay between batches to respect rate limits
- if (i + BATCH_SIZE < allPages.length) {
- await delay(1000);
- }
- }
-
- return pages.filter((page) => page.content.length > 10);
- } catch (error) {
- console.error("Error fetching Notion pages:", error);
- throw error;
- }
-};
diff --git a/apps/backend/src/utils/tweetsToThreads.ts b/apps/backend/src/utils/tweetsToThreads.ts
deleted file mode 100644
index 85f69b87..00000000
--- a/apps/backend/src/utils/tweetsToThreads.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import * as cheerio from "cheerio";
-import { BaseError } from "../errors/baseError";
-import { Ok, Result } from "../errors/results";
-
-interface Tweet {
- id: string;
- text: string;
- links: Array<string>;
- images: Array<string>;
- videos: Array<string>;
-}
-
-class ProcessTweetsError extends BaseError {
- constructor(message?: string, source?: string) {
- super("[Thread Proceessing Error]", message, source);
- }
-}
-
-type TweetProcessResult = Array<Tweet>;
-
-// there won't be a need for url caching right?
-export async function unrollTweets(
- url: string
-): Promise<Result<TweetProcessResult, ProcessTweetsError>> {
- const tweetId = url.split("/").pop();
- const response = await fetch(`https://unrollnow.com/status/${tweetId}`, {
- headers: {
- "User-Agent":
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
- "Cache-Control": "max-age=3600",
- },
- });
-
- if (!response.ok) {
- const error = await response.text();
- console.error(error);
- throw new Error(`HTTP error! status: ${response.status} - ${error}`);
- }
-
- const html = await response.text();
- const $ = cheerio.load(html);
- const tweets: Array<Tweet> = [];
-
- const urlRegex = /(https?:\/\/\S+)/g;
- const paragraphs = $(".mainarticle p").toArray();
-
- const processedTweets = await Promise.all(
- paragraphs.map(async (element, i) => {
- const $tweet = $(element);
- let tweetText = $tweet.text().trim();
- if (tweetText.length < 1) {
- return null;
- }
-
- if (i === paragraphs.length - 1 && tweetText.toLowerCase() === "yes") {
- return null;
- }
-
- const shortUrls = tweetText.match(urlRegex) || [];
- console.log("SHORT_URLS_LEN", shortUrls.length);
- console.log("SHORT_URLS", shortUrls);
-
- const expandedUrls = await Promise.all(shortUrls.map(expandShortUrl));
-
- tweetText = tweetText.replace(urlRegex, "").trim().replace(/\s+/g, " ");
-
- const images = $tweet
- .nextUntil("p")
- .find("img.tweetimg")
- .map((i, img) => $(img).attr("src"))
- .get();
-
- const videos = $tweet
- .nextUntil("p")
- .find("video > source")
- .map((i, vid) => $(vid).attr("src"))
- .get();
-
- return {
- id: `${tweetId}_${i}`,
- text: tweetText,
- links: expandedUrls,
- images: images,
- videos: videos,
- };
- })
- );
-
- tweets.push(
- ...processedTweets.filter((tweet): tweet is Tweet => tweet !== null)
- );
-
- return Ok(tweets);
-}
-
-async function expandShortUrl(shortUrl: string): Promise<string> {
- try {
- const response = await fetch(shortUrl, {
- method: "HEAD",
- redirect: "follow",
- });
- const expandedUrl = response.url;
- return expandedUrl;
- } catch (error) {
- console.error(`Failed to expand URL: ${shortUrl}`, error);
- return shortUrl;
- }
-}
diff --git a/apps/backend/src/utils/typeDecider.ts b/apps/backend/src/utils/typeDecider.ts
deleted file mode 100644
index 642b178e..00000000
--- a/apps/backend/src/utils/typeDecider.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Result, Ok, Err } from "../errors/results";
-import { BaseError } from "../errors/baseError";
-
-export type contentType = "page" | "tweet" | "note" | "document" | "notion";
-
-class GetTypeError extends BaseError {
- constructor(message?: string, source?: string) {
- super("[Decide Type Error]", message, source);
- }
-}
-export const typeDecider = (
- content: string
-): Result<contentType, GetTypeError> => {
- try {
- // if the content is a URL, then it's a page. if its a URL with https://x.com/user/status/123, then it's a tweet.
- // if it ends with .pdf etc then it's a document. else, it's a note.
- // do strict checking with regex
- if (
- content.match(/https?:\/\/(x\.com|twitter\.com)\/[\w]+\/[\w]+\/[\d]+/)
- ) {
- return Ok("tweet");
- } else if (content.match(/\.(pdf|doc|docx|txt|rtf|odt|md)/i)) {
- return Ok("document");
- } else if (
- content.match(/https?:\/\/(www\.)?notion\.so\/.*/)
- ) {
- return Ok("notion");
- } else if (
- content.match(
- /^(https?:\/\/)?(www\.)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(\/.*)?$/i
- )
- ) {
- return Ok("page");
- } else {
- return Ok("note");
- }
- } catch (e) {
- console.error("[Decide Type Error]", e);
- return Err(new GetTypeError((e as Error).message, "typeDecider"));
- }
-};
diff --git a/apps/backend/src/workflow/index.ts b/apps/backend/src/workflow/index.ts
deleted file mode 100644
index 8efcfacc..00000000
--- a/apps/backend/src/workflow/index.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-import {
- WorkflowEntrypoint,
- WorkflowStep,
- WorkflowEvent,
-} from "cloudflare:workers";
-import { Env, WorkflowParams } from "../types";
-import { fetchContent } from "../utils/fetchers";
-import chunkText from "../utils/chunkers";
-import { database, eq, inArray } from "@supermemory/db";
-import {
- ChunkInsert,
- contentToSpace,
- documents,
- spaces,
-} from "@supermemory/db/schema";
-import { embedMany } from "ai";
-import { openai } from "../providers";
-import { chunk } from "@supermemory/db/schema";
-import { NonRetryableError } from "cloudflare:workflows";
-
-// TODO: handle errors properly here.
-
-export class ContentWorkflow extends WorkflowEntrypoint<Env, WorkflowParams> {
- async run(event: WorkflowEvent<WorkflowParams>, step: WorkflowStep) {
- // Step 0: Check if user has reached memory limit
- await step.do("check memory limit", async () => {
- const existingMemories = await database(
- this.env.HYPERDRIVE.connectionString
- )
- .select()
- .from(documents)
- .where(eq(documents.userId, event.payload.userId));
-
- if (existingMemories.length >= 2000) {
- await database(this.env.HYPERDRIVE.connectionString)
- .delete(documents)
- .where(eq(documents.uuid, event.payload.uuid));
- throw new NonRetryableError(
- "You have reached the maximum limit of 2000 memories"
- );
- }
- });
-
- // Step 1: Get and format the content.
- const rawContent =
- event.payload.prefetched ??
- (await step.do(
- "fetch content",
- async () => await fetchContent(event.payload, this.env, step)
- ));
-
- // check that the rawcontent is not too big
- if (rawContent.contentToVectorize.length > 100000) {
- await database(this.env.HYPERDRIVE.connectionString)
- .delete(documents)
- .where(eq(documents.uuid, event.payload.uuid));
- throw new NonRetryableError("The content is too big (maximum 20 pages)");
- }
-
- const chunked = await step.do("chunk content", async () =>
- chunkText(rawContent.contentToVectorize, 768)
- );
-
- // Step 2: Create the document in the database.
- const document = await step.do("create document", async () => {
- try {
- // First check if document exists
- const existingDoc = await database(this.env.HYPERDRIVE.connectionString)
- .select()
- .from(documents)
- .where(eq(documents.uuid, event.payload.uuid))
- .limit(1);
-
- return await database(this.env.HYPERDRIVE.connectionString)
- .insert(documents)
- .values({
- userId: event.payload.userId,
- type: event.payload.type,
- uuid: event.payload.uuid,
- ...(event.payload.url && { url: event.payload.url }),
- title: rawContent.title,
- content: rawContent.contentToSave,
- description:
- "description" in rawContent
- ? (rawContent.description ?? "")
- : (event.payload.prefetched?.description ?? undefined),
- ogImage:
- "image" in rawContent
- ? (rawContent.image ?? "")
- : (event.payload.prefetched?.ogImage ?? undefined),
- raw: rawContent.contentToVectorize,
- isSuccessfullyProcessed: false,
- updatedAt: new Date(),
- ...(event.payload.createdAt && {
- createdAt: new Date(event.payload.createdAt),
- }),
- })
- .onConflictDoUpdate({
- target: documents.uuid,
- set: {
- title: rawContent.title,
- content: rawContent.contentToSave,
- description:
- "description" in rawContent
- ? (rawContent.description ?? "")
- : (event.payload.prefetched?.description ?? undefined),
- ogImage:
- "image" in rawContent
- ? (rawContent.image ?? "")
- : (event.payload.prefetched?.ogImage ?? undefined),
- raw: rawContent.contentToVectorize,
- isSuccessfullyProcessed: false,
- updatedAt: new Date(),
- },
- })
- .returning();
- } catch (error) {
- console.log("here's the error", error);
- // Check if error is a unique constraint violation
- if (
- error instanceof Error &&
- error.message.includes("document_url_user_id_idx")
- ) {
- // Document already exists for this user, stop workflow
- await database(this.env.HYPERDRIVE.connectionString)
- .delete(documents)
- .where(eq(documents.uuid, event.payload.uuid));
- throw new NonRetryableError("Document already exists for this user");
- }
- if (
- error instanceof Error &&
- error.message.includes("document_raw_user_idx")
- ) {
- await database(this.env.HYPERDRIVE.connectionString)
- .delete(documents)
- .where(eq(documents.uuid, event.payload.uuid));
- throw new NonRetryableError("The exact same document already exists");
- }
- throw error; // Re-throw other errors
- }
- });
-
- if (!document || document.length === 0) {
- throw new Error(
- "Failed to create/update document - no document returned"
- );
- }
-
- // Step 3: Generate embeddings
- const { data: embeddings } = await this.env.AI.run(
- "@cf/baai/bge-base-en-v1.5",
- {
- text: chunked,
- }
- );
-
- // Step 4: Prepare chunk data
- const chunkInsertData: ChunkInsert[] = await step.do(
- "prepare chunk data",
- async () =>
- chunked.map((chunk, index) => ({
- documentId: document[0].id,
- textContent: chunk,
- orderInDocument: index,
- embeddings: embeddings[index],
- }))
- );
-
- // Step 5: Insert chunks
- if (chunkInsertData.length > 0) {
- await step.do("insert chunks", async () =>
- database(this.env.HYPERDRIVE.connectionString).transaction(
- async (trx) => {
- await trx.insert(chunk).values(chunkInsertData);
- }
- )
- );
- }
-
- // step 6: add content to spaces
- if (event.payload.spaces) {
- await step.do("add content to spaces", async () => {
- await database(this.env.HYPERDRIVE.connectionString).transaction(
- async (trx) => {
- // First get the space IDs from the UUIDs
- const spaceIds = await trx
- .select({ id: spaces.id })
- .from(spaces)
- .where(inArray(spaces.uuid, event.payload.spaces ?? []));
-
- if (spaceIds.length === 0) {
- return;
- }
-
- // Then insert the content-space mappings using the actual space IDs
- await trx.insert(contentToSpace).values(
- spaceIds.map((space) => ({
- contentId: document[0].id,
- spaceId: space.id,
- }))
- );
- }
- );
- });
- }
-
- // Step 7: Mark the document as successfully processed
- await step.do("mark document as successfully processed", async () => {
- await database(this.env.HYPERDRIVE.connectionString)
- .update(documents)
- .set({
- isSuccessfullyProcessed: true,
- })
- .where(eq(documents.id, document[0].id));
- });
- }
-}
diff --git a/apps/backend/tailwind.config.js b/apps/backend/tailwind.config.js
deleted file mode 100644
index f7c22ec3..00000000
--- a/apps/backend/tailwind.config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: ["./src/**/*.{js,ts,jsx,tsx}"],
- theme: {
- extend: {},
- },
- plugins: [],
-}
-
diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json
deleted file mode 100644
index a45a87e9..00000000
--- a/apps/backend/tsconfig.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "compilerOptions": {
- "target": "ESNext",
- "module": "ESNext",
- "moduleResolution": "Bundler",
- "strict": true,
- "skipLibCheck": true,
- "lib": ["ESNext"],
- "types": [
- "@cloudflare/workers-types/experimental",
- "@cloudflare/workers-types"
- ],
- "jsx": "react-jsx",
- "jsxImportSource": "hono/jsx"
- }
-}
diff --git a/apps/backend/types.d.ts b/apps/backend/types.d.ts
deleted file mode 100644
index 1a304bc7..00000000
--- a/apps/backend/types.d.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-declare module "@mixmark-io/domino" {
- export function createDocument(html: string): Document;
- } \ No newline at end of file
diff --git a/apps/backend/wrangler.toml b/apps/backend/wrangler.toml
deleted file mode 100644
index 36832053..00000000
--- a/apps/backend/wrangler.toml
+++ /dev/null
@@ -1,55 +0,0 @@
-name = "supermemory-backend"
-main = "src/index.tsx"
-compatibility_date = "2024-10-11"
-compatibility_flags = [ "nodejs_compat" ]
-
-[assets]
-directory = "./public/"
-binding = "ASSETS"
-
-
-[observability]
-enabled = true
-
-[placement]
-mode = "smart"
-
-[ai]
-binding = "AI"
-
-[[workflows]]
-name = "content-workflow-supermemory"
-binding = "CONTENT_WORKFLOW"
-class_name = "ContentWorkflow"
-
-
-[[kv_namespaces]]
-binding= "MD_CACHE"
-id = "3186489f943d409a9b772d876a58a73e"
-preview_id = "3186489f943d409a9b772d876a58a73e"
-
-[[kv_namespaces]]
-binding = "ENCRYPTED_TOKENS"
-id = "a1f048ee14644468ad63b817b5648a31"
-preview_id = "a1f048ee14644468ad63b817b5648a31"
-
-[[hyperdrive]]
-binding = "HYPERDRIVE"
-id = "3a377d1b9c084e698ee201f10dfa8131"
-localConnectionString = "postgres://postgres:postgres@localhost:5432/supermemorylocal?sslmode=require"
-
-[[unsafe.bindings]]
-name = "EMAIL_LIMITER"
-type = "ratelimit"
-namespace_id = "2114284"
-simple = { limit = 1, period = 60 }
-
-tail_consumers = [{service = "supermemory-backend-tail"}]
-
-[[durable_objects.bindings]]
-name = "RATE_LIMITER"
-class_name = "DurableObjectRateLimiter"
-
-[[migrations]]
-tag = "v1"
-new_classes = ["DurableObjectRateLimiter"] \ No newline at end of file
diff --git a/apps/docs/README.md b/apps/docs/README.md
deleted file mode 100644
index 4cbe0750..00000000
--- a/apps/docs/README.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# Mintlify Starter Kit
-
-Click on `Use this template` to copy the Mintlify starter kit. The starter kit contains examples including
-
-- Guide pages
-- Navigation
-- Customizations
-- API Reference pages
-- Use of popular components
-
-### Development
-
-Install the [Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the documentation changes locally. To install, use the following command
-
-```
-npm i -g mintlify
-```
-
-Run the following command at the root of your documentation (where mint.json is)
-
-```
-mintlify dev
-```
-
-### Publishing Changes
-
-Install our Github App to auto propagate changes from your repo to your deployment. Changes will be deployed to production automatically after pushing to the default branch. Find the link to install on your dashboard.
-
-#### Troubleshooting
-
-- Mintlify dev isn't running - Run `mintlify install` it'll re-install dependencies.
-- Page loads as a 404 - Make sure you are running in a folder with `mint.json`
diff --git a/apps/docs/api-reference/endpoints/connect/connect-app.mdx b/apps/docs/api-reference/endpoints/connect/connect-app.mdx
deleted file mode 100644
index 712c0739..00000000
--- a/apps/docs/api-reference/endpoints/connect/connect-app.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-openapi: get /connect/{app}
----
-
-You may connect supermemory to other apps.
-when you send a GET request to the \/connect:APP?id= endpoint, you will get a redirectURL. This is a safe URL that your users can click and select the appropriate files with. Once this is done, supermemory will periodically re-fetch and make sure that the data is always fresh.
-
-As of right now, these apps are supported:
-- Notion
-
diff --git a/apps/docs/api-reference/endpoints/connect/connection-information.mdx b/apps/docs/api-reference/endpoints/connect/connection-information.mdx
deleted file mode 100644
index d13af160..00000000
--- a/apps/docs/api-reference/endpoints/connect/connection-information.mdx
+++ /dev/null
@@ -1,5 +0,0 @@
----
-openapi: get /connections/{connectionId}
----
-
-Get the connection details using this endpoint. \ No newline at end of file
diff --git a/apps/docs/api-reference/endpoints/memory-management/delete-delete.mdx b/apps/docs/api-reference/endpoints/memory-management/delete-delete.mdx
deleted file mode 100644
index 57ba0ea4..00000000
--- a/apps/docs/api-reference/endpoints/memory-management/delete-delete.mdx
+++ /dev/null
@@ -1,3 +0,0 @@
----
-openapi: delete /delete/{id}
---- \ No newline at end of file
diff --git a/apps/docs/api-reference/endpoints/memory-management/post-add.mdx b/apps/docs/api-reference/endpoints/memory-management/post-add.mdx
deleted file mode 100644
index dd03dbe4..00000000
--- a/apps/docs/api-reference/endpoints/memory-management/post-add.mdx
+++ /dev/null
@@ -1,59 +0,0 @@
----
-openapi: post /add
----
-
-Add a new memory with content and metadata.
-
-Fields:
-
-`content`: string
-
-`id`: string
-
-`metadata`: Record
-
-The `content` can be of the following types:
-
-- note \/ Markdown
-
- - If it is a markdown, all the images inside `![]` image tags will automatically be parsed.
-
-- pdf
-
-- tweet
-
-- google_doc
-
-- notion_doc
-
-- webpage URL
-
- - Images and other content is also intelligently parsed in case of a webpage.
-
-
-The metadata provided is a JSON object.
-
-for eg.
-
-``` json
-{
- "classId": "21412",
- "year": "fifth"
-}
-
- ```
-
-If you wish to do exact searches, please use strings. But if you want to search in a range (time, numbers, prices), you can use numbers too.
-
-``` json
-{
- "price": 1250
-}
-
- ```
-
-More about \[metadata filtering here\]([https://docs.supermemory.ai/essentials/metadata-filtering](https://docs.supermemory.ai/essentials/metadata-filtering))
-
-The `id` is optional. If provided, supermemory will store the same ID as your internal database. This can help for retrieval purposes.
-
-If the `id` already exists, supermemory will update it instead. \ No newline at end of file
diff --git a/apps/docs/api-reference/endpoints/memory-management/put-update.mdx b/apps/docs/api-reference/endpoints/memory-management/put-update.mdx
deleted file mode 100644
index cd8b408c..00000000
--- a/apps/docs/api-reference/endpoints/memory-management/put-update.mdx
+++ /dev/null
@@ -1,7 +0,0 @@
----
-openapi: put /update/{id}
----
-
-Update an existing memory.
-Please note that all existing metadata will be replaced with the new ones.
-You can also use the \/add endpoint along with the ID specified. \ No newline at end of file
diff --git a/apps/docs/api-reference/endpoints/search/get-fastsearch.mdx b/apps/docs/api-reference/endpoints/search/get-fastsearch.mdx
deleted file mode 100644
index 8e088e35..00000000
--- a/apps/docs/api-reference/endpoints/search/get-fastsearch.mdx
+++ /dev/null
@@ -1,5 +0,0 @@
----
-openapi: get /fastsearch
----
-
-Fast, lossy search using quantized embeddings. This can be used in case your app has text completions, or when searching fast is absolutely necessary. \ No newline at end of file
diff --git a/apps/docs/api-reference/endpoints/search/post-search.mdx b/apps/docs/api-reference/endpoints/search/post-search.mdx
deleted file mode 100644
index 72352e4a..00000000
--- a/apps/docs/api-reference/endpoints/search/post-search.mdx
+++ /dev/null
@@ -1,12 +0,0 @@
----
-openapi: post /search
----
-
-Search through documents with metadata filtering.
-
-Body:
-`q`: Your search query
-
-`limit`: Number of documents you want to get
-
-`filters`: Filters can be applied as `AND, OR, negate, numeric` types. You can read more about it here - \[metadata filtering here\]([https://docs.supermemory.ai/essentials/metadata-filtering](https://docs.supermemory.ai/essentials/metadata-filtering))
diff --git a/apps/docs/api-reference/endpoints/settings.mdx b/apps/docs/api-reference/endpoints/settings.mdx
deleted file mode 100644
index 89120be7..00000000
--- a/apps/docs/api-reference/endpoints/settings.mdx
+++ /dev/null
@@ -1,3 +0,0 @@
----
-openapi: put /settings
---- \ No newline at end of file
diff --git a/apps/docs/changelog/overview.mdx b/apps/docs/changelog/overview.mdx
deleted file mode 100644
index d9cf59fe..00000000
--- a/apps/docs/changelog/overview.mdx
+++ /dev/null
@@ -1,12 +0,0 @@
----
-title: "Product Updates"
-description: "New updates and improvements"
-mode: "center"
----
-
-<Update label="2025-02-01" description="v0.1.1">
- - You can now search for memories in multiple spaces at once.
- - All endpoints have been updated to `/v1` for better versioning
- - Improved documentation and examples
- - Interactive [API Playground](https://docs.supermemory.ai/api-reference)
-</Update> \ No newline at end of file
diff --git a/apps/docs/essentials/metadata-filtering.mdx b/apps/docs/essentials/metadata-filtering.mdx
deleted file mode 100644
index d2df77ea..00000000
--- a/apps/docs/essentials/metadata-filtering.mdx
+++ /dev/null
@@ -1,78 +0,0 @@
----
-title: "Managing Multi-User Search Results"
-description: "Learn how to handle search results for different users in Supermemory"
-icon: "users"
----
-
-When building multi-user applications with Supermemory, you'll often need to manage data for different users accessing the same account.
-
-You might also want filters, like memories from **_multiple users_**, or in a certain **_time range_**, or products within a certain price category.
-
-You can do all this filtering using Supermemory's api.
-
-Here's a quick example
-
-```json
-{
- "AND": [
- {
- "filterType": "numeric",
- "key": "timestamp",
- "value": "1742745777",
- "negate": false,
- "numericOperator": ">"
- },
- {
- "key": "group",
- "value": "jira_users",
- "negate": false
- },
- {
- "OR": [
- {
- "key": "team_name",
- "value": "engineering",
- "negate": false
- },
- {
- "key": "org_name",
- "value": "supermemory",
- "negate": false
- }
- ]
- }
- ]
-}
-```
-
-You can compose these conditions together to add filtering:
-
-- `AND`
-- `OR`
-- `numeric` (greater than / less than)
-
-Here's an example call:
-
-```bash
-curl --location 'https://v2.api.supermemory.ai/search' \
---header 'x-api-key: supermemory_RXPx' \
---header 'Content-Type: application/json' \
---data '{
- "q": "How to use teamcity to set up a project?",
- "limit": 10,
- "filters": {
- "AND": [
- {
- "key": "book",
- "value": "maths",
- "negate": false
- },
- {
- "key": "author",
- "value": "r.d. sharma",
- "negate": false
- }
- ]
- }
-}'
-``` \ No newline at end of file
diff --git a/apps/docs/essentials/pricing.mdx b/apps/docs/essentials/pricing.mdx
deleted file mode 100644
index a2167460..00000000
--- a/apps/docs/essentials/pricing.mdx
+++ /dev/null
@@ -1,47 +0,0 @@
----
-title: "Pricing"
-description: "Our pricing plans"
-icon: "dollar-sign"
----
-
-### Free!
-
-Yes, everything is free & open source.
-Supermemory is built by [me](https://dhravya.dev), a college student. My life situations make it very difficult and almost impossible to monetise the product.
-
-Any kind of sponsorships / support would mean a lot to me, and help me keep supermemory alive.
-
-You can sponsor on my Github sponsors page - https://github.com/sponsors/dhravya
-
-### How can I trust you?
-
-Making the product free makes it hard to trust. "How will you manage the infra?", "How will you keep the product running?" are very valid questions.
-
-I've got you covered.
-
-Supermemory has the support of [Cloudflare startups program](https://www.cloudflare.com/forstartups/), [Google Cloud startup program](https://cloud.google.com/startups) and grants by [Vercel](https://vercel.com) and other amazing companies.
-
-We're fully committed to keeping the product running, and happy to sign any agreements you'd like.
-
-### Ask
-
-Please email me at [email protected] if you're interested in:
-
-- Sponsoring the product
-- Funding the product
-- Signing a letter of intent for potential funding rounds
-- Partnering with us
-
-### Self hosting guidelines
-
-As of right now, Supermemory is licensed under the [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](https://github.com/supermemoryai/supermemory/blob/main/LICENSE)
-
-This means,
-
-- You can use the code for personal projects, given appropriate attribution.
-- For non-commercial use, the code must be open source.
-- Please reach out to me if you want to use the code for commercial projects.
-
-If you're an enterprise, please reach out to me at [email protected].
-
-You can still use the API as a hosted service.
diff --git a/apps/docs/favicon.png b/apps/docs/favicon.png
deleted file mode 100644
index fd3dd8d3..00000000
--- a/apps/docs/favicon.png
+++ /dev/null
Binary files differ
diff --git a/apps/docs/image.png b/apps/docs/image.png
deleted file mode 100644
index a99caa14..00000000
--- a/apps/docs/image.png
+++ /dev/null
Binary files differ
diff --git a/apps/docs/images/checks-passed.png b/apps/docs/images/checks-passed.png
deleted file mode 100644
index 3303c773..00000000
--- a/apps/docs/images/checks-passed.png
+++ /dev/null
Binary files differ
diff --git a/apps/docs/images/hero-dark.svg b/apps/docs/images/hero-dark.svg
deleted file mode 100644
index b2c2bcee..00000000
--- a/apps/docs/images/hero-dark.svg
+++ /dev/null
@@ -1,32 +0,0 @@
-<svg width="2344" height="969" viewBox="0 0 2344 969" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_2392_147)">
-<rect width="2344" height="969" rx="100" fill="#0F1114"/>
-<g opacity="0.4" filter="url(#filter0_f_2392_147)">
-<rect x="-2772.03" y="1936.79" width="7287.1" height="929.709" transform="rotate(-30 -2772.03 1936.79)" fill="#369DFD"/>
-</g>
-<g filter="url(#filter1_d_2392_147)">
-<path d="M854.654 430.165L832.896 432.517C832.678 430.295 832.003 428.226 830.87 426.31C829.738 424.393 828.039 422.868 825.774 421.736C823.552 420.56 820.677 419.972 817.149 419.972C812.008 419.972 807.5 421.126 803.623 423.435C799.746 425.743 797.546 428.705 797.024 432.321C796.544 435.022 797.198 437.222 798.984 438.92C800.77 440.619 804.08 442.057 808.916 443.233L825.512 446.892C834.66 448.939 841.238 452.25 845.245 456.824C849.296 461.398 850.712 467.387 849.492 474.793C848.447 480.978 845.746 486.423 841.39 491.128C837.034 495.789 831.458 499.426 824.663 502.04C817.911 504.653 810.418 505.96 802.186 505.96C789.684 505.96 780.057 503.368 773.305 498.185C766.597 492.957 763.264 485.77 763.308 476.622L786.569 474.401C786.83 478.931 788.42 482.35 791.339 484.659C794.258 486.924 798.309 488.079 803.492 488.122C809.329 488.166 814.252 486.946 818.259 484.463C822.311 481.937 824.597 478.866 825.12 475.25C825.556 472.549 824.881 470.349 823.095 468.651C821.352 466.952 818.238 465.601 813.751 464.599L797.285 461.006C787.963 458.958 781.32 455.495 777.356 450.616C773.436 445.694 772.107 439.487 773.37 431.994C774.416 425.896 776.964 420.647 781.015 416.247C785.11 411.804 790.359 408.384 796.762 405.989C803.166 403.549 810.331 402.33 818.259 402.33C830.195 402.33 839.19 404.834 845.245 409.844C851.3 414.853 854.436 421.627 854.654 430.165ZM931.376 461.79L941.112 403.636H964.765L948.038 504H925.104L928.109 486.162H927.064C923.927 491.781 919.441 496.377 913.604 499.949C907.81 503.521 901.145 505.307 893.609 505.307C887.032 505.307 881.478 503.848 876.947 500.929C872.417 497.967 869.194 493.676 867.277 488.057C865.404 482.394 865.121 475.555 866.427 467.54L877.143 403.636H900.797L890.734 463.881C889.732 470.241 890.647 475.294 893.479 479.04C896.354 482.786 900.622 484.659 906.285 484.659C909.77 484.659 913.277 483.81 916.805 482.111C920.377 480.412 923.492 477.885 926.149 474.531C928.806 471.134 930.549 466.886 931.376 461.79ZM962.474 541.636L985.605 403.636H1008.87L1006.19 420.233H1007.56C1009.13 417.794 1011.24 415.202 1013.9 412.457C1016.6 409.67 1020.02 407.295 1024.16 405.335C1028.29 403.331 1033.33 402.33 1039.25 402.33C1046.96 402.33 1053.6 404.312 1059.18 408.276C1064.75 412.196 1068.74 418.011 1071.14 425.722C1073.58 433.388 1073.88 442.797 1072.05 453.949C1070.22 464.97 1066.85 474.335 1061.92 482.045C1057 489.756 1051.1 495.636 1044.22 499.688C1037.33 503.739 1029.95 505.764 1022.06 505.764C1016.31 505.764 1011.68 504.806 1008.15 502.889C1004.66 500.973 1002.03 498.664 1000.24 495.963C998.455 493.219 997.126 490.627 996.255 488.188H995.21L986.323 541.636H962.474ZM1000.44 453.818C999.392 460.309 999.37 465.993 1000.37 470.872C1001.42 475.751 1003.49 479.562 1006.58 482.307C1009.67 485.008 1013.72 486.358 1018.73 486.358C1023.96 486.358 1028.53 484.964 1032.45 482.176C1036.42 479.345 1039.71 475.49 1042.32 470.611C1044.98 465.688 1046.85 460.091 1047.94 453.818C1048.94 447.589 1048.94 442.057 1047.94 437.222C1046.94 432.386 1044.91 428.597 1041.86 425.852C1038.81 423.108 1034.68 421.736 1029.45 421.736C1024.35 421.736 1019.8 423.064 1015.79 425.722C1011.83 428.379 1008.54 432.103 1005.93 436.895C1003.31 441.687 1001.48 447.328 1000.44 453.818ZM1124.35 505.96C1114.29 505.96 1105.97 503.869 1099.39 499.688C1092.81 495.462 1088.2 489.494 1085.54 481.784C1082.93 474.03 1082.49 464.904 1084.23 454.406C1085.93 444.082 1089.39 435.022 1094.62 427.224C1099.85 419.384 1106.38 413.285 1114.22 408.929C1122.11 404.529 1130.82 402.33 1140.36 402.33C1146.55 402.33 1152.23 403.331 1157.41 405.335C1162.6 407.295 1166.98 410.345 1170.55 414.483C1174.12 418.621 1176.58 423.892 1177.93 430.295C1179.28 436.655 1179.22 444.235 1177.73 453.034L1176.62 460.287H1094.29L1096.84 444.344H1156.43C1157.22 439.813 1156.96 435.784 1155.65 432.256C1154.34 428.684 1152.16 425.874 1149.12 423.827C1146.07 421.779 1142.25 420.756 1137.68 420.756C1133.02 420.756 1128.66 421.954 1124.61 424.349C1120.61 426.702 1117.23 429.751 1114.48 433.497C1111.74 437.243 1110 441.186 1109.26 445.324L1106.58 460.614C1105.62 466.886 1105.84 472.005 1107.23 475.969C1108.63 479.933 1111.07 482.851 1114.55 484.724C1118.04 486.598 1122.43 487.534 1127.75 487.534C1131.23 487.534 1134.46 487.055 1137.42 486.097C1140.38 485.138 1143.02 483.701 1145.33 481.784C1147.68 479.824 1149.64 477.428 1151.21 474.597L1172.83 477.08C1170.44 482.917 1166.93 488.013 1162.31 492.369C1157.7 496.682 1152.16 500.036 1145.72 502.432C1139.31 504.784 1132.19 505.96 1124.35 505.96ZM1185.75 504L1202.48 403.636H1225.41L1222.6 420.364H1223.65C1226.39 414.57 1230.27 410.105 1235.28 406.969C1240.33 403.789 1245.73 402.199 1251.48 402.199C1252.83 402.199 1254.29 402.264 1255.86 402.395C1257.43 402.482 1258.78 402.634 1259.91 402.852L1256.25 424.611C1255.25 424.262 1253.68 423.957 1251.55 423.696C1249.46 423.391 1247.39 423.239 1245.34 423.239C1241.03 423.239 1237.02 424.175 1233.32 426.048C1229.66 427.878 1226.57 430.426 1224.04 433.693C1221.51 436.96 1219.88 440.728 1219.14 444.997L1209.4 504H1185.75ZM1255.27 504L1272 403.636H1294.67L1291.79 420.69H1293.04C1296.09 414.94 1300.29 410.454 1305.65 407.23C1311 403.963 1317.04 402.33 1323.75 402.33C1330.5 402.33 1335.94 403.985 1340.08 407.295C1344.26 410.562 1346.83 415.027 1347.79 420.69H1348.84C1351.97 415.071 1356.46 410.606 1362.3 407.295C1368.18 403.985 1374.78 402.33 1382.1 402.33C1391.24 402.33 1398.26 405.248 1403.14 411.085C1408.01 416.922 1409.52 425.438 1407.64 436.634L1396.41 504H1372.75L1383.47 440.293C1384.43 434.063 1383.47 429.511 1380.59 426.636C1377.76 423.718 1373.95 422.259 1369.16 422.259C1363.45 422.259 1358.7 424.023 1354.91 427.551C1351.17 431.08 1348.86 435.697 1347.99 441.403L1337.47 504H1314.27L1325.12 439.312C1325.9 434.129 1324.99 429.991 1322.37 426.898C1319.8 423.805 1316.01 422.259 1311 422.259C1307.61 422.259 1304.36 423.13 1301.27 424.872C1298.18 426.615 1295.54 429.054 1293.36 432.19C1291.23 435.283 1289.83 438.877 1289.18 442.972L1278.99 504H1255.27ZM1460.83 505.96C1450.76 505.96 1442.44 503.869 1435.87 499.688C1429.29 495.462 1424.67 489.494 1422.01 481.784C1419.4 474.03 1418.97 464.904 1420.71 454.406C1422.41 444.082 1425.87 435.022 1431.1 427.224C1436.32 419.384 1442.86 413.285 1450.7 408.929C1458.58 404.529 1467.3 402.33 1476.84 402.33C1483.02 402.33 1488.71 403.331 1493.89 405.335C1499.07 407.295 1503.45 410.345 1507.02 414.483C1510.6 418.621 1513.06 423.892 1514.41 430.295C1515.76 436.655 1515.69 444.235 1514.21 453.034L1513.1 460.287H1430.77L1433.32 444.344H1492.91C1493.69 439.813 1493.43 435.784 1492.13 432.256C1490.82 428.684 1488.64 425.874 1485.59 423.827C1482.54 421.779 1478.73 420.756 1474.16 420.756C1469.5 420.756 1465.14 421.954 1461.09 424.349C1457.08 426.702 1453.71 429.751 1450.96 433.497C1448.22 437.243 1446.47 441.186 1445.73 445.324L1443.05 460.614C1442.1 466.886 1442.31 472.005 1443.71 475.969C1445.1 479.933 1447.54 482.851 1451.03 484.724C1454.51 486.598 1458.91 487.534 1464.22 487.534C1467.71 487.534 1470.93 487.055 1473.9 486.097C1476.86 485.138 1479.49 483.701 1481.8 481.784C1484.15 479.824 1486.11 477.428 1487.68 474.597L1509.31 477.08C1506.91 482.917 1503.41 488.013 1498.79 492.369C1494.17 496.682 1488.64 500.036 1482.19 502.432C1475.79 504.784 1468.67 505.96 1460.83 505.96ZM1522.29 504L1539.02 403.636H1561.69L1558.82 420.69H1560.06C1563.11 414.94 1567.31 410.454 1572.67 407.23C1578.03 403.963 1584.06 402.33 1590.77 402.33C1597.52 402.33 1602.97 403.985 1607.1 407.295C1611.29 410.562 1613.86 415.027 1614.81 420.69H1615.86C1619 415.071 1623.48 410.606 1629.32 407.295C1635.2 403.985 1641.8 402.33 1649.12 402.33C1658.27 402.33 1665.28 405.248 1670.16 411.085C1675.04 416.922 1676.54 425.438 1674.67 436.634L1663.43 504H1639.77L1650.49 440.293C1651.45 434.063 1650.49 429.511 1647.62 426.636C1644.78 423.718 1640.97 422.259 1636.18 422.259C1630.47 422.259 1625.73 424.023 1621.94 427.551C1618.19 431.08 1615.88 435.697 1615.01 441.403L1604.49 504H1581.29L1592.14 439.312C1592.93 434.129 1592.01 429.991 1589.4 426.898C1586.83 423.805 1583.04 422.259 1578.03 422.259C1574.63 422.259 1571.38 423.13 1568.29 424.872C1565.2 426.615 1562.56 429.054 1560.39 432.19C1558.25 435.283 1556.86 438.877 1556.2 442.972L1546.01 504H1522.29ZM1727.72 505.96C1717.83 505.96 1709.6 503.782 1703.02 499.426C1696.49 495.07 1691.89 488.95 1689.23 481.065C1686.58 473.181 1686.1 464.033 1687.8 453.622C1689.49 443.342 1692.89 434.347 1697.99 426.636C1703.13 418.926 1709.6 412.958 1717.4 408.733C1725.24 404.464 1734.01 402.33 1743.73 402.33C1753.62 402.33 1761.83 404.529 1768.36 408.929C1774.9 413.285 1779.49 419.405 1782.15 427.29C1784.85 435.174 1785.35 444.366 1783.65 454.864C1782 465.1 1778.58 474.052 1773.39 481.719C1768.25 489.385 1761.78 495.353 1753.99 499.622C1746.19 503.848 1737.43 505.96 1727.72 505.96ZM1729.68 487.011C1735.17 487.011 1739.98 485.53 1744.12 482.568C1748.3 479.562 1751.72 475.533 1754.38 470.48C1757.08 465.384 1758.95 459.721 1760 453.491C1760.96 447.48 1760.98 442.035 1760.06 437.156C1759.15 432.234 1757.19 428.335 1754.18 425.46C1751.22 422.542 1747.1 421.082 1741.83 421.082C1736.34 421.082 1731.49 422.607 1727.26 425.656C1723.08 428.662 1719.66 432.713 1717 437.81C1714.35 442.863 1712.52 448.504 1711.51 454.733C1710.51 460.744 1710.47 466.189 1711.38 471.068C1712.3 475.947 1714.26 479.824 1717.26 482.699C1720.27 485.574 1724.41 487.011 1729.68 487.011ZM1791.94 504L1808.67 403.636H1831.61L1828.8 420.364H1829.84C1832.59 414.57 1836.46 410.105 1841.47 406.969C1846.53 403.789 1851.93 402.199 1857.68 402.199C1859.03 402.199 1860.49 402.264 1862.06 402.395C1863.62 402.482 1864.97 402.634 1866.11 402.852L1862.45 424.611C1861.45 424.262 1859.88 423.957 1857.74 423.696C1855.65 423.391 1853.58 423.239 1851.54 423.239C1847.22 423.239 1843.22 424.175 1839.51 426.048C1835.85 427.878 1832.76 430.426 1830.23 433.693C1827.71 436.96 1826.07 440.728 1825.33 444.997L1815.6 504H1791.94ZM1873.01 541.636C1869.79 541.636 1866.85 541.375 1864.19 540.852C1861.58 540.373 1859.51 539.807 1857.98 539.153L1866.61 520.727C1869.88 521.729 1872.82 522.165 1875.43 522.034C1878.09 521.947 1880.53 521.054 1882.75 519.355C1885.01 517.7 1887.23 514.977 1889.41 511.188L1892.42 505.764L1873.21 403.636H1897.38L1907.84 479.562H1908.88L1944.76 403.636H1970.83L1912.09 516.023C1909.25 521.468 1905.97 526.085 1902.22 529.875C1898.47 533.708 1894.18 536.627 1889.35 538.631C1884.56 540.634 1879.11 541.636 1873.01 541.636Z" fill="white"/>
-</g>
-<path d="M481.982 251C506.33 251 513.5 270.609 513.5 294.75V379.125H554.473V346.711C554.473 330.963 560.782 315.861 572.013 304.726L578.531 298.263C577.246 295.264 576.536 291.964 576.536 288.5C576.536 274.693 587.825 263.5 601.75 263.5C615.675 263.5 626.964 274.693 626.964 288.5C626.964 302.307 615.675 313.5 601.75 313.5C598.256 313.5 594.928 312.795 591.903 311.522L585.385 317.984C577.701 325.603 573.384 335.936 573.384 346.711V379.125H641.404C645.145 369.963 654.204 363.5 664.786 363.5C678.711 363.5 690 374.693 690 388.5C690 402.307 678.711 413.5 664.786 413.5C654.204 413.5 645.145 407.037 641.404 397.875H513.5V454.125H578.369C582.109 444.963 591.168 438.5 601.75 438.5C615.675 438.5 626.964 449.693 626.964 463.5C626.964 477.307 615.675 488.5 601.75 488.5C591.168 488.5 582.109 482.037 578.368 472.875H513.5V529.125H568.255C584.137 529.125 599.369 535.381 610.599 546.516L642.331 577.978C645.357 576.705 648.685 576 652.179 576C666.104 576 677.393 587.193 677.393 601C677.393 614.807 666.104 626 652.179 626C638.253 626 626.964 614.807 626.964 601C626.964 597.536 627.675 594.236 628.96 591.237L597.227 559.774C589.543 552.155 579.121 547.875 568.255 547.875H513.5V607.25C513.5 631.391 506.33 651 481.982 651C459.211 651 440.457 633.891 438.094 611.859C433.996 612.953 429.662 613.5 425.25 613.5C397.435 613.5 374.821 591.078 374.821 563.5C374.821 557.719 375.846 552.094 377.658 546.937C353.862 538.031 337 515.219 337 488.5C337 463.578 351.735 442.016 373.088 432.016C366.233 423.5 362.214 412.719 362.214 401C362.214 377.016 379.234 357.016 401.927 352.094C400.666 347.797 400.036 343.187 400.036 338.5C400.036 315.141 416.267 295.453 438.094 289.984C440.457 268.109 459.211 251 481.982 251Z" fill="white"/>
-</g>
-<defs>
-<filter id="filter0_f_2392_147" x="-3653.3" y="-2588.03" width="8538.21" height="6211.24" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
-<feFlood flood-opacity="0" result="BackgroundImageFix"/>
-<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
-<feGaussianBlur stdDeviation="440.634" result="effect1_foregroundBlur_2392_147"/>
-</filter>
-<filter id="filter1_d_2392_147" x="751.242" y="402.199" width="1219.59" height="147.438" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
-<feFlood flood-opacity="0" result="BackgroundImageFix"/>
-<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
-<feOffset dx="-8" dy="4"/>
-<feGaussianBlur stdDeviation="2"/>
-<feComposite in2="hardAlpha" operator="out"/>
-<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.41 0"/>
-<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2392_147"/>
-<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2392_147" result="shape"/>
-</filter>
-<clipPath id="clip0_2392_147">
-<rect width="2344" height="969" rx="100" fill="white"/>
-</clipPath>
-</defs>
-</svg>
diff --git a/apps/docs/images/hero-light.svg b/apps/docs/images/hero-light.svg
deleted file mode 100644
index 4d347b2a..00000000
--- a/apps/docs/images/hero-light.svg
+++ /dev/null
@@ -1,32 +0,0 @@
-<svg width="2344" height="969" viewBox="0 0 2344 969" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_2397_133)">
-<rect width="2344" height="969" rx="100" fill="white"/>
-<g filter="url(#filter0_d_2397_133)">
-<path d="M854.654 430.165L832.896 432.517C832.678 430.295 832.003 428.226 830.87 426.31C829.738 424.393 828.039 422.868 825.774 421.736C823.552 420.56 820.677 419.972 817.149 419.972C812.008 419.972 807.5 421.126 803.623 423.435C799.746 425.743 797.546 428.705 797.024 432.321C796.544 435.022 797.198 437.222 798.984 438.92C800.77 440.619 804.08 442.057 808.916 443.233L825.512 446.892C834.66 448.939 841.238 452.25 845.245 456.824C849.296 461.398 850.712 467.387 849.492 474.793C848.447 480.978 845.746 486.423 841.39 491.128C837.034 495.789 831.458 499.426 824.663 502.04C817.911 504.653 810.418 505.96 802.186 505.96C789.684 505.96 780.057 503.368 773.305 498.185C766.597 492.957 763.264 485.77 763.308 476.622L786.569 474.401C786.83 478.931 788.42 482.35 791.339 484.659C794.258 486.924 798.309 488.079 803.492 488.122C809.329 488.166 814.252 486.946 818.259 484.463C822.311 481.937 824.597 478.866 825.12 475.25C825.556 472.549 824.881 470.349 823.095 468.651C821.352 466.952 818.238 465.601 813.751 464.599L797.285 461.006C787.963 458.958 781.32 455.495 777.356 450.616C773.436 445.694 772.107 439.487 773.37 431.994C774.416 425.896 776.964 420.647 781.015 416.247C785.11 411.804 790.359 408.384 796.762 405.989C803.166 403.549 810.331 402.33 818.259 402.33C830.195 402.33 839.19 404.834 845.245 409.844C851.3 414.853 854.436 421.627 854.654 430.165ZM931.376 461.79L941.112 403.636H964.765L948.038 504H925.104L928.109 486.162H927.064C923.927 491.781 919.441 496.377 913.604 499.949C907.81 503.521 901.145 505.307 893.609 505.307C887.032 505.307 881.478 503.848 876.947 500.929C872.417 497.967 869.194 493.676 867.277 488.057C865.404 482.394 865.121 475.555 866.427 467.54L877.143 403.636H900.797L890.734 463.881C889.732 470.241 890.647 475.294 893.479 479.04C896.354 482.786 900.622 484.659 906.285 484.659C909.77 484.659 913.277 483.81 916.805 482.111C920.377 480.412 923.492 477.885 926.149 474.531C928.806 471.134 930.549 466.886 931.376 461.79ZM962.474 541.636L985.605 403.636H1008.87L1006.19 420.233H1007.56C1009.13 417.794 1011.24 415.202 1013.9 412.457C1016.6 409.67 1020.02 407.295 1024.16 405.335C1028.29 403.331 1033.33 402.33 1039.25 402.33C1046.96 402.33 1053.6 404.312 1059.18 408.276C1064.75 412.196 1068.74 418.011 1071.14 425.722C1073.58 433.388 1073.88 442.797 1072.05 453.949C1070.22 464.97 1066.85 474.335 1061.92 482.045C1057 489.756 1051.1 495.636 1044.22 499.688C1037.33 503.739 1029.95 505.764 1022.06 505.764C1016.31 505.764 1011.68 504.806 1008.15 502.889C1004.66 500.973 1002.03 498.664 1000.24 495.963C998.455 493.219 997.126 490.627 996.255 488.188H995.21L986.323 541.636H962.474ZM1000.44 453.818C999.392 460.309 999.37 465.993 1000.37 470.872C1001.42 475.751 1003.49 479.562 1006.58 482.307C1009.67 485.008 1013.72 486.358 1018.73 486.358C1023.96 486.358 1028.53 484.964 1032.45 482.176C1036.42 479.345 1039.71 475.49 1042.32 470.611C1044.98 465.688 1046.85 460.091 1047.94 453.818C1048.94 447.589 1048.94 442.057 1047.94 437.222C1046.94 432.386 1044.91 428.597 1041.86 425.852C1038.81 423.108 1034.68 421.736 1029.45 421.736C1024.35 421.736 1019.8 423.064 1015.79 425.722C1011.83 428.379 1008.54 432.103 1005.93 436.895C1003.31 441.687 1001.48 447.328 1000.44 453.818ZM1124.35 505.96C1114.29 505.96 1105.97 503.869 1099.39 499.688C1092.81 495.462 1088.2 489.494 1085.54 481.784C1082.93 474.03 1082.49 464.904 1084.23 454.406C1085.93 444.082 1089.39 435.022 1094.62 427.224C1099.85 419.384 1106.38 413.285 1114.22 408.929C1122.11 404.529 1130.82 402.33 1140.36 402.33C1146.55 402.33 1152.23 403.331 1157.41 405.335C1162.6 407.295 1166.98 410.345 1170.55 414.483C1174.12 418.621 1176.58 423.892 1177.93 430.295C1179.28 436.655 1179.22 444.235 1177.73 453.034L1176.62 460.287H1094.29L1096.84 444.344H1156.43C1157.22 439.813 1156.96 435.784 1155.65 432.256C1154.34 428.684 1152.16 425.874 1149.12 423.827C1146.07 421.779 1142.25 420.756 1137.68 420.756C1133.02 420.756 1128.66 421.954 1124.61 424.349C1120.61 426.702 1117.23 429.751 1114.48 433.497C1111.74 437.243 1110 441.186 1109.26 445.324L1106.58 460.614C1105.62 466.886 1105.84 472.005 1107.23 475.969C1108.63 479.933 1111.07 482.851 1114.55 484.724C1118.04 486.598 1122.43 487.534 1127.75 487.534C1131.23 487.534 1134.46 487.055 1137.42 486.097C1140.38 485.138 1143.02 483.701 1145.33 481.784C1147.68 479.824 1149.64 477.428 1151.21 474.597L1172.83 477.08C1170.44 482.917 1166.93 488.013 1162.31 492.369C1157.7 496.682 1152.16 500.036 1145.72 502.432C1139.31 504.784 1132.19 505.96 1124.35 505.96ZM1185.75 504L1202.48 403.636H1225.41L1222.6 420.364H1223.65C1226.39 414.57 1230.27 410.105 1235.28 406.969C1240.33 403.789 1245.73 402.199 1251.48 402.199C1252.83 402.199 1254.29 402.264 1255.86 402.395C1257.43 402.482 1258.78 402.634 1259.91 402.852L1256.25 424.611C1255.25 424.262 1253.68 423.957 1251.55 423.696C1249.46 423.391 1247.39 423.239 1245.34 423.239C1241.03 423.239 1237.02 424.175 1233.32 426.048C1229.66 427.878 1226.57 430.426 1224.04 433.693C1221.51 436.96 1219.88 440.728 1219.14 444.997L1209.4 504H1185.75ZM1255.27 504L1272 403.636H1294.67L1291.79 420.69H1293.04C1296.09 414.94 1300.29 410.454 1305.65 407.23C1311 403.963 1317.04 402.33 1323.75 402.33C1330.5 402.33 1335.94 403.985 1340.08 407.295C1344.26 410.562 1346.83 415.027 1347.79 420.69H1348.84C1351.97 415.071 1356.46 410.606 1362.3 407.295C1368.18 403.985 1374.78 402.33 1382.1 402.33C1391.24 402.33 1398.26 405.248 1403.14 411.085C1408.01 416.922 1409.52 425.438 1407.64 436.634L1396.41 504H1372.75L1383.47 440.293C1384.43 434.063 1383.47 429.511 1380.59 426.636C1377.76 423.718 1373.95 422.259 1369.16 422.259C1363.45 422.259 1358.7 424.023 1354.91 427.551C1351.17 431.08 1348.86 435.697 1347.99 441.403L1337.47 504H1314.27L1325.12 439.312C1325.9 434.129 1324.99 429.991 1322.37 426.898C1319.8 423.805 1316.01 422.259 1311 422.259C1307.61 422.259 1304.36 423.13 1301.27 424.872C1298.18 426.615 1295.54 429.054 1293.36 432.19C1291.23 435.283 1289.83 438.877 1289.18 442.972L1278.99 504H1255.27ZM1460.83 505.96C1450.76 505.96 1442.44 503.869 1435.87 499.688C1429.29 495.462 1424.67 489.494 1422.01 481.784C1419.4 474.03 1418.97 464.904 1420.71 454.406C1422.41 444.082 1425.87 435.022 1431.1 427.224C1436.32 419.384 1442.86 413.285 1450.7 408.929C1458.58 404.529 1467.3 402.33 1476.84 402.33C1483.02 402.33 1488.71 403.331 1493.89 405.335C1499.07 407.295 1503.45 410.345 1507.02 414.483C1510.6 418.621 1513.06 423.892 1514.41 430.295C1515.76 436.655 1515.69 444.235 1514.21 453.034L1513.1 460.287H1430.77L1433.32 444.344H1492.91C1493.69 439.813 1493.43 435.784 1492.13 432.256C1490.82 428.684 1488.64 425.874 1485.59 423.827C1482.54 421.779 1478.73 420.756 1474.16 420.756C1469.5 420.756 1465.14 421.954 1461.09 424.349C1457.08 426.702 1453.71 429.751 1450.96 433.497C1448.22 437.243 1446.47 441.186 1445.73 445.324L1443.05 460.614C1442.1 466.886 1442.31 472.005 1443.71 475.969C1445.1 479.933 1447.54 482.851 1451.03 484.724C1454.51 486.598 1458.91 487.534 1464.22 487.534C1467.71 487.534 1470.93 487.055 1473.9 486.097C1476.86 485.138 1479.49 483.701 1481.8 481.784C1484.15 479.824 1486.11 477.428 1487.68 474.597L1509.31 477.08C1506.91 482.917 1503.41 488.013 1498.79 492.369C1494.17 496.682 1488.64 500.036 1482.19 502.432C1475.79 504.784 1468.67 505.96 1460.83 505.96ZM1522.29 504L1539.02 403.636H1561.69L1558.82 420.69H1560.06C1563.11 414.94 1567.31 410.454 1572.67 407.23C1578.03 403.963 1584.06 402.33 1590.77 402.33C1597.52 402.33 1602.97 403.985 1607.1 407.295C1611.29 410.562 1613.86 415.027 1614.81 420.69H1615.86C1619 415.071 1623.48 410.606 1629.32 407.295C1635.2 403.985 1641.8 402.33 1649.12 402.33C1658.27 402.33 1665.28 405.248 1670.16 411.085C1675.04 416.922 1676.54 425.438 1674.67 436.634L1663.43 504H1639.77L1650.49 440.293C1651.45 434.063 1650.49 429.511 1647.62 426.636C1644.78 423.718 1640.97 422.259 1636.18 422.259C1630.47 422.259 1625.73 424.023 1621.94 427.551C1618.19 431.08 1615.88 435.697 1615.01 441.403L1604.49 504H1581.29L1592.14 439.312C1592.93 434.129 1592.01 429.991 1589.4 426.898C1586.83 423.805 1583.04 422.259 1578.03 422.259C1574.63 422.259 1571.38 423.13 1568.29 424.872C1565.2 426.615 1562.56 429.054 1560.39 432.19C1558.25 435.283 1556.86 438.877 1556.2 442.972L1546.01 504H1522.29ZM1727.72 505.96C1717.83 505.96 1709.6 503.782 1703.02 499.426C1696.49 495.07 1691.89 488.95 1689.23 481.065C1686.58 473.181 1686.1 464.033 1687.8 453.622C1689.49 443.342 1692.89 434.347 1697.99 426.636C1703.13 418.926 1709.6 412.958 1717.4 408.733C1725.24 404.464 1734.01 402.33 1743.73 402.33C1753.62 402.33 1761.83 404.529 1768.36 408.929C1774.9 413.285 1779.49 419.405 1782.15 427.29C1784.85 435.174 1785.35 444.366 1783.65 454.864C1782 465.1 1778.58 474.052 1773.39 481.719C1768.25 489.385 1761.78 495.353 1753.99 499.622C1746.19 503.848 1737.43 505.96 1727.72 505.96ZM1729.68 487.011C1735.17 487.011 1739.98 485.53 1744.12 482.568C1748.3 479.562 1751.72 475.533 1754.38 470.48C1757.08 465.384 1758.95 459.721 1760 453.491C1760.96 447.48 1760.98 442.035 1760.06 437.156C1759.15 432.234 1757.19 428.335 1754.18 425.46C1751.22 422.542 1747.1 421.082 1741.83 421.082C1736.34 421.082 1731.49 422.607 1727.26 425.656C1723.08 428.662 1719.66 432.713 1717 437.81C1714.35 442.863 1712.52 448.504 1711.51 454.733C1710.51 460.744 1710.47 466.189 1711.38 471.068C1712.3 475.947 1714.26 479.824 1717.26 482.699C1720.27 485.574 1724.41 487.011 1729.68 487.011ZM1791.94 504L1808.67 403.636H1831.61L1828.8 420.364H1829.84C1832.59 414.57 1836.46 410.105 1841.47 406.969C1846.53 403.789 1851.93 402.199 1857.68 402.199C1859.03 402.199 1860.49 402.264 1862.06 402.395C1863.62 402.482 1864.97 402.634 1866.11 402.852L1862.45 424.611C1861.45 424.262 1859.88 423.957 1857.74 423.696C1855.65 423.391 1853.58 423.239 1851.54 423.239C1847.22 423.239 1843.22 424.175 1839.51 426.048C1835.85 427.878 1832.76 430.426 1830.23 433.693C1827.71 436.96 1826.07 440.728 1825.33 444.997L1815.6 504H1791.94ZM1873.01 541.636C1869.79 541.636 1866.85 541.375 1864.19 540.852C1861.58 540.373 1859.51 539.807 1857.98 539.153L1866.61 520.727C1869.88 521.729 1872.82 522.165 1875.43 522.034C1878.09 521.947 1880.53 521.054 1882.75 519.355C1885.01 517.7 1887.23 514.977 1889.41 511.188L1892.42 505.764L1873.21 403.636H1897.38L1907.84 479.562H1908.88L1944.76 403.636H1970.83L1912.09 516.023C1909.25 521.468 1905.97 526.085 1902.22 529.875C1898.47 533.708 1894.18 536.627 1889.35 538.631C1884.56 540.634 1879.11 541.636 1873.01 541.636Z" fill="black"/>
-</g>
-<path d="M481.982 251C506.33 251 513.5 270.609 513.5 294.75V379.125H554.473V346.711C554.473 330.963 560.782 315.861 572.013 304.726L578.531 298.263C577.246 295.264 576.536 291.964 576.536 288.5C576.536 274.693 587.825 263.5 601.75 263.5C615.675 263.5 626.964 274.693 626.964 288.5C626.964 302.307 615.675 313.5 601.75 313.5C598.256 313.5 594.928 312.795 591.903 311.522L585.385 317.984C577.701 325.603 573.384 335.936 573.384 346.711V379.125H641.404C645.145 369.963 654.204 363.5 664.786 363.5C678.711 363.5 690 374.693 690 388.5C690 402.307 678.711 413.5 664.786 413.5C654.204 413.5 645.145 407.037 641.404 397.875H513.5V454.125H578.369C582.109 444.963 591.168 438.5 601.75 438.5C615.675 438.5 626.964 449.693 626.964 463.5C626.964 477.307 615.675 488.5 601.75 488.5C591.168 488.5 582.109 482.037 578.368 472.875H513.5V529.125H568.255C584.137 529.125 599.369 535.381 610.599 546.516L642.331 577.978C645.357 576.705 648.685 576 652.179 576C666.104 576 677.393 587.193 677.393 601C677.393 614.807 666.104 626 652.179 626C638.253 626 626.964 614.807 626.964 601C626.964 597.536 627.675 594.236 628.96 591.237L597.227 559.774C589.543 552.155 579.121 547.875 568.255 547.875H513.5V607.25C513.5 631.391 506.33 651 481.982 651C459.21 651 440.457 633.891 438.094 611.859C433.996 612.953 429.662 613.5 425.25 613.5C397.435 613.5 374.821 591.078 374.821 563.5C374.821 557.719 375.846 552.094 377.658 546.937C353.862 538.031 337 515.219 337 488.5C337 463.578 351.735 442.016 373.088 432.016C366.233 423.5 362.214 412.719 362.214 401C362.214 377.016 379.234 357.016 401.927 352.094C400.666 347.797 400.036 343.187 400.036 338.5C400.036 315.141 416.267 295.453 438.094 289.984C440.457 268.109 459.21 251 481.982 251Z" fill="black"/>
-<g opacity="0.4" filter="url(#filter1_f_2397_133)">
-<rect x="-3176" y="2574.55" width="7287.1" height="929.709" transform="rotate(-30 -3176 2574.55)" fill="#369DFD"/>
-</g>
-</g>
-<defs>
-<filter id="filter0_d_2397_133" x="751.242" y="402.199" width="1219.59" height="147.438" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
-<feFlood flood-opacity="0" result="BackgroundImageFix"/>
-<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
-<feOffset dx="-8" dy="4"/>
-<feGaussianBlur stdDeviation="2"/>
-<feComposite in2="hardAlpha" operator="out"/>
-<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.41 0"/>
-<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2397_133"/>
-<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2397_133" result="shape"/>
-</filter>
-<filter id="filter1_f_2397_133" x="-4057.27" y="-1950.27" width="8538.21" height="6211.24" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
-<feFlood flood-opacity="0" result="BackgroundImageFix"/>
-<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
-<feGaussianBlur stdDeviation="440.634" result="effect1_foregroundBlur_2397_133"/>
-</filter>
-<clipPath id="clip0_2397_133">
-<rect width="2344" height="969" rx="100" fill="white"/>
-</clipPath>
-</defs>
-</svg>
diff --git a/apps/docs/images/setup/1.png b/apps/docs/images/setup/1.png
deleted file mode 100644
index aba55dc7..00000000
--- a/apps/docs/images/setup/1.png
+++ /dev/null
Binary files differ
diff --git a/apps/docs/images/setup/2.png b/apps/docs/images/setup/2.png
deleted file mode 100644
index 32f195cb..00000000
--- a/apps/docs/images/setup/2.png
+++ /dev/null
Binary files differ
diff --git a/apps/docs/images/setup/3.png b/apps/docs/images/setup/3.png
deleted file mode 100644
index a91ecaa4..00000000
--- a/apps/docs/images/setup/3.png
+++ /dev/null
Binary files differ
diff --git a/apps/docs/introduction.mdx b/apps/docs/introduction.mdx
deleted file mode 100644
index 1d804825..00000000
--- a/apps/docs/introduction.mdx
+++ /dev/null
@@ -1,156 +0,0 @@
----
-title: "Introduction"
-description: "Supermemory is the Memory API for the AI era."
----
-
-We built [Supermemory](https://supermemory.ai) and scaled the RAG system to 10,000,000\+ documents and multiple thousands of users.
-We faced challenges. It turns out that building scalable, reliable, production-ready Memory layer is pretty hard.
-
-Introducing the Supermemory API. An _affordable_, _easy-to-use_, and _production-ready_ Memory API for the AI era.
-
-<img
- className="block dark:hidden"
- src="/images/hero-light.svg"
- alt="Hero Light"
-/>
-
-<img
- className="hidden dark:block"
- src="/images/hero-dark.svg"
- alt="Hero Dark"
-/>
-
-Trusted by Open source [9k\+ stars](https://git.new/memory), one of the fastest [growing projects in Q3 2024](https://runacap.com/ross-index/q3-2024/), Product of the day on [ProductHunt](https://www.producthunt.com/posts/supermemory).
-
-...and thousands of you\!
-
-## Why Supermemory?
-
-### The problem
-
-... so you want to build your own memory layer. Let's go through your decision process:
-
-<Steps>
- <Step title="Let's choose a vector database">
- <Warning>
- Found a vector database? good luck
-
- - Oh no, it's way too expensive. Time to switch.
- - Turns out it's painfully slow. Let's try another.
- - Great, now it won't scale. Back to square one.
- - The maintenance is a nightmare. Need something else.
- </Warning>
- </Step>
- <Step title="Now for the embedding model">
- <Note>
- Which one to choose? Unless you have a PhD in AI, good luck figuring out:
-
- - Which model fits your use case
- - What are the performance tradeoffs
- - How to keep up with new releases
- </Note>
- </Step>
- <Step title="Time to build the memory layer">
- <CardGroup cols="2">
- <Card title="Support multimodal">
- - Websites: How do you handle JavaScript? What about rate limits?
- - PDFs: OCR keeps failing, text extraction is inconsistent
- - Images: Need computer vision models now?
- - Audio/Video: Transcription costs add up quickly
- </Card>
- <Card title="Handle everything">
- - Multiple languages: Different models for each?
- - Various formats to parse: \
- • Markdown: Tables break everything \
- • HTML: Scripts and styles get in the way \
- • PDF: Layout ruins the extraction \
- • Word docs: Good luck with formatting \
- • And somehow make it all work together...
- </Card>
- </CardGroup>
- </Step>
-</Steps>
-
-And in the middle of all this, you're wondering...
-
-> "When will I actually ship my product?"
-
-### The solution
-
-If you are not a fan of reinventing the wheel, you can use Supermemory.
-
-<CardGroup cols="2">
- <Card title="Affordable & Easy to Use" icon="circle-check">
- <div className="text-emerald-700 space-y-1">
- - Start for free, scale as you grow - Simple API, deploy in minutes - No
- complex setup or maintenance - Clear, predictable pricing
-
- </div>
- </Card>
- <Card title="Ready-made Connectors" icon="circle-check">
- <div className="text-emerald-700 space-y-1">
- - Notion, Google Drive, Slack integration - Web scraping and PDF
- processing - Email and calendar sync - Custom connector SDK
-
- </div>
- </Card>
- <Card title="Production Ready" icon="circle-check">
- <div className="text-emerald-700 space-y-1">
- - Enterprise-grade security - Sub-200ms latency at scale - Automatic
- failover and redundancy - 99.9% uptime guarantee
-
- </div>
- </Card>
- <Card title="Open Source & Trusted" icon="circle-check">
- <div className="text-emerald-700 space-y-1">
- - Open source core - Active community - Regular security audits -
- Transparent development
-
- </div>
- </Card>
-</CardGroup>
-
-Stop reinventing the wheel. Focus on building your product while we handle the memory infrastructure.
-
-## Use cases
-
-What can you do with Supermemory?
-
-<CardGroup cols="2">
- <Card title="Chat with <X> app" icon="message">
- Quickly built chat apps like:
-
- • Chat with your Twitter bookmarks \
- • Interact with your PDF documents \
- • Chat with your company documentation \
- • Chat with your personal knowledge base
- ... and more\\!
- </Card>
- <Card title="Smart search in your apps" icon="magnifying-glass">
- Search things with AI:
-
- • Product recommendations \
- • Knowledge base search \
- • Document similarity matching \
- • Content discovery systems \
- • Research paper analysis
- </Card>
- <Card title="Assistants and Agents" icon="chart-line">
- Assistants and Agents:
-
- • Email management \
- • Meeting summarization \
- • Task prioritization \
- • Calendar organization \
- • Personal knowledge management
- </Card>
- <Card title="Import tools and integrations" icon="toolbox">
- You can contribute to supermemory by making community import tools. Examples:
-
- • Notion \
- • IOS shortcuts
-
- \
- • YOUR app / service
- </Card>
-</CardGroup> \ No newline at end of file
diff --git a/apps/docs/logo/dark.svg b/apps/docs/logo/dark.svg
deleted file mode 100644
index 513d7685..00000000
--- a/apps/docs/logo/dark.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-<svg width="992" height="1120" viewBox="0 0 992 1120" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M407.233 0C475.621 0 495.762 54.8799 495.762 122.441V358.578H610.849V267.861C610.849 223.79 628.571 181.524 660.115 150.361L678.424 132.274C674.815 123.879 672.819 114.645 672.819 104.95C672.819 66.3082 704.528 34.9832 743.642 34.9832C782.757 34.9832 814.465 66.3082 814.465 104.95C814.465 143.591 782.757 174.916 743.642 174.916C733.828 174.916 724.481 172.944 715.983 169.379L697.675 187.466C676.091 208.788 663.966 237.707 663.966 267.861V358.578H855.025C865.531 332.936 890.977 314.849 920.7 314.849C959.814 314.849 991.523 346.174 991.523 384.815C991.523 423.456 959.814 454.781 920.7 454.781C890.977 454.781 865.531 436.693 855.025 411.052H495.762V568.477H677.967C688.474 542.835 713.92 524.748 743.642 524.748C782.757 524.748 814.465 556.073 814.465 594.714C814.465 633.355 782.757 664.68 743.642 664.68C713.92 664.68 688.474 646.593 677.967 620.951H495.762V778.376H649.559C694.17 778.376 736.953 795.883 768.498 827.046L857.629 915.099C866.127 911.534 875.475 909.562 885.288 909.562C924.403 909.562 956.111 940.887 956.111 979.529C956.111 1018.17 924.403 1049.5 885.288 1049.5C846.174 1049.5 814.465 1018.17 814.465 979.529C814.465 969.834 816.461 960.599 820.07 952.204L730.938 864.151C709.355 842.829 680.082 830.85 649.559 830.85H495.762V997.02C495.762 1064.58 475.621 1119.46 407.233 1119.46C343.271 1119.46 290.596 1071.58 283.956 1009.92C272.448 1012.98 260.275 1014.51 247.881 1014.51C169.754 1014.51 106.235 951.761 106.235 874.579C106.235 858.4 109.112 842.657 114.202 828.227C47.3629 803.301 0 739.457 0 664.68C0 594.933 41.3872 534.587 101.366 506.6C82.1105 482.768 70.8231 452.595 70.8231 419.798C70.8231 352.674 118.629 296.701 182.369 282.926C178.828 270.901 177.058 258.001 177.058 244.882C177.058 179.507 222.65 124.409 283.956 109.104C290.596 47.8832 343.271 0 407.233 0Z" fill="white" fill-opacity="0.8"/>
-</svg>
diff --git a/apps/docs/logo/light.svg b/apps/docs/logo/light.svg
deleted file mode 100644
index 96ae2b7b..00000000
--- a/apps/docs/logo/light.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-<svg width="992" height="1120" viewBox="0 0 992 1120" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M407.233 0C475.621 0 495.762 54.8799 495.762 122.441V358.578H610.849V267.861C610.849 223.79 628.571 181.524 660.115 150.361L678.424 132.274C674.815 123.879 672.819 114.645 672.819 104.95C672.819 66.3082 704.528 34.9832 743.642 34.9832C782.757 34.9832 814.465 66.3082 814.465 104.95C814.465 143.591 782.757 174.916 743.642 174.916C733.828 174.916 724.481 172.944 715.983 169.379L697.675 187.466C676.091 208.788 663.966 237.707 663.966 267.861V358.578H855.025C865.531 332.936 890.977 314.849 920.7 314.849C959.814 314.849 991.523 346.174 991.523 384.815C991.523 423.456 959.814 454.781 920.7 454.781C890.977 454.781 865.531 436.693 855.025 411.052H495.762V568.477H677.967C688.474 542.835 713.92 524.748 743.642 524.748C782.757 524.748 814.465 556.073 814.465 594.714C814.465 633.355 782.757 664.68 743.642 664.68C713.92 664.68 688.474 646.593 677.967 620.951H495.762V778.376H649.559C694.17 778.376 736.953 795.883 768.498 827.046L857.629 915.099C866.127 911.534 875.475 909.562 885.288 909.562C924.403 909.562 956.111 940.887 956.111 979.529C956.111 1018.17 924.403 1049.5 885.288 1049.5C846.174 1049.5 814.465 1018.17 814.465 979.529C814.465 969.834 816.461 960.599 820.07 952.204L730.938 864.151C709.355 842.829 680.082 830.85 649.559 830.85H495.762V997.02C495.762 1064.58 475.621 1119.46 407.233 1119.46C343.271 1119.46 290.596 1071.58 283.956 1009.92C272.448 1012.98 260.275 1014.51 247.881 1014.51C169.754 1014.51 106.235 951.761 106.235 874.579C106.235 858.4 109.112 842.657 114.202 828.227C47.3629 803.301 0 739.457 0 664.68C0 594.933 41.3872 534.587 101.366 506.6C82.1105 482.768 70.8231 452.595 70.8231 419.798C70.8231 352.674 118.629 296.701 182.369 282.926C178.828 270.901 177.058 258.001 177.058 244.882C177.058 179.507 222.65 124.409 283.956 109.104C290.596 47.8832 343.271 0 407.233 0Z" fill="black" fill-opacity="0.8"/>
-</svg>
diff --git a/apps/docs/mint.json b/apps/docs/mint.json
deleted file mode 100644
index bb47ae72..00000000
--- a/apps/docs/mint.json
+++ /dev/null
@@ -1,75 +0,0 @@
-{
- "$schema": "https://mintlify.com/schema.json",
- "name": "Supermemory | Memory API for the AI era",
- "logo": {
- "dark": "/logo/dark.svg",
- "light": "/logo/light.svg"
- },
- "favicon": "/favicon.png",
- "colors": {
- "primary": "#1E3A8A",
- "light": "#3B82F6",
- "dark": "#1E3A8A",
- "anchors": {
- "from": "#1E3A8A",
- "to": "#3B82F6"
- }
- },
- "topbarLinks": [
- {
- "name": "Support",
- "url": "mailto:[email protected]"
- }
- ],
- "topbarCtaButton": {
- "name": "Dashboard",
- "url": "https://supermemory.ai"
- },
- "tabs": [
- {
- "name": "API Reference",
- "url": "api-reference/endpoints",
- "openapi": "/openapi.json"
- },
- {
- "name": "Changelog",
- "url": "changelog/overview"
- }
- ],
- "anchors": [
- {
- "name": "Github",
- "icon": "github",
- "url": "https://git.new/memory"
- },
- {
- "name": "Ask the founder",
- "icon": "mail",
- "url": "mailto:[email protected]"
- },
- {
- "name": "X (Twitter)",
- "icon": "twitter",
- "url": "https://x.com/supermemoryai"
- }
- ],
- "navigation": [
- {
- "group": "Get Started (5 mins)",
- "pages": ["introduction", "quickstart", "self-hosting"]
- },
- {
- "group": "Essentials",
- "pages": ["essentials/metadata-filtering", "essentials/pricing"]
- },
- {
- "group": "Changelog",
- "pages": ["changelog/overview"]
- }
- ],
- "footerSocials": {
- "x": "https://x.com/supermemoryai",
- "github": "https://github.com/supermemoryai",
- "linkedin": "https://linkedin.com/company/supermemoryai"
- }
-}
diff --git a/apps/docs/openapi.json b/apps/docs/openapi.json
deleted file mode 100644
index 1ac21936..00000000
--- a/apps/docs/openapi.json
+++ /dev/null
@@ -1,625 +0,0 @@
-{
- "openapi": "3.1.0",
- "info": {
- "title": "Supermemory API",
- "description": "best memory api on 🌍",
- "version": "2.0.0"
- },
- "servers": [
- {
- "url": "https://v2.api.supermemory.ai",
- "description": "Production Server"
- }
- ],
- "paths": {
- "/settings": {
- "put": {
- "responses": {},
- "operationId": "putSettings",
- "parameters": [],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "shouldLLMFilter": {
- "type": "boolean"
- },
- "categories": {
- "type": "array",
- "items": {
- "type": "string",
- "minLength": 1,
- "maxLength": 50
- }
- },
- "filterPrompt": {
- "type": "string",
- "minLength": 1,
- "maxLength": 750
- },
- "includeItems": {
- "type": "array",
- "items": {
- "type": "string",
- "minLength": 1,
- "maxLength": 20
- }
- },
- "excludeItems": {
- "type": "array",
- "items": {
- "type": "string",
- "minLength": 1,
- "maxLength": 20
- }
- },
- "filterTags": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "tag": {
- "type": "string",
- "minLength": 1,
- "maxLength": 50
- },
- "score": {
- "type": "number",
- "minimum": 0
- }
- },
- "required": ["tag", "score"]
- }
- }
- }
- }
- }
- }
- }
- }
- },
- "/add": {
- "post": {
- "responses": {
- "200": {
- "description": "Memory added successfully",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "status": {
- "type": "string"
- }
- },
- "required": ["id", "status"]
- }
- }
- }
- },
- "401": {
- "description": "Unauthorized",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "error": {
- "type": "string"
- },
- "details": {
- "type": "string"
- }
- },
- "required": ["error"]
- }
- }
- }
- }
- },
- "operationId": "postAdd",
- "tags": ["Memory Management"],
- "parameters": [],
- "description": "Add a new memory with content and metadata",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "content": {
- "type": "string"
- },
- "metadata": {
- "type": "object",
- "additionalProperties": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "number"
- },
- {
- "type": "boolean"
- }
- ]
- }
- },
- "spaceIds": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "summarizeMemories": {
- "type": "boolean",
- "default": false
- },
- "id": {
- "type": "string"
- }
- },
- "required": ["content"]
- }
- }
- }
- }
- }
- },
- "/update/{id}": {
- "put": {
- "responses": {
- "200": {
- "description": "Memory updated successfully",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "status": {
- "type": "string"
- }
- },
- "required": ["id", "status"]
- }
- }
- }
- },
- "404": {
- "description": "Memory not found",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "error": {
- "type": "string"
- },
- "details": {
- "type": "string"
- }
- },
- "required": ["error"]
- }
- }
- }
- }
- },
- "operationId": "putUpdateById",
- "tags": ["Memory Management"],
- "parameters": [
- {
- "in": "path",
- "name": "id",
- "schema": {
- "type": "string"
- },
- "required": true
- }
- ],
- "description": "Update an existing memory",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "content": {
- "type": "string"
- },
- "metadata": {
- "type": "object",
- "additionalProperties": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "number"
- },
- {
- "type": "boolean"
- }
- ]
- }
- },
- "spaceIds": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "summarizeMemories": {
- "type": "boolean",
- "default": false
- }
- },
- "required": ["content"]
- }
- }
- }
- }
- }
- },
- "/delete/{id}": {
- "delete": {
- "responses": {
- "200": {
- "description": "Memory deleted successfully",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "success": {
- "type": "boolean"
- }
- },
- "required": ["success"]
- }
- }
- }
- },
- "404": {
- "description": "Memory not found",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "error": {
- "type": "string"
- },
- "details": {
- "type": "string"
- }
- },
- "required": ["error"]
- }
- }
- }
- }
- },
- "operationId": "deleteDeleteById",
- "tags": ["Memory Management"],
- "parameters": [
- {
- "in": "path",
- "name": "id",
- "schema": {
- "type": "string"
- },
- "required": true
- }
- ],
- "description": "Delete a memory"
- }
- },
- "/search": {
- "post": {
- "responses": {
- "200": {
- "description": "Search results",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "results": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "documentId": {
- "type": "string"
- },
- "chunks": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "content": {
- "type": "string"
- },
- "isRelevant": {
- "type": "boolean"
- }
- },
- "required": ["content", "isRelevant"]
- }
- },
- "score": {
- "type": "number"
- },
- "documentSummary": {
- "type": "string"
- },
- "metadata": {
- "type": "object",
- "additionalProperties": {}
- },
- "title": {
- "type": "string"
- },
- "createdAt": {
- "type": "string"
- },
- "updatedAt": {
- "type": "string"
- }
- },
- "required": [
- "documentId",
- "chunks",
- "score",
- "documentSummary",
- "metadata",
- "title",
- "createdAt",
- "updatedAt"
- ]
- }
- }
- },
- "required": ["results"]
- }
- }
- }
- },
- "400": {
- "description": "Invalid request parameters",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "error": {
- "type": "string"
- },
- "details": {
- "type": "string"
- }
- },
- "required": ["error"]
- }
- }
- }
- },
- "500": {
- "description": "Server error",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "error": {
- "type": "string"
- },
- "details": {
- "type": "string"
- }
- },
- "required": ["error"]
- }
- }
- }
- }
- },
- "operationId": "postSearch",
- "tags": ["Search"],
- "parameters": [],
- "description": "Search through documents with metadata filtering",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "q": {
- "type": "string",
- "minLength": 1
- },
- "limit": {
- "type": "integer",
- "exclusiveMinimum": 0,
- "default": 10
- },
- "filters": {
- "anyOf": [
- {
- "type": "object",
- "properties": {
- "OR": {
- "type": "array",
- "items": {}
- },
- "AND": {
- "type": "array",
- "items": {}
- }
- }
- },
- {
- "type": "object",
- "additionalProperties": {}
- }
- ]
- },
- "categoriesFilter": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "required": ["q"]
- }
- }
- }
- }
- }
- },
- "/fastsearch": {
- "get": {
- "responses": {
- "200": {
- "description": "Fast search results",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "results": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "content": {
- "type": "string"
- },
- "similarity": {
- "type": "number"
- }
- },
- "required": ["id", "content", "similarity"]
- }
- }
- },
- "required": ["results"]
- }
- }
- }
- },
- "500": {
- "description": "Server error",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "error": {
- "type": "string"
- },
- "details": {
- "type": "string"
- }
- },
- "required": ["error"]
- }
- }
- }
- }
- },
- "operationId": "getFastsearch",
- "tags": ["Search"],
- "parameters": [
- {
- "in": "query",
- "name": "q",
- "schema": {
- "type": "string",
- "minLength": 1
- },
- "required": true
- },
- {
- "in": "query",
- "name": "limit",
- "schema": {
- "type": "string",
- "pattern": "^\\d+$"
- },
- "required": false
- }
- ],
- "description": "Fast, lossy search using quantized embeddings"
- }
- },
- "/connect/{app}": {
- "get": {
- "responses": {},
- "operationId": "getConnectByApp",
- "tags": ["Connect"],
- "parameters": [
- {
- "in": "path",
- "name": "app",
- "schema": {
- "type": "string",
- "enum": ["notion"]
- },
- "required": true
- },
- {
- "in": "query",
- "name": "id",
- "schema": {
- "type": "string"
- },
- "required": true
- },
- {
- "in": "query",
- "name": "redirectUrl",
- "schema": {
- "type": "string"
- },
- "required": false
- }
- ]
- }
- },
- "/connections/{connectionId}": {
- "get": {
- "responses": {},
- "operationId": "getConnectionsByConnectionId",
- "tags": ["Connect"],
- "parameters": [
- {
- "in": "path",
- "name": "connectionId",
- "schema": {
- "type": "string"
- },
- "required": true
- }
- ]
- }
- }
- },
- "components": {
- "schemas": {}
- }
-}
diff --git a/apps/docs/quickstart.mdx b/apps/docs/quickstart.mdx
deleted file mode 100644
index bf7b2f6b..00000000
--- a/apps/docs/quickstart.mdx
+++ /dev/null
@@ -1,59 +0,0 @@
----
-title: "Get Started (5 mins)"
-description: "Start using Supermemory API in under 5 minutes"
----
-
-To use the Supermemory API, you'll need:
-
-1. An API key (get one by signing up at https://dev.supermemory.ai )
-2. Basic understanding of REST APIs
-3. A tool to make HTTP requests (like curl, Postman, or your favorite programming language)
-
-<AccordionGroup>
- <Accordion title="Getting Your API Key" icon="key">
- 1. Login into https://dev.supermemory.ai and create an organization
- 2. Create an api key, copy and save it securely.
-
- ![image.png](/image.png)
-
- Keep your API key secure and never share it publicly. You'll need this key for authenticating all API requests.
- </Accordion>
-</AccordionGroup>
-
-For even starter quickstart, you can use our API playground here - [API Playground](/api-reference/endpoints/add-new-content)
-
-You can also browse the postman collection - [Postman Collection](https://documenter.getpostman.com/view/21641015/2sB2cUBhvq#cf679c90-d60b-4d0d-a7aa-a909e97e41c3)
-
-## Base URL
-
-All API requests should be made to:
-
-```
-https://v2.api.supermemory.ai
-```
-
-## Add your first memory
-
-```bash
-curl -X POST https://v2.api.supermemory.ai/add \
- -H "x-api-key: YOUR_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{"content": "This is the content of my first memory."}'
-```
-
-This will add a new memory to your Supermemory account.
-
-Try it out in the [API Playground](/api-reference/endpoints/add-new-content)
-
-## Search your memories
-
-```bash
-curl -X POST https://v2.api.supermemory.ai/search \
- -H "x-api-key: YOUR_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{"q": "This is the content of my first memory."}'
-```
-
-Try it out in the [API Playground](/api-reference/endpoints/search-content)
-
-That's it\\! You've now added your first memory and searched for it. \ No newline at end of file
diff --git a/apps/docs/self-hosting.mdx b/apps/docs/self-hosting.mdx
deleted file mode 100644
index efac8170..00000000
--- a/apps/docs/self-hosting.mdx
+++ /dev/null
@@ -1,94 +0,0 @@
----
-title: "Self-hosting"
-description: "Self-host Supermemory on your own infrastructure"
----
-
-## Local Setup
-
-<AccordionGroup>
- <Accordion icon="github" title="Clone the repository">
- ```bash
- git clone https://github.com/supermemoryai/supermemory.git
- cd supermemory
- npm i -g bun
- bun install
- ```
- </Accordion>
-
- <Accordion icon="database" title="Database Setup">
- To spin up the database locally, use Docker Compose:
-
- ```bash
- docker-compose up -d
- ```
-
- This will start a PostgreSQL database with pgvector extension at `localhost:5432`.
-
- To generate and apply migrations:
- ```bash
- bun run generate-migration
- bun run migrate:local
- ```
-
- > Note: When coding or making changes, you MUST use the drizzle-orm functions exported from `packages/db` for interacting with the database. Not using them will cause type errors that are hard to debug.
- </Accordion>
-
- <Accordion icon="gear" title="Environment Variables">
- #### Backend (`apps/backend/.env` and `apps/backend/.dev.vars`):
-
- ```env
- WORKOS_API_KEY=your_workos_api_key
- WORKOS_CLIENT_ID=your_workos_client_id
- WORKOS_COOKIE_PASSWORD=your_cookie_password
- DATABASE_URL="postgresql://postgres:postgres@localhost:5432/supermemorylocal"
- CONTENT_WORKFLOW=your_content_workflow
- GEMINI_API_KEY=your_gemini_api_key
- NODE_ENV=development
- OPEN_AI_API_KEY=your_openai_api_key
- BRAINTRUST_API_KEY=your_braintrust_api_key
- RESEND_API_KEY=your_resend_api_key
- TURNSTILE_SECRET_KEY=your_turnstile_secret_key
- ```
-
- #### Web (`apps/web/.env` and `apps/web/.dev.vars`):
-
- ```env
- WORKOS_CLIENT_ID=your_workos_client_id
- WORKOS_API_KEY=your_workos_api_key
- WORKOS_REDIRECT_URI="http://localhost:3000/callback"
- WORKOS_COOKIE_PASSWORD=your_cookie_password
- DATABASE_URL="postgresql://postgres:postgres@localhost:5432/supermemorylocal"
-
- CLOUDFLARE_ACCOUNT_ID=your_cloudflare_account_id
- R2_ACCESS_KEY_ID=your_r2_access_key_id
- R2_SECRET_ACCESS_KEY=your_r2_secret_access_key
-
- BACKEND_URL=http://localhost:8787
- OPENAI_API_KEY=your_openai_api_key
- NOTION_CLIENT_ID=your_notion_client_id
- NOTION_CLIENT_SECRET=your_notion_client_secret
-
- NODE_ENV=development
- STRIPE_CHECKOUT_KEY=your_stripe_checkout_key
- STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret
- ```
-
- You also need to update the Wrangler config for the web app and backend to your own account's resources on Cloudflare.
- </Accordion>
-
- <Accordion icon="code" title="Schema Changes">
- To edit the database schema, modify the files in `packages/db/schema.ts`, and then repeat the migration steps from the Database Setup section.
- </Accordion>
-
- <Accordion icon="rocket" title="Running the Application">
- 1. Install dependencies:
- ```bash
- bun install
- ```
-
- 2. Start the development servers:
- ```bash
- bun run dev
- ```
- </Accordion>
-</AccordionGroup>
diff --git a/apps/docs/snippets/snippet-intro.mdx b/apps/docs/snippets/snippet-intro.mdx
deleted file mode 100644
index 32f95c0d..00000000
--- a/apps/docs/snippets/snippet-intro.mdx
+++ /dev/null
@@ -1 +0,0 @@
-hi \ No newline at end of file
diff --git a/apps/extension/.gitignore b/apps/extension/.gitignore
deleted file mode 100644
index 219b3b74..00000000
--- a/apps/extension/.gitignore
+++ /dev/null
@@ -1,27 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-node_modules
-
-# testing
-coverage
-
-# production
-dist
-
-# misc
-.DS_Store
-
-# local env files
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
-
-# debug files
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# extension.js
-extension-env.d.ts \ No newline at end of file
diff --git a/apps/extension/README.md b/apps/extension/README.md
deleted file mode 100644
index a019a77d..00000000
--- a/apps/extension/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-Extension for Supermemory
-
-helps in importing twitter bookmarks / chrome bookmarks / current tab content into Supermemory \ No newline at end of file
diff --git a/apps/extension/components.json b/apps/extension/components.json
deleted file mode 100644
index 7769c9a4..00000000
--- a/apps/extension/components.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "$schema": "https://ui.shadcn.com/schema.json",
- "style": "new-york",
- "rsc": false,
- "tsx": true,
- "tailwind": {
- "config": "tailwind.config.js",
- "css": "css/globals.css",
- "baseColor": "zinc",
- "cssVariables": true,
- "prefix": ""
- },
- "aliases": {
- "components": "@/ui",
- "utils": "@/lib/utils",
- "ui": "@/ui/shadcn",
- "lib": "@/lib",
- "hooks": "@/hooks"
- },
- "iconLibrary": "lucide"
-}
diff --git a/apps/extension/css/globals.css b/apps/extension/css/globals.css
deleted file mode 100644
index a1be0c34..00000000
--- a/apps/extension/css/globals.css
+++ /dev/null
@@ -1,59 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-@layer base {
- :root {
- --background: 0 0% 100%;
- --foreground: 222.2 47.4% 11.2%;
- --muted: 210 40% 96.1%;
- --muted-foreground: 215.4 16.3% 46.9%;
- --popover: 0 0% 100%;
- --popover-foreground: 222.2 47.4% 11.2%;
- --border: 214.3 31.8% 91.4%;
- --input: 214.3 31.8% 91.4%;
- --card: 0 0% 100%;
- --card-foreground: 222.2 47.4% 11.2%;
- --primary: 222.2 47.4% 11.2%;
- --primary-foreground: 210 40% 98%;
- --secondary: 210 40% 96.1%;
- --secondary-foreground: 222.2 47.4% 11.2%;
- --accent: 210 40% 96.1%;
- --accent-foreground: 222.2 47.4% 11.2%;
- --destructive: 0 100% 50%;
- --destructive-foreground: 210 40% 98%;
- --ring: 215 20.2% 65.1%;
- --radius: 0.5rem;
- }
-
- .dark {
- --background: 224 71% 4%;
- --foreground: 213 31% 91%;
- --muted: 223 47% 11%;
- --muted-foreground: 215.4 16.3% 56.9%;
- --accent: 216 34% 17%;
- --accent-foreground: 210 40% 98%;
- --popover: 224 71% 4%;
- --popover-foreground: 215 20.2% 65.1%;
- --border: 216 34% 17%;
- --input: 216 34% 17%;
- --card: 224 71% 4%;
- --card-foreground: 213 31% 91%;
- --primary: 210 40% 98%;
- --primary-foreground: 222.2 47.4% 1.2%;
- --secondary: 222.2 47.4% 11.2%;
- --secondary-foreground: 210 40% 98%;
- --destructive: 0 63% 31%;
- --destructive-foreground: 210 40% 98%;
- --ring: 216 34% 17%;
- }
-}
-
-@layer base {
- * {
- @apply border-border;
- }
- body {
- @apply font-sans antialiased bg-background text-foreground;
- }
-}
diff --git a/apps/extension/extension-env.d.ts b/apps/extension/extension-env.d.ts
deleted file mode 100644
index 6005b801..00000000
--- a/apps/extension/extension-env.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-// Required Extension.js types for TypeScript projects.
-// This file is auto-generated and should not be excluded.
-// If you need additional types, consider creating a new *.d.ts file and
-// referencing it in the "include" array of your tsconfig.json file.
-// See https://www.typescriptlang.org/tsconfig#include for more information.
-/// <reference types="extension/dist/types/index.d.ts" />
-
-// Polyfill types for browser.* APIs.
-/// <reference types="extension/dist/types/polyfill.d.ts" />
diff --git a/apps/extension/images/icon/128.png b/apps/extension/images/icon/128.png
deleted file mode 100644
index d1fedfe2..00000000
--- a/apps/extension/images/icon/128.png
+++ /dev/null
Binary files differ
diff --git a/apps/extension/images/icon/16.png b/apps/extension/images/icon/16.png
deleted file mode 100644
index 7c5d791a..00000000
--- a/apps/extension/images/icon/16.png
+++ /dev/null
Binary files differ
diff --git a/apps/extension/images/icon/32.png b/apps/extension/images/icon/32.png
deleted file mode 100644
index 3cd1aa93..00000000
--- a/apps/extension/images/icon/32.png
+++ /dev/null
Binary files differ
diff --git a/apps/extension/images/icon/48.png b/apps/extension/images/icon/48.png
deleted file mode 100644
index fe9e1c49..00000000
--- a/apps/extension/images/icon/48.png
+++ /dev/null
Binary files differ
diff --git a/apps/extension/images/icon/favicon.ico b/apps/extension/images/icon/favicon.ico
deleted file mode 100644
index 90d7aafc..00000000
--- a/apps/extension/images/icon/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/apps/extension/lib/utils.ts b/apps/extension/lib/utils.ts
deleted file mode 100644
index b406ea53..00000000
--- a/apps/extension/lib/utils.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { clsx, type ClassValue } from "clsx";
-import { twMerge } from "tailwind-merge";
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
-}
-
-// getBaseURL function that checks if we are in dev or prod and returns the correct baseURL
-export async function getBaseURL() {
- if (typeof chrome === "undefined") {
- console.error("chrome is undefined: only run in background script");
- throw new Error("chrome is undefined: only run in background script");
- }
- const extensionInfo = await chrome.management.getSelf();
- console.info(`Running in ${extensionInfo.installType} mode`);
- // If we're in development mode, the id will contain 'development'
- return extensionInfo.installType.includes("development")
- ? "http://localhost:3000"
- : "https://supermemory.ai";
-}
diff --git a/apps/extension/manifest.json b/apps/extension/manifest.json
deleted file mode 100644
index ff4aba05..00000000
--- a/apps/extension/manifest.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "$schema": "https://json.schemastore.org/chrome-manifest.json",
- "manifest_version": 3,
- "version": "5.0.200",
- "homepage_url": "https://supermemory.ai",
- "name": "Supermemory",
- "description": "An extension for https://supermemory.ai - an AI hub for all your knowledge.",
- "author": "Dhravya Shah",
- "permissions": [
- "activeTab",
- "scripting",
- "tabs",
- "management",
- "webRequest",
- "storage",
- "bookmarks"
- ],
- "host_permissions": ["*://*/*"],
- "background": {
- "service_worker": "src/background.ts"
- },
- "action": {},
- "externally_connectable": {
- "matches": [
- "http://localhost:3000/*",
- "https://supermemory.ai/*",
- "https://beta.supermemory.ai/*",
- "http://supermemory.com/*"
- ]
- },
- "icons": {
- "16": "images/icon/16.png",
- "32": "images/icon/32.png",
- "48": "images/icon/48.png",
- "128": "images/icon/128.png"
- }
-}
diff --git a/apps/extension/package.json b/apps/extension/package.json
deleted file mode 100644
index eadb7393..00000000
--- a/apps/extension/package.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "name": "supermemory-extension",
- "version": "1.0.0",
- "description": "A memory enhancement extension",
- "type": "module",
- "scripts": {
- "dev:css": "npx tailwindcss -i css/globals.css -o public/globals.css --watch",
- "dev:extension": "extension dev",
- "dev": "concurrently \"npm:dev:css\" \"npm:dev:extension\"",
- "build:css": "npx tailwindcss -i css/globals.css -o public/globals.css",
- "build:extension": "extension build",
- "build": "npm run build:css && npm run build:extension",
- "start": "npm run build && extension start",
- "zip": "zip -r supermemory-extension.zip ./dist/chrome/*"
- },
- "dependencies": {
- "@mozilla/readability": "^0.5.0",
- "@radix-ui/react-dialog": "^1.1.2",
- "@radix-ui/react-popover": "^1.1.2",
- "@radix-ui/react-slot": "^1.1.0",
- "@radix-ui/react-toast": "^1.2.2",
- "@types/react": "^18.3.1",
- "@types/react-dom": "^18.3.1",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
- "cmdk": "1.0.0",
- "lucide-react": "^0.462.0",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
- "tailwind-merge": "^2.5.5",
- "tailwindcss-animate": "^1.0.7"
- },
- "devDependencies": {
- "@types/chrome": "^0.0.260",
- "extension": "latest",
- "typescript": "^5.3.3"
- }
-}
diff --git a/apps/extension/postcss.config.js b/apps/extension/postcss.config.js
deleted file mode 100644
index 33ad091d..00000000
--- a/apps/extension/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-}
diff --git a/apps/extension/public/globals.css b/apps/extension/public/globals.css
deleted file mode 100644
index afee1545..00000000
--- a/apps/extension/public/globals.css
+++ /dev/null
@@ -1,1632 +0,0 @@
-*, ::before, ::after {
- --tw-border-spacing-x: 0;
- --tw-border-spacing-y: 0;
- --tw-translate-x: 0;
- --tw-translate-y: 0;
- --tw-rotate: 0;
- --tw-skew-x: 0;
- --tw-skew-y: 0;
- --tw-scale-x: 1;
- --tw-scale-y: 1;
- --tw-pan-x: ;
- --tw-pan-y: ;
- --tw-pinch-zoom: ;
- --tw-scroll-snap-strictness: proximity;
- --tw-gradient-from-position: ;
- --tw-gradient-via-position: ;
- --tw-gradient-to-position: ;
- --tw-ordinal: ;
- --tw-slashed-zero: ;
- --tw-numeric-figure: ;
- --tw-numeric-spacing: ;
- --tw-numeric-fraction: ;
- --tw-ring-inset: ;
- --tw-ring-offset-width: 0px;
- --tw-ring-offset-color: #fff;
- --tw-ring-color: rgb(59 130 246 / 0.5);
- --tw-ring-offset-shadow: 0 0 #0000;
- --tw-ring-shadow: 0 0 #0000;
- --tw-shadow: 0 0 #0000;
- --tw-shadow-colored: 0 0 #0000;
- --tw-blur: ;
- --tw-brightness: ;
- --tw-contrast: ;
- --tw-grayscale: ;
- --tw-hue-rotate: ;
- --tw-invert: ;
- --tw-saturate: ;
- --tw-sepia: ;
- --tw-drop-shadow: ;
- --tw-backdrop-blur: ;
- --tw-backdrop-brightness: ;
- --tw-backdrop-contrast: ;
- --tw-backdrop-grayscale: ;
- --tw-backdrop-hue-rotate: ;
- --tw-backdrop-invert: ;
- --tw-backdrop-opacity: ;
- --tw-backdrop-saturate: ;
- --tw-backdrop-sepia: ;
- --tw-contain-size: ;
- --tw-contain-layout: ;
- --tw-contain-paint: ;
- --tw-contain-style: ;
-}
-
-::backdrop {
- --tw-border-spacing-x: 0;
- --tw-border-spacing-y: 0;
- --tw-translate-x: 0;
- --tw-translate-y: 0;
- --tw-rotate: 0;
- --tw-skew-x: 0;
- --tw-skew-y: 0;
- --tw-scale-x: 1;
- --tw-scale-y: 1;
- --tw-pan-x: ;
- --tw-pan-y: ;
- --tw-pinch-zoom: ;
- --tw-scroll-snap-strictness: proximity;
- --tw-gradient-from-position: ;
- --tw-gradient-via-position: ;
- --tw-gradient-to-position: ;
- --tw-ordinal: ;
- --tw-slashed-zero: ;
- --tw-numeric-figure: ;
- --tw-numeric-spacing: ;
- --tw-numeric-fraction: ;
- --tw-ring-inset: ;
- --tw-ring-offset-width: 0px;
- --tw-ring-offset-color: #fff;
- --tw-ring-color: rgb(59 130 246 / 0.5);
- --tw-ring-offset-shadow: 0 0 #0000;
- --tw-ring-shadow: 0 0 #0000;
- --tw-shadow: 0 0 #0000;
- --tw-shadow-colored: 0 0 #0000;
- --tw-blur: ;
- --tw-brightness: ;
- --tw-contrast: ;
- --tw-grayscale: ;
- --tw-hue-rotate: ;
- --tw-invert: ;
- --tw-saturate: ;
- --tw-sepia: ;
- --tw-drop-shadow: ;
- --tw-backdrop-blur: ;
- --tw-backdrop-brightness: ;
- --tw-backdrop-contrast: ;
- --tw-backdrop-grayscale: ;
- --tw-backdrop-hue-rotate: ;
- --tw-backdrop-invert: ;
- --tw-backdrop-opacity: ;
- --tw-backdrop-saturate: ;
- --tw-backdrop-sepia: ;
- --tw-contain-size: ;
- --tw-contain-layout: ;
- --tw-contain-paint: ;
- --tw-contain-style: ;
-}
-
-/*
-! tailwindcss v3.4.15 | MIT License | https://tailwindcss.com
-*/
-
-/*
-1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
-2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
-*/
-
-*,
-::before,
-::after {
- box-sizing: border-box;
- /* 1 */
- border-width: 0;
- /* 2 */
- border-style: solid;
- /* 2 */
- border-color: #e5e7eb;
- /* 2 */
-}
-
-::before,
-::after {
- --tw-content: '';
-}
-
-/*
-1. Use a consistent sensible line-height in all browsers.
-2. Prevent adjustments of font size after orientation changes in iOS.
-3. Use a more readable tab size.
-4. Use the user's configured `sans` font-family by default.
-5. Use the user's configured `sans` font-feature-settings by default.
-6. Use the user's configured `sans` font-variation-settings by default.
-7. Disable tap highlights on iOS
-*/
-
-html,
-:host {
- line-height: 1.5;
- /* 1 */
- -webkit-text-size-adjust: 100%;
- /* 2 */
- -moz-tab-size: 4;
- /* 3 */
- -o-tab-size: 4;
- tab-size: 4;
- /* 3 */
- font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
- /* 4 */
- font-feature-settings: normal;
- /* 5 */
- font-variation-settings: normal;
- /* 6 */
- -webkit-tap-highlight-color: transparent;
- /* 7 */
-}
-
-/*
-1. Remove the margin in all browsers.
-2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
-*/
-
-body {
- margin: 0;
- /* 1 */
- line-height: inherit;
- /* 2 */
-}
-
-/*
-1. Add the correct height in Firefox.
-2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
-3. Ensure horizontal rules are visible by default.
-*/
-
-hr {
- height: 0;
- /* 1 */
- color: inherit;
- /* 2 */
- border-top-width: 1px;
- /* 3 */
-}
-
-/*
-Add the correct text decoration in Chrome, Edge, and Safari.
-*/
-
-abbr:where([title]) {
- -webkit-text-decoration: underline dotted;
- text-decoration: underline dotted;
-}
-
-/*
-Remove the default font size and weight for headings.
-*/
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- font-size: inherit;
- font-weight: inherit;
-}
-
-/*
-Reset links to optimize for opt-in styling instead of opt-out.
-*/
-
-a {
- color: inherit;
- text-decoration: inherit;
-}
-
-/*
-Add the correct font weight in Edge and Safari.
-*/
-
-b,
-strong {
- font-weight: bolder;
-}
-
-/*
-1. Use the user's configured `mono` font-family by default.
-2. Use the user's configured `mono` font-feature-settings by default.
-3. Use the user's configured `mono` font-variation-settings by default.
-4. Correct the odd `em` font sizing in all browsers.
-*/
-
-code,
-kbd,
-samp,
-pre {
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
- /* 1 */
- font-feature-settings: normal;
- /* 2 */
- font-variation-settings: normal;
- /* 3 */
- font-size: 1em;
- /* 4 */
-}
-
-/*
-Add the correct font size in all browsers.
-*/
-
-small {
- font-size: 80%;
-}
-
-/*
-Prevent `sub` and `sup` elements from affecting the line height in all browsers.
-*/
-
-sub,
-sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
-}
-
-sub {
- bottom: -0.25em;
-}
-
-sup {
- top: -0.5em;
-}
-
-/*
-1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
-2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
-3. Remove gaps between table borders by default.
-*/
-
-table {
- text-indent: 0;
- /* 1 */
- border-color: inherit;
- /* 2 */
- border-collapse: collapse;
- /* 3 */
-}
-
-/*
-1. Change the font styles in all browsers.
-2. Remove the margin in Firefox and Safari.
-3. Remove default padding in all browsers.
-*/
-
-button,
-input,
-optgroup,
-select,
-textarea {
- font-family: inherit;
- /* 1 */
- font-feature-settings: inherit;
- /* 1 */
- font-variation-settings: inherit;
- /* 1 */
- font-size: 100%;
- /* 1 */
- font-weight: inherit;
- /* 1 */
- line-height: inherit;
- /* 1 */
- letter-spacing: inherit;
- /* 1 */
- color: inherit;
- /* 1 */
- margin: 0;
- /* 2 */
- padding: 0;
- /* 3 */
-}
-
-/*
-Remove the inheritance of text transform in Edge and Firefox.
-*/
-
-button,
-select {
- text-transform: none;
-}
-
-/*
-1. Correct the inability to style clickable types in iOS and Safari.
-2. Remove default button styles.
-*/
-
-button,
-input:where([type='button']),
-input:where([type='reset']),
-input:where([type='submit']) {
- -webkit-appearance: button;
- /* 1 */
- background-color: transparent;
- /* 2 */
- background-image: none;
- /* 2 */
-}
-
-/*
-Use the modern Firefox focus style for all focusable elements.
-*/
-
-:-moz-focusring {
- outline: auto;
-}
-
-/*
-Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
-*/
-
-:-moz-ui-invalid {
- box-shadow: none;
-}
-
-/*
-Add the correct vertical alignment in Chrome and Firefox.
-*/
-
-progress {
- vertical-align: baseline;
-}
-
-/*
-Correct the cursor style of increment and decrement buttons in Safari.
-*/
-
-::-webkit-inner-spin-button,
-::-webkit-outer-spin-button {
- height: auto;
-}
-
-/*
-1. Correct the odd appearance in Chrome and Safari.
-2. Correct the outline style in Safari.
-*/
-
-[type='search'] {
- -webkit-appearance: textfield;
- /* 1 */
- outline-offset: -2px;
- /* 2 */
-}
-
-/*
-Remove the inner padding in Chrome and Safari on macOS.
-*/
-
-::-webkit-search-decoration {
- -webkit-appearance: none;
-}
-
-/*
-1. Correct the inability to style clickable types in iOS and Safari.
-2. Change font properties to `inherit` in Safari.
-*/
-
-::-webkit-file-upload-button {
- -webkit-appearance: button;
- /* 1 */
- font: inherit;
- /* 2 */
-}
-
-/*
-Add the correct display in Chrome and Safari.
-*/
-
-summary {
- display: list-item;
-}
-
-/*
-Removes the default spacing and border for appropriate elements.
-*/
-
-blockquote,
-dl,
-dd,
-h1,
-h2,
-h3,
-h4,
-h5,
-h6,
-hr,
-figure,
-p,
-pre {
- margin: 0;
-}
-
-fieldset {
- margin: 0;
- padding: 0;
-}
-
-legend {
- padding: 0;
-}
-
-ol,
-ul,
-menu {
- list-style: none;
- margin: 0;
- padding: 0;
-}
-
-/*
-Reset default styling for dialogs.
-*/
-
-dialog {
- padding: 0;
-}
-
-/*
-Prevent resizing textareas horizontally by default.
-*/
-
-textarea {
- resize: vertical;
-}
-
-/*
-1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
-2. Set the default placeholder color to the user's configured gray 400 color.
-*/
-
-input::-moz-placeholder, textarea::-moz-placeholder {
- opacity: 1;
- /* 1 */
- color: #9ca3af;
- /* 2 */
-}
-
-input::placeholder,
-textarea::placeholder {
- opacity: 1;
- /* 1 */
- color: #9ca3af;
- /* 2 */
-}
-
-/*
-Set the default cursor for buttons.
-*/
-
-button,
-[role="button"] {
- cursor: pointer;
-}
-
-/*
-Make sure disabled buttons don't get the pointer cursor.
-*/
-
-:disabled {
- cursor: default;
-}
-
-/*
-1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
-2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
- This can trigger a poorly considered lint error in some tools but is included by design.
-*/
-
-img,
-svg,
-video,
-canvas,
-audio,
-iframe,
-embed,
-object {
- display: block;
- /* 1 */
- vertical-align: middle;
- /* 2 */
-}
-
-/*
-Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
-*/
-
-img,
-video {
- max-width: 100%;
- height: auto;
-}
-
-/* Make elements with the HTML hidden attribute stay hidden by default */
-
-[hidden]:where(:not([hidden="until-found"])) {
- display: none;
-}
-
-:root {
- --background: 0 0% 100%;
- --foreground: 222.2 47.4% 11.2%;
- --muted: 210 40% 96.1%;
- --muted-foreground: 215.4 16.3% 46.9%;
- --popover: 0 0% 100%;
- --popover-foreground: 222.2 47.4% 11.2%;
- --border: 214.3 31.8% 91.4%;
- --input: 214.3 31.8% 91.4%;
- --card: 0 0% 100%;
- --card-foreground: 222.2 47.4% 11.2%;
- --primary: 222.2 47.4% 11.2%;
- --primary-foreground: 210 40% 98%;
- --secondary: 210 40% 96.1%;
- --secondary-foreground: 222.2 47.4% 11.2%;
- --accent: 210 40% 96.1%;
- --accent-foreground: 222.2 47.4% 11.2%;
- --destructive: 0 100% 50%;
- --destructive-foreground: 210 40% 98%;
- --ring: 215 20.2% 65.1%;
- --radius: 0.5rem;
-}
-
-* {
- border-color: hsl(var(--border));
-}
-
-body {
- background-color: hsl(var(--background));
- font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
- color: hsl(var(--foreground));
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-.container {
- width: 100%;
-}
-
-@media (min-width: 640px) {
- .container {
- max-width: 640px;
- }
-}
-
-@media (min-width: 768px) {
- .container {
- max-width: 768px;
- }
-}
-
-@media (min-width: 1024px) {
- .container {
- max-width: 1024px;
- }
-}
-
-@media (min-width: 1280px) {
- .container {
- max-width: 1280px;
- }
-}
-
-@media (min-width: 1536px) {
- .container {
- max-width: 1536px;
- }
-}
-
-.sr-only {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border-width: 0;
-}
-
-.fixed {
- position: fixed;
-}
-
-.absolute {
- position: absolute;
-}
-
-.relative {
- position: relative;
-}
-
-.inset-0 {
- inset: 0px;
-}
-
-.left-0 {
- left: 0px;
-}
-
-.left-\[50\%\] {
- left: 50%;
-}
-
-.right-0 {
- right: 0px;
-}
-
-.right-4 {
- right: 1rem;
-}
-
-.top-4 {
- top: 1rem;
-}
-
-.top-\[50\%\] {
- top: 50%;
-}
-
-.top-full {
- top: 100%;
-}
-
-.z-50 {
- z-index: 50;
-}
-
-.-mx-1 {
- margin-left: -0.25rem;
- margin-right: -0.25rem;
-}
-
-.ml-auto {
- margin-left: auto;
-}
-
-.mr-2 {
- margin-right: 0.5rem;
-}
-
-.mt-1 {
- margin-top: 0.25rem;
-}
-
-.mt-2 {
- margin-top: 0.5rem;
-}
-
-.flex {
- display: flex;
-}
-
-.inline-flex {
- display: inline-flex;
-}
-
-.grid {
- display: grid;
-}
-
-.hidden {
- display: none;
-}
-
-.h-1 {
- height: 0.25rem;
-}
-
-.h-1\.5 {
- height: 0.375rem;
-}
-
-.h-10 {
- height: 2.5rem;
-}
-
-.h-2\.5 {
- height: 0.625rem;
-}
-
-.h-3 {
- height: 0.75rem;
-}
-
-.h-4 {
- height: 1rem;
-}
-
-.h-6 {
- height: 1.5rem;
-}
-
-.h-8 {
- height: 2rem;
-}
-
-.h-9 {
- height: 2.25rem;
-}
-
-.h-full {
- height: 100%;
-}
-
-.h-px {
- height: 1px;
-}
-
-.max-h-\[240px\] {
- max-height: 240px;
-}
-
-.max-h-\[300px\] {
- max-height: 300px;
-}
-
-.w-1\.5 {
- width: 0.375rem;
-}
-
-.w-2\.5 {
- width: 0.625rem;
-}
-
-.w-3 {
- width: 0.75rem;
-}
-
-.w-4 {
- width: 1rem;
-}
-
-.w-6 {
- width: 1.5rem;
-}
-
-.w-72 {
- width: 18rem;
-}
-
-.w-9 {
- width: 2.25rem;
-}
-
-.w-full {
- width: 100%;
-}
-
-.max-w-\[200px\] {
- max-width: 200px;
-}
-
-.max-w-lg {
- max-width: 32rem;
-}
-
-.flex-1 {
- flex: 1 1 0%;
-}
-
-.flex-shrink-0 {
- flex-shrink: 0;
-}
-
-.shrink-0 {
- flex-shrink: 0;
-}
-
-.translate-x-\[-50\%\] {
- --tw-translate-x: -50%;
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-.translate-y-\[-50\%\] {
- --tw-translate-y: -50%;
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-.rotate-180 {
- --tw-rotate: 180deg;
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-.transform {
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-@keyframes spin {
- to {
- transform: rotate(360deg);
- }
-}
-
-.animate-spin {
- animation: spin 1s linear infinite;
-}
-
-.cursor-default {
- cursor: default;
-}
-
-.select-none {
- -webkit-user-select: none;
- -moz-user-select: none;
- user-select: none;
-}
-
-.flex-col {
- flex-direction: column;
-}
-
-.flex-col-reverse {
- flex-direction: column-reverse;
-}
-
-.flex-wrap {
- flex-wrap: wrap;
-}
-
-.items-center {
- align-items: center;
-}
-
-.justify-center {
- justify-content: center;
-}
-
-.justify-between {
- justify-content: space-between;
-}
-
-.gap-0\.5 {
- gap: 0.125rem;
-}
-
-.gap-1 {
- gap: 0.25rem;
-}
-
-.gap-1\.5 {
- gap: 0.375rem;
-}
-
-.gap-2 {
- gap: 0.5rem;
-}
-
-.gap-3 {
- gap: 0.75rem;
-}
-
-.gap-4 {
- gap: 1rem;
-}
-
-.space-y-1\.5 > :not([hidden]) ~ :not([hidden]) {
- --tw-space-y-reverse: 0;
- margin-top: calc(0.375rem * calc(1 - var(--tw-space-y-reverse)));
- margin-bottom: calc(0.375rem * var(--tw-space-y-reverse));
-}
-
-.space-y-2 > :not([hidden]) ~ :not([hidden]) {
- --tw-space-y-reverse: 0;
- margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
- margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
-}
-
-.space-y-4 > :not([hidden]) ~ :not([hidden]) {
- --tw-space-y-reverse: 0;
- margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
- margin-bottom: calc(1rem * var(--tw-space-y-reverse));
-}
-
-.overflow-hidden {
- overflow: hidden;
-}
-
-.overflow-y-auto {
- overflow-y: auto;
-}
-
-.overflow-x-hidden {
- overflow-x: hidden;
-}
-
-.truncate {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.whitespace-nowrap {
- white-space: nowrap;
-}
-
-.rounded {
- border-radius: 0.25rem;
-}
-
-.rounded-full {
- border-radius: 9999px;
-}
-
-.rounded-md {
- border-radius: calc(var(--radius) - 2px);
-}
-
-.rounded-sm {
- border-radius: calc(var(--radius) - 4px);
-}
-
-.rounded-xl {
- border-radius: 0.75rem;
-}
-
-.rounded-b-xl {
- border-bottom-right-radius: 0.75rem;
- border-bottom-left-radius: 0.75rem;
-}
-
-.border {
- border-width: 1px;
-}
-
-.border-b {
- border-bottom-width: 1px;
-}
-
-.border-b-2 {
- border-bottom-width: 2px;
-}
-
-.border-blue-500 {
- --tw-border-opacity: 1;
- border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
-}
-
-.border-input {
- border-color: hsl(var(--input));
-}
-
-.border-transparent {
- border-color: transparent;
-}
-
-.border-white {
- --tw-border-opacity: 1;
- border-color: rgb(255 255 255 / var(--tw-border-opacity, 1));
-}
-
-.border-white\/20 {
- border-color: rgb(255 255 255 / 0.2);
-}
-
-.border-white\/50 {
- border-color: rgb(255 255 255 / 0.5);
-}
-
-.bg-background {
- background-color: hsl(var(--background));
-}
-
-.bg-black\/80 {
- background-color: rgb(0 0 0 / 0.8);
-}
-
-.bg-blue-400 {
- --tw-bg-opacity: 1;
- background-color: rgb(96 165 250 / var(--tw-bg-opacity, 1));
-}
-
-.bg-blue-500 {
- --tw-bg-opacity: 1;
- background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
-}
-
-.bg-border {
- background-color: hsl(var(--border));
-}
-
-.bg-destructive {
- background-color: hsl(var(--destructive));
-}
-
-.bg-gray-700 {
- --tw-bg-opacity: 1;
- background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
-}
-
-.bg-gray-800 {
- --tw-bg-opacity: 1;
- background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1));
-}
-
-.bg-gray-900 {
- --tw-bg-opacity: 1;
- background-color: rgb(17 24 39 / var(--tw-bg-opacity, 1));
-}
-
-.bg-green-500 {
- --tw-bg-opacity: 1;
- background-color: rgb(34 197 94 / var(--tw-bg-opacity, 1));
-}
-
-.bg-popover {
- background-color: hsl(var(--popover));
-}
-
-.bg-primary {
- background-color: hsl(var(--primary));
-}
-
-.bg-red-500 {
- --tw-bg-opacity: 1;
- background-color: rgb(239 68 68 / var(--tw-bg-opacity, 1));
-}
-
-.bg-secondary {
- background-color: hsl(var(--secondary));
-}
-
-.bg-transparent {
- background-color: transparent;
-}
-
-.bg-white\/10 {
- background-color: rgb(255 255 255 / 0.1);
-}
-
-.bg-white\/20 {
- background-color: rgb(255 255 255 / 0.2);
-}
-
-.bg-yellow-500 {
- --tw-bg-opacity: 1;
- background-color: rgb(234 179 8 / var(--tw-bg-opacity, 1));
-}
-
-.bg-gradient-to-br {
- background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
-}
-
-.from-gray-800 {
- --tw-gradient-from: #1f2937 var(--tw-gradient-from-position);
- --tw-gradient-to: rgb(31 41 55 / 0) var(--tw-gradient-to-position);
- --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
-}
-
-.to-gray-900 {
- --tw-gradient-to: #111827 var(--tw-gradient-to-position);
-}
-
-.p-0 {
- padding: 0px;
-}
-
-.p-1 {
- padding: 0.25rem;
-}
-
-.p-4 {
- padding: 1rem;
-}
-
-.p-6 {
- padding: 1.5rem;
-}
-
-.px-2 {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-
-.px-2\.5 {
- padding-left: 0.625rem;
- padding-right: 0.625rem;
-}
-
-.px-3 {
- padding-left: 0.75rem;
- padding-right: 0.75rem;
-}
-
-.px-4 {
- padding-left: 1rem;
- padding-right: 1rem;
-}
-
-.px-8 {
- padding-left: 2rem;
- padding-right: 2rem;
-}
-
-.py-0\.5 {
- padding-top: 0.125rem;
- padding-bottom: 0.125rem;
-}
-
-.py-1 {
- padding-top: 0.25rem;
- padding-bottom: 0.25rem;
-}
-
-.py-1\.5 {
- padding-top: 0.375rem;
- padding-bottom: 0.375rem;
-}
-
-.py-2 {
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
-}
-
-.py-3 {
- padding-top: 0.75rem;
- padding-bottom: 0.75rem;
-}
-
-.py-6 {
- padding-top: 1.5rem;
- padding-bottom: 1.5rem;
-}
-
-.text-left {
- text-align: left;
-}
-
-.text-center {
- text-align: center;
-}
-
-.text-2xl {
- font-size: 1.5rem;
- line-height: 2rem;
-}
-
-.text-lg {
- font-size: 1.125rem;
- line-height: 1.75rem;
-}
-
-.text-sm {
- font-size: 0.875rem;
- line-height: 1.25rem;
-}
-
-.text-xs {
- font-size: 0.75rem;
- line-height: 1rem;
-}
-
-.font-bold {
- font-weight: 700;
-}
-
-.font-medium {
- font-weight: 500;
-}
-
-.font-semibold {
- font-weight: 600;
-}
-
-.leading-none {
- line-height: 1;
-}
-
-.tracking-tight {
- letter-spacing: -0.025em;
-}
-
-.tracking-widest {
- letter-spacing: 0.1em;
-}
-
-.text-blue-100 {
- --tw-text-opacity: 1;
- color: rgb(219 234 254 / var(--tw-text-opacity, 1));
-}
-
-.text-destructive-foreground {
- color: hsl(var(--destructive-foreground));
-}
-
-.text-foreground {
- color: hsl(var(--foreground));
-}
-
-.text-gray-100 {
- --tw-text-opacity: 1;
- color: rgb(243 244 246 / var(--tw-text-opacity, 1));
-}
-
-.text-gray-200 {
- --tw-text-opacity: 1;
- color: rgb(229 231 235 / var(--tw-text-opacity, 1));
-}
-
-.text-gray-300 {
- --tw-text-opacity: 1;
- color: rgb(209 213 219 / var(--tw-text-opacity, 1));
-}
-
-.text-green-400 {
- --tw-text-opacity: 1;
- color: rgb(74 222 128 / var(--tw-text-opacity, 1));
-}
-
-.text-muted-foreground {
- color: hsl(var(--muted-foreground));
-}
-
-.text-popover-foreground {
- color: hsl(var(--popover-foreground));
-}
-
-.text-primary {
- color: hsl(var(--primary));
-}
-
-.text-primary-foreground {
- color: hsl(var(--primary-foreground));
-}
-
-.text-secondary-foreground {
- color: hsl(var(--secondary-foreground));
-}
-
-.text-white {
- --tw-text-opacity: 1;
- color: rgb(255 255 255 / var(--tw-text-opacity, 1));
-}
-
-.text-white\/70 {
- color: rgb(255 255 255 / 0.7);
-}
-
-.text-white\/90 {
- color: rgb(255 255 255 / 0.9);
-}
-
-.underline-offset-4 {
- text-underline-offset: 4px;
-}
-
-.placeholder-white\/50::-moz-placeholder {
- color: rgb(255 255 255 / 0.5);
-}
-
-.placeholder-white\/50::placeholder {
- color: rgb(255 255 255 / 0.5);
-}
-
-.opacity-50 {
- opacity: 0.5;
-}
-
-.opacity-60 {
- opacity: 0.6;
-}
-
-.opacity-70 {
- opacity: 0.7;
-}
-
-.shadow {
- --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
- --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
- box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.shadow-lg {
- --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
- --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
- box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.shadow-md {
- --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
- --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
- box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.shadow-sm {
- --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
- --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
- box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.outline-none {
- outline: 2px solid transparent;
- outline-offset: 2px;
-}
-
-.outline {
- outline-style: solid;
-}
-
-.ring-offset-background {
- --tw-ring-offset-color: hsl(var(--background));
-}
-
-.filter {
- filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
-}
-
-.transition-all {
- transition-property: all;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
-}
-
-.transition-colors {
- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
-}
-
-.transition-opacity {
- transition-property: opacity;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
-}
-
-.transition-transform {
- transition-property: transform;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
-}
-
-.duration-150 {
- transition-duration: 150ms;
-}
-
-.duration-200 {
- transition-duration: 200ms;
-}
-
-.placeholder\:text-muted-foreground::-moz-placeholder {
- color: hsl(var(--muted-foreground));
-}
-
-.placeholder\:text-muted-foreground::placeholder {
- color: hsl(var(--muted-foreground));
-}
-
-.hover\:bg-accent:hover {
- background-color: hsl(var(--accent));
-}
-
-.hover\:bg-destructive\/80:hover {
- background-color: hsl(var(--destructive) / 0.8);
-}
-
-.hover\:bg-destructive\/90:hover {
- background-color: hsl(var(--destructive) / 0.9);
-}
-
-.hover\:bg-gray-800:hover {
- --tw-bg-opacity: 1;
- background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1));
-}
-
-.hover\:bg-green-600:hover {
- --tw-bg-opacity: 1;
- background-color: rgb(22 163 74 / var(--tw-bg-opacity, 1));
-}
-
-.hover\:bg-primary\/80:hover {
- background-color: hsl(var(--primary) / 0.8);
-}
-
-.hover\:bg-primary\/90:hover {
- background-color: hsl(var(--primary) / 0.9);
-}
-
-.hover\:bg-secondary\/80:hover {
- background-color: hsl(var(--secondary) / 0.8);
-}
-
-.hover\:bg-white\/20:hover {
- background-color: rgb(255 255 255 / 0.2);
-}
-
-.hover\:text-accent-foreground:hover {
- color: hsl(var(--accent-foreground));
-}
-
-.hover\:text-white:hover {
- --tw-text-opacity: 1;
- color: rgb(255 255 255 / var(--tw-text-opacity, 1));
-}
-
-.hover\:underline:hover {
- text-decoration-line: underline;
-}
-
-.hover\:opacity-100:hover {
- opacity: 1;
-}
-
-.focus\:border-blue-500:focus {
- --tw-border-opacity: 1;
- border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
-}
-
-.focus\:outline-none:focus {
- outline: 2px solid transparent;
- outline-offset: 2px;
-}
-
-.focus\:ring-2:focus {
- --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
- --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
- box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
-}
-
-.focus\:ring-ring:focus {
- --tw-ring-color: hsl(var(--ring));
-}
-
-.focus\:ring-offset-2:focus {
- --tw-ring-offset-width: 2px;
-}
-
-.focus-visible\:outline-none:focus-visible {
- outline: 2px solid transparent;
- outline-offset: 2px;
-}
-
-.focus-visible\:ring-1:focus-visible {
- --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
- --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
- box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
-}
-
-.focus-visible\:ring-ring:focus-visible {
- --tw-ring-color: hsl(var(--ring));
-}
-
-.disabled\:pointer-events-none:disabled {
- pointer-events: none;
-}
-
-.disabled\:cursor-not-allowed:disabled {
- cursor: not-allowed;
-}
-
-.disabled\:opacity-50:disabled {
- opacity: 0.5;
-}
-
-.data-\[disabled\=true\]\:pointer-events-none[data-disabled="true"] {
- pointer-events: none;
-}
-
-.data-\[selected\=true\]\:bg-accent[data-selected="true"] {
- background-color: hsl(var(--accent));
-}
-
-.data-\[state\=open\]\:bg-accent[data-state="open"] {
- background-color: hsl(var(--accent));
-}
-
-.data-\[selected\=true\]\:text-accent-foreground[data-selected="true"] {
- color: hsl(var(--accent-foreground));
-}
-
-.data-\[state\=open\]\:text-muted-foreground[data-state="open"] {
- color: hsl(var(--muted-foreground));
-}
-
-.data-\[disabled\=true\]\:opacity-50[data-disabled="true"] {
- opacity: 0.5;
-}
-
-@media (min-width: 640px) {
- .sm\:flex-row {
- flex-direction: row;
- }
-
- .sm\:justify-end {
- justify-content: flex-end;
- }
-
- .sm\:space-x-2 > :not([hidden]) ~ :not([hidden]) {
- --tw-space-x-reverse: 0;
- margin-right: calc(0.5rem * var(--tw-space-x-reverse));
- margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
- }
-
- .sm\:rounded-lg {
- border-radius: var(--radius);
- }
-
- .sm\:text-left {
- text-align: left;
- }
-}
-
-.\[\&_\[cmdk-group-heading\]\]\:px-2 [cmdk-group-heading] {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-
-.\[\&_\[cmdk-group-heading\]\]\:py-1\.5 [cmdk-group-heading] {
- padding-top: 0.375rem;
- padding-bottom: 0.375rem;
-}
-
-.\[\&_\[cmdk-group-heading\]\]\:text-xs [cmdk-group-heading] {
- font-size: 0.75rem;
- line-height: 1rem;
-}
-
-.\[\&_\[cmdk-group-heading\]\]\:font-medium [cmdk-group-heading] {
- font-weight: 500;
-}
-
-.\[\&_\[cmdk-group-heading\]\]\:text-muted-foreground [cmdk-group-heading] {
- color: hsl(var(--muted-foreground));
-}
-
-.\[\&_\[cmdk-group\]\:not\(\[hidden\]\)_\~\[cmdk-group\]\]\:pt-0 [cmdk-group]:not([hidden]) ~[cmdk-group] {
- padding-top: 0px;
-}
-
-.\[\&_\[cmdk-group\]\]\:px-2 [cmdk-group] {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-
-.\[\&_\[cmdk-input-wrapper\]_svg\]\:h-5 [cmdk-input-wrapper] svg {
- height: 1.25rem;
-}
-
-.\[\&_\[cmdk-input-wrapper\]_svg\]\:w-5 [cmdk-input-wrapper] svg {
- width: 1.25rem;
-}
-
-.\[\&_\[cmdk-input\]\]\:h-12 [cmdk-input] {
- height: 3rem;
-}
-
-.\[\&_\[cmdk-item\]\]\:px-2 [cmdk-item] {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-
-.\[\&_\[cmdk-item\]\]\:py-3 [cmdk-item] {
- padding-top: 0.75rem;
- padding-bottom: 0.75rem;
-}
-
-.\[\&_\[cmdk-item\]_svg\]\:h-5 [cmdk-item] svg {
- height: 1.25rem;
-}
-
-.\[\&_\[cmdk-item\]_svg\]\:w-5 [cmdk-item] svg {
- width: 1.25rem;
-}
-
-.\[\&_svg\]\:pointer-events-none svg {
- pointer-events: none;
-}
-
-.\[\&_svg\]\:size-4 svg {
- width: 1rem;
- height: 1rem;
-}
-
-.\[\&_svg\]\:shrink-0 svg {
- flex-shrink: 0;
-}
diff --git a/apps/extension/public/logo.svg b/apps/extension/public/logo.svg
deleted file mode 100644
index 5ca15922..00000000
--- a/apps/extension/public/logo.svg
+++ /dev/null
@@ -1,11 +0,0 @@
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="42"
- height="42"
- viewBox="0 0 42 42"
- >
- <path
- className="fill-gray-700 dark:fill-gray-100"
- d="M19.0357 8C20.5531 8 21 9.27461 21 10.8438V16.3281H23.5536V14.2212C23.5536 13.1976 23.9468 12.216 24.6467 11.4922L25.0529 11.0721C24.9729 10.8772 24.9286 10.6627 24.9286 10.4375C24.9286 9.54004 25.6321 8.8125 26.5 8.8125C27.3679 8.8125 28.0714 9.54004 28.0714 10.4375C28.0714 11.335 27.3679 12.0625 26.5 12.0625C26.2822 12.0625 26.0748 12.0167 25.8863 11.9339L25.4801 12.354C25.0012 12.8492 24.7321 13.5209 24.7321 14.2212V16.3281H28.9714C29.2045 15.7326 29.7691 15.3125 30.4286 15.3125C31.2964 15.3125 32 16.04 32 16.9375C32 17.835 31.2964 18.5625 30.4286 18.5625C29.7691 18.5625 29.2045 18.1424 28.9714 17.5469H21V21.2031H25.0428C25.2759 20.6076 25.8405 20.1875 26.5 20.1875C27.3679 20.1875 28.0714 20.915 28.0714 21.8125C28.0714 22.71 27.3679 23.4375 26.5 23.4375C25.8405 23.4375 25.2759 23.0174 25.0428 22.4219H21V26.0781H24.4125C25.4023 26.0781 26.3516 26.4847 27.0515 27.2085L29.0292 29.2536C29.2177 29.1708 29.4251 29.125 29.6429 29.125C30.5107 29.125 31.2143 29.8525 31.2143 30.75C31.2143 31.6475 30.5107 32.375 29.6429 32.375C28.775 32.375 28.0714 31.6475 28.0714 30.75C28.0714 30.5248 28.1157 30.3103 28.1958 30.1154L26.2181 28.0703C25.7392 27.5751 25.0897 27.2969 24.4125 27.2969H21V31.1562C21 32.7254 20.5531 34 19.0357 34C17.6165 34 16.4478 32.8879 16.3004 31.4559C16.0451 31.527 15.775 31.5625 15.5 31.5625C13.7665 31.5625 12.3571 30.1051 12.3571 28.3125C12.3571 27.9367 12.421 27.5711 12.5339 27.2359C11.0509 26.657 10 25.1742 10 23.4375C10 21.8176 10.9183 20.416 12.2491 19.766C11.8219 19.2125 11.5714 18.5117 11.5714 17.75C11.5714 16.191 12.6321 14.891 14.0464 14.5711C13.9679 14.2918 13.9286 13.9922 13.9286 13.6875C13.9286 12.1691 14.9402 10.8895 16.3004 10.534C16.4478 9.11211 17.6165 8 19.0357 8Z"
- />
- </svg> \ No newline at end of file
diff --git a/apps/extension/scripts/content.tsx b/apps/extension/scripts/content.tsx
deleted file mode 100644
index a7668545..00000000
--- a/apps/extension/scripts/content.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import React, { StrictMode } from "react";
-import { createRoot, Root } from "react-dom/client";
-import SupermemoryContent from "../src/content";
-import { css } from "./css";
-
-let globalRoot: Root | null = null;
-
-// Function to cleanup React app
-function cleanup() {
- const container = document.getElementById("supermemory-root");
- if (container) {
- globalRoot?.unmount();
- container.remove();
- }
-}
-
-async function injectReactApp() {
- // Create a shadow DOM root
- const containerId = "supermemory-root";
- let container = document.getElementById(containerId);
-
- if (!container) {
- container = document.createElement("div");
- container.id = containerId;
- container.style.position = "absolute";
- container.style.top = "0";
- container.style.left = "0";
- container.style.width = "100%";
- container.style.height = "100%";
- container.style.zIndex = "2147483647"; // Maximum z-index value
-
- // Add the container to document first
- document.body.appendChild(container);
-
- // Attach shadow DOM
- const shadowRoot = container.attachShadow({ mode: "open" });
-
- // Create an inner container for React
- const reactContainer = document.createElement("div");
- shadowRoot.appendChild(reactContainer);
-
- // Inject global CSS into shadow DOM by fetching the CSS file content
- // const cssURL = chrome.runtime.getURL("globals.css");
- // const cssURL = `chrome-extension://${chrome.runtime.id}/globals.css`;
-
- // i'm, fucking tired lol.
- // const response = await fetch(cssURL);
- // const cssText = await response.text();
- const globalStyle = document.createElement("style");
- globalStyle.textContent = css;
- shadowRoot.appendChild(globalStyle);
-
- const root = createRoot(reactContainer);
- globalRoot = root;
- root.render(<SupermemoryContent onClose={cleanup} />);
- }
-}
-
-// check if there's already our react container
-if (document.getElementById("supermemory-root")) {
- cleanup();
-} else {
- injectReactApp();
-}
diff --git a/apps/extension/scripts/css.tsx b/apps/extension/scripts/css.tsx
deleted file mode 100644
index 5e8701a2..00000000
--- a/apps/extension/scripts/css.tsx
+++ /dev/null
@@ -1,1584 +0,0 @@
-export const css = `*, ::before, ::after {
- --tw-border-spacing-x: 0;
- --tw-border-spacing-y: 0;
- --tw-translate-x: 0;
- --tw-translate-y: 0;
- --tw-rotate: 0;
- --tw-skew-x: 0;
- --tw-skew-y: 0;
- --tw-scale-x: 1;
- --tw-scale-y: 1;
- --tw-pan-x: ;
- --tw-pan-y: ;
- --tw-pinch-zoom: ;
- --tw-scroll-snap-strictness: proximity;
- --tw-gradient-from-position: ;
- --tw-gradient-via-position: ;
- --tw-gradient-to-position: ;
- --tw-ordinal: ;
- --tw-slashed-zero: ;
- --tw-numeric-figure: ;
- --tw-numeric-spacing: ;
- --tw-numeric-fraction: ;
- --tw-ring-inset: ;
- --tw-ring-offset-width: 0px;
- --tw-ring-offset-color: #fff;
- --tw-ring-color: rgb(59 130 246 / 0.5);
- --tw-ring-offset-shadow: 0 0 #0000;
- --tw-ring-shadow: 0 0 #0000;
- --tw-shadow: 0 0 #0000;
- --tw-shadow-colored: 0 0 #0000;
- --tw-blur: ;
- --tw-brightness: ;
- --tw-contrast: ;
- --tw-grayscale: ;
- --tw-hue-rotate: ;
- --tw-invert: ;
- --tw-saturate: ;
- --tw-sepia: ;
- --tw-drop-shadow: ;
- --tw-backdrop-blur: ;
- --tw-backdrop-brightness: ;
- --tw-backdrop-contrast: ;
- --tw-backdrop-grayscale: ;
- --tw-backdrop-hue-rotate: ;
- --tw-backdrop-invert: ;
- --tw-backdrop-opacity: ;
- --tw-backdrop-saturate: ;
- --tw-backdrop-sepia: ;
- --tw-contain-size: ;
- --tw-contain-layout: ;
- --tw-contain-paint: ;
- --tw-contain-style: ;
-}
-
-::backdrop {
- --tw-border-spacing-x: 0;
- --tw-border-spacing-y: 0;
- --tw-translate-x: 0;
- --tw-translate-y: 0;
- --tw-rotate: 0;
- --tw-skew-x: 0;
- --tw-skew-y: 0;
- --tw-scale-x: 1;
- --tw-scale-y: 1;
- --tw-pan-x: ;
- --tw-pan-y: ;
- --tw-pinch-zoom: ;
- --tw-scroll-snap-strictness: proximity;
- --tw-gradient-from-position: ;
- --tw-gradient-via-position: ;
- --tw-gradient-to-position: ;
- --tw-ordinal: ;
- --tw-slashed-zero: ;
- --tw-numeric-figure: ;
- --tw-numeric-spacing: ;
- --tw-numeric-fraction: ;
- --tw-ring-inset: ;
- --tw-ring-offset-width: 0px;
- --tw-ring-offset-color: #fff;
- --tw-ring-color: rgb(59 130 246 / 0.5);
- --tw-ring-offset-shadow: 0 0 #0000;
- --tw-ring-shadow: 0 0 #0000;
- --tw-shadow: 0 0 #0000;
- --tw-shadow-colored: 0 0 #0000;
- --tw-blur: ;
- --tw-brightness: ;
- --tw-contrast: ;
- --tw-grayscale: ;
- --tw-hue-rotate: ;
- --tw-invert: ;
- --tw-saturate: ;
- --tw-sepia: ;
- --tw-drop-shadow: ;
- --tw-backdrop-blur: ;
- --tw-backdrop-brightness: ;
- --tw-backdrop-contrast: ;
- --tw-backdrop-grayscale: ;
- --tw-backdrop-hue-rotate: ;
- --tw-backdrop-invert: ;
- --tw-backdrop-opacity: ;
- --tw-backdrop-saturate: ;
- --tw-backdrop-sepia: ;
- --tw-contain-size: ;
- --tw-contain-layout: ;
- --tw-contain-paint: ;
- --tw-contain-style: ;
-}
-
-/*
-! tailwindcss v3.4.15 | MIT License | https://tailwindcss.com
-*/
-
-/*
-1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
-2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
-*/
-
-*,
-::before,
-::after {
- box-sizing: border-box;
- /* 1 */
- border-width: 0;
- /* 2 */
- border-style: solid;
- /* 2 */
- border-color: #e5e7eb;
- /* 2 */
-}
-
-::before,
-::after {
- --tw-content: '';
-}
-
-/*
-1. Use a consistent sensible line-height in all browsers.
-2. Prevent adjustments of font size after orientation changes in iOS.
-3. Use a more readable tab size.
-4. Use the user's configured \`sans\` font-family by default.
-5. Use the user's configured \`sans\` font-feature-settings by default.
-6. Use the user's configured \`sans\` font-variation-settings by default.
-7. Disable tap highlights on iOS
-*/
-
-html,
-:host {
- line-height: 1.5;
- /* 1 */
- -webkit-text-size-adjust: 100%;
- /* 2 */
- -moz-tab-size: 4;
- /* 3 */
- -o-tab-size: 4;
- tab-size: 4;
- /* 3 */
- font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
- /* 4 */
- font-feature-settings: normal;
- /* 5 */
- font-variation-settings: normal;
- /* 6 */
- -webkit-tap-highlight-color: transparent;
- /* 7 */
-}
-
-/*
-1. Remove the margin in all browsers.
-2. Inherit line-height from \`html\` so users can set them as a class directly on the \`html\` element.
-*/
-
-body {
- margin: 0;
- /* 1 */
- line-height: inherit;
- /* 2 */
-}
-
-/*
-1. Add the correct height in Firefox.
-2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
-3. Ensure horizontal rules are visible by default.
-*/
-
-hr {
- height: 0;
- /* 1 */
- color: inherit;
- /* 2 */
- border-top-width: 1px;
- /* 3 */
-}
-
-/*
-Add the correct text decoration in Chrome, Edge, and Safari.
-*/
-
-abbr:where([title]) {
- -webkit-text-decoration: underline dotted;
- text-decoration: underline dotted;
-}
-
-/*
-Remove the default font size and weight for headings.
-*/
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- font-size: inherit;
- font-weight: inherit;
-}
-
-/*
-Reset links to optimize for opt-in styling instead of opt-out.
-*/
-
-a {
- color: inherit;
- text-decoration: inherit;
-}
-
-/*
-Add the correct font weight in Edge and Safari.
-*/
-
-b,
-strong {
- font-weight: bolder;
-}
-
-/*
-1. Use the user's configured \`mono\` font-family by default.
-2. Use the user's configured \`mono\` font-feature-settings by default.
-3. Use the user's configured \`mono\` font-variation-settings by default.
-4. Correct the odd \`em\` font sizing in all browsers.
-*/
-
-code,
-kbd,
-samp,
-pre {
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
- /* 1 */
- font-feature-settings: normal;
- /* 2 */
- font-variation-settings: normal;
- /* 3 */
- font-size: 1em;
- /* 4 */
-}
-
-/*
-Add the correct font size in all browsers.
-*/
-
-small {
- font-size: 80%;
-}
-
-/*
-Prevent \`sub\` and \`sup\` elements from affecting the line height in all browsers.
-*/
-
-sub,
-sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
-}
-
-sub {
- bottom: -0.25em;
-}
-
-sup {
- top: -0.5em;
-}
-
-/*
-1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
-2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
-3. Remove gaps between table borders by default.
-*/
-
-table {
- text-indent: 0;
- /* 1 */
- border-color: inherit;
- /* 2 */
- border-collapse: collapse;
- /* 3 */
-}
-
-/*
-1. Change the font styles in all browsers.
-2. Remove the margin in Firefox and Safari.
-3. Remove default padding in all browsers.
-*/
-
-button,
-input,
-optgroup,
-select,
-textarea {
- font-family: inherit;
- /* 1 */
- font-feature-settings: inherit;
- /* 1 */
- font-variation-settings: inherit;
- /* 1 */
- font-size: 100%;
- /* 1 */
- font-weight: inherit;
- /* 1 */
- line-height: inherit;
- /* 1 */
- letter-spacing: inherit;
- /* 1 */
- color: inherit;
- /* 1 */
- margin: 0;
- /* 2 */
- padding: 0;
- /* 3 */
-}
-
-/*
-Remove the inheritance of text transform in Edge and Firefox.
-*/
-
-button,
-select {
- text-transform: none;
-}
-
-/*
-1. Correct the inability to style clickable types in iOS and Safari.
-2. Remove default button styles.
-*/
-
-button,
-input:where([type='button']),
-input:where([type='reset']),
-input:where([type='submit']) {
- -webkit-appearance: button;
- /* 1 */
- background-color: transparent;
- /* 2 */
- background-image: none;
- /* 2 */
-}
-
-/*
-Use the modern Firefox focus style for all focusable elements.
-*/
-
-:-moz-focusring {
- outline: auto;
-}
-
-/*
-Remove the additional \`:invalid\` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
-*/
-
-:-moz-ui-invalid {
- box-shadow: none;
-}
-
-/*
-Add the correct vertical alignment in Chrome and Firefox.
-*/
-
-progress {
- vertical-align: baseline;
-}
-
-/*
-Correct the cursor style of increment and decrement buttons in Safari.
-*/
-
-::-webkit-inner-spin-button,
-::-webkit-outer-spin-button {
- height: auto;
-}
-
-/*
-1. Correct the odd appearance in Chrome and Safari.
-2. Correct the outline style in Safari.
-*/
-
-[type='search'] {
- -webkit-appearance: textfield;
- /* 1 */
- outline-offset: -2px;
- /* 2 */
-}
-
-/*
-Remove the inner padding in Chrome and Safari on macOS.
-*/
-
-::-webkit-search-decoration {
- -webkit-appearance: none;
-}
-
-/*
-1. Correct the inability to style clickable types in iOS and Safari.
-2. Change font properties to \`inherit\` in Safari.
-*/
-
-::-webkit-file-upload-button {
- -webkit-appearance: button;
- /* 1 */
- font: inherit;
- /* 2 */
-}
-
-/*
-Add the correct display in Chrome and Safari.
-*/
-
-summary {
- display: list-item;
-}
-
-/*
-Removes the default spacing and border for appropriate elements.
-*/
-
-blockquote,
-dl,
-dd,
-h1,
-h2,
-h3,
-h4,
-h5,
-h6,
-hr,
-figure,
-p,
-pre {
- margin: 0;
-}
-
-fieldset {
- margin: 0;
- padding: 0;
-}
-
-legend {
- padding: 0;
-}
-
-ol,
-ul,
-menu {
- list-style: none;
- margin: 0;
- padding: 0;
-}
-
-/*
-Reset default styling for dialogs.
-*/
-
-dialog {
- padding: 0;
-}
-
-/*
-Prevent resizing textareas horizontally by default.
-*/
-
-textarea {
- resize: vertical;
-}
-
-/*
-1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
-2. Set the default placeholder color to the user's configured gray 400 color.
-*/
-
-input::-moz-placeholder, textarea::-moz-placeholder {
- opacity: 1;
- /* 1 */
- color: #9ca3af;
- /* 2 */
-}
-
-input::placeholder,
-textarea::placeholder {
- opacity: 1;
- /* 1 */
- color: #9ca3af;
- /* 2 */
-}
-
-/*
-Set the default cursor for buttons.
-*/
-
-button,
-[role="button"] {
- cursor: pointer;
-}
-
-/*
-Make sure disabled buttons don't get the pointer cursor.
-*/
-
-:disabled {
- cursor: default;
-}
-
-/*
-1. Make replaced elements \`display: block\` by default. (https://github.com/mozdevs/cssremedy/issues/14)
-2. Add \`vertical-align: middle\` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
- This can trigger a poorly considered lint error in some tools but is included by design.
-*/
-
-img,
-svg,
-video,
-canvas,
-audio,
-iframe,
-embed,
-object {
- display: block;
- /* 1 */
- vertical-align: middle;
- /* 2 */
-}
-
-/*
-Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
-*/
-
-img,
-video {
- max-width: 100%;
- height: auto;
-}
-
-/* Make elements with the HTML hidden attribute stay hidden by default */
-
-[hidden]:where(:not([hidden="until-found"])) {
- display: none;
-}
-
-:root {
- --background: 0 0% 100%;
- --foreground: 222.2 47.4% 11.2%;
- --muted: 210 40% 96.1%;
- --muted-foreground: 215.4 16.3% 46.9%;
- --popover: 0 0% 100%;
- --popover-foreground: 222.2 47.4% 11.2%;
- --border: 214.3 31.8% 91.4%;
- --input: 214.3 31.8% 91.4%;
- --card: 0 0% 100%;
- --card-foreground: 222.2 47.4% 11.2%;
- --primary: 222.2 47.4% 11.2%;
- --primary-foreground: 210 40% 98%;
- --secondary: 210 40% 96.1%;
- --secondary-foreground: 222.2 47.4% 11.2%;
- --accent: 210 40% 96.1%;
- --accent-foreground: 222.2 47.4% 11.2%;
- --destructive: 0 100% 50%;
- --destructive-foreground: 210 40% 98%;
- --ring: 215 20.2% 65.1%;
- --radius: 0.5rem;
-}
-
-* {
- border-color: hsl(var(--border));
-}
-
-body {
- background-color: hsl(var(--background));
- font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
- color: hsl(var(--foreground));
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-.container {
- width: 100%;
-}
-
-@media (min-width: 640px) {
- .container {
- max-width: 640px;
- }
-}
-
-@media (min-width: 768px) {
- .container {
- max-width: 768px;
- }
-}
-
-@media (min-width: 1024px) {
- .container {
- max-width: 1024px;
- }
-}
-
-@media (min-width: 1280px) {
- .container {
- max-width: 1280px;
- }
-}
-
-@media (min-width: 1536px) {
- .container {
- max-width: 1536px;
- }
-}
-
-.sr-only {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border-width: 0;
-}
-
-.visible {
- visibility: visible;
-}
-
-.fixed {
- position: fixed;
-}
-
-.absolute {
- position: absolute;
-}
-
-.relative {
- position: relative;
-}
-
-.inset-0 {
- inset: 0px;
-}
-
-.left-0 {
- left: 0px;
-}
-
-.left-\[50\%\] {
- left: 50%;
-}
-
-.right-0 {
- right: 0px;
-}
-
-.right-4 {
- right: 1rem;
-}
-
-.top-4 {
- top: 1rem;
-}
-
-.top-\[50\%\] {
- top: 50%;
-}
-
-.top-full {
- top: 100%;
-}
-
-.z-50 {
- z-index: 50;
-}
-
-.-mx-1 {
- margin-left: -0.25rem;
- margin-right: -0.25rem;
-}
-
-.ml-auto {
- margin-left: auto;
-}
-
-.mr-2 {
- margin-right: 0.5rem;
-}
-
-.mt-1 {
- margin-top: 0.25rem;
-}
-
-.flex {
- display: flex;
-}
-
-.inline-flex {
- display: inline-flex;
-}
-
-.grid {
- display: grid;
-}
-
-.hidden {
- display: none;
-}
-
-.h-1 {
- height: 0.25rem;
-}
-
-.h-1\.5 {
- height: 0.375rem;
-}
-
-.h-10 {
- height: 2.5rem;
-}
-
-.h-2\.5 {
- height: 0.625rem;
-}
-
-.h-3 {
- height: 0.75rem;
-}
-
-.h-4 {
- height: 1rem;
-}
-
-.h-5 {
- height: 1.25rem;
-}
-
-.h-8 {
- height: 2rem;
-}
-
-.h-9 {
- height: 2.25rem;
-}
-
-.h-full {
- height: 100%;
-}
-
-.h-px {
- height: 1px;
-}
-
-.max-h-\[240px\] {
- max-height: 240px;
-}
-
-.max-h-\[300px\] {
- max-height: 300px;
-}
-
-.w-1\.5 {
- width: 0.375rem;
-}
-
-.w-2\.5 {
- width: 0.625rem;
-}
-
-.w-3 {
- width: 0.75rem;
-}
-
-.w-4 {
- width: 1rem;
-}
-
-.w-5 {
- width: 1.25rem;
-}
-
-.w-72 {
- width: 18rem;
-}
-
-.w-9 {
- width: 2.25rem;
-}
-
-.w-full {
- width: 100%;
-}
-
-.max-w-\[200px\] {
- max-width: 200px;
-}
-
-.max-w-lg {
- max-width: 32rem;
-}
-
-.flex-1 {
- flex: 1 1 0%;
-}
-
-.flex-shrink-0 {
- flex-shrink: 0;
-}
-
-.shrink-0 {
- flex-shrink: 0;
-}
-
-.translate-x-\[-50\%\] {
- --tw-translate-x: -50%;
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-.translate-y-\[-50\%\] {
- --tw-translate-y: -50%;
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-.rotate-180 {
- --tw-rotate: 180deg;
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-.transform {
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-@keyframes pulse {
- 50% {
- opacity: .5;
- }
-}
-
-.animate-pulse {
- animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
-}
-
-.cursor-default {
- cursor: default;
-}
-
-.select-none {
- -webkit-user-select: none;
- -moz-user-select: none;
- user-select: none;
-}
-
-.flex-col {
- flex-direction: column;
-}
-
-.flex-col-reverse {
- flex-direction: column-reverse;
-}
-
-.flex-wrap {
- flex-wrap: wrap;
-}
-
-.items-center {
- align-items: center;
-}
-
-.justify-center {
- justify-content: center;
-}
-
-.justify-between {
- justify-content: space-between;
-}
-
-.gap-0\.5 {
- gap: 0.125rem;
-}
-
-.gap-1 {
- gap: 0.25rem;
-}
-
-.gap-1\.5 {
- gap: 0.375rem;
-}
-
-.gap-2 {
- gap: 0.5rem;
-}
-
-.gap-3 {
- gap: 0.75rem;
-}
-
-.gap-4 {
- gap: 1rem;
-}
-
-.space-y-1\.5 > :not([hidden]) ~ :not([hidden]) {
- --tw-space-y-reverse: 0;
- margin-top: calc(0.375rem * calc(1 - var(--tw-space-y-reverse)));
- margin-bottom: calc(0.375rem * var(--tw-space-y-reverse));
-}
-
-.space-y-2 > :not([hidden]) ~ :not([hidden]) {
- --tw-space-y-reverse: 0;
- margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
- margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
-}
-
-.space-y-4 > :not([hidden]) ~ :not([hidden]) {
- --tw-space-y-reverse: 0;
- margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
- margin-bottom: calc(1rem * var(--tw-space-y-reverse));
-}
-
-.overflow-hidden {
- overflow: hidden;
-}
-
-.overflow-y-auto {
- overflow-y: auto;
-}
-
-.overflow-x-hidden {
- overflow-x: hidden;
-}
-
-.truncate {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.whitespace-nowrap {
- white-space: nowrap;
-}
-
-.rounded {
- border-radius: 0.25rem;
-}
-
-.rounded-full {
- border-radius: 9999px;
-}
-
-.rounded-lg {
- border-radius: var(--radius);
-}
-
-.rounded-md {
- border-radius: calc(var(--radius) - 2px);
-}
-
-.rounded-sm {
- border-radius: calc(var(--radius) - 4px);
-}
-
-.rounded-xl {
- border-radius: 0.75rem;
-}
-
-.border {
- border-width: 1px;
-}
-
-.border-b {
- border-bottom-width: 1px;
-}
-
-.border-blue-500 {
- --tw-border-opacity: 1;
- border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
-}
-
-.border-input {
- border-color: hsl(var(--input));
-}
-
-.border-transparent {
- border-color: transparent;
-}
-
-.border-white\/20 {
- border-color: rgb(255 255 255 / 0.2);
-}
-
-.border-white\/50 {
- border-color: rgb(255 255 255 / 0.5);
-}
-
-.bg-background {
- background-color: hsl(var(--background));
-}
-
-.bg-black\/80 {
- background-color: rgb(0 0 0 / 0.8);
-}
-
-.bg-blue-400 {
- --tw-bg-opacity: 1;
- background-color: rgb(96 165 250 / var(--tw-bg-opacity, 1));
-}
-
-.bg-blue-500 {
- --tw-bg-opacity: 1;
- background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
-}
-
-.bg-border {
- background-color: hsl(var(--border));
-}
-
-.bg-destructive {
- background-color: hsl(var(--destructive));
-}
-
-.bg-gray-400 {
- --tw-bg-opacity: 1;
- background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1));
-}
-
-.bg-gray-800 {
- --tw-bg-opacity: 1;
- background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1));
-}
-
-.bg-gray-900 {
- --tw-bg-opacity: 1;
- background-color: rgb(17 24 39 / var(--tw-bg-opacity, 1));
-}
-
-.bg-popover {
- background-color: hsl(var(--popover));
-}
-
-.bg-primary {
- background-color: hsl(var(--primary));
-}
-
-.bg-secondary {
- background-color: hsl(var(--secondary));
-}
-
-.bg-transparent {
- background-color: transparent;
-}
-
-.bg-white\/10 {
- background-color: rgb(255 255 255 / 0.1);
-}
-
-.bg-white\/20 {
- background-color: rgb(255 255 255 / 0.2);
-}
-
-.bg-gradient-to-br {
- background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
-}
-
-.from-gray-800 {
- --tw-gradient-from: #1f2937 var(--tw-gradient-from-position);
- --tw-gradient-to: rgb(31 41 55 / 0) var(--tw-gradient-to-position);
- --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
-}
-
-.to-gray-900 {
- --tw-gradient-to: #111827 var(--tw-gradient-to-position);
-}
-
-.p-0 {
- padding: 0px;
-}
-
-.p-1 {
- padding: 0.25rem;
-}
-
-.p-4 {
- padding: 1rem;
-}
-
-.p-6 {
- padding: 1.5rem;
-}
-
-.px-2 {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-
-.px-2\.5 {
- padding-left: 0.625rem;
- padding-right: 0.625rem;
-}
-
-.px-3 {
- padding-left: 0.75rem;
- padding-right: 0.75rem;
-}
-
-.px-4 {
- padding-left: 1rem;
- padding-right: 1rem;
-}
-
-.px-8 {
- padding-left: 2rem;
- padding-right: 2rem;
-}
-
-.py-0\.5 {
- padding-top: 0.125rem;
- padding-bottom: 0.125rem;
-}
-
-.py-1 {
- padding-top: 0.25rem;
- padding-bottom: 0.25rem;
-}
-
-.py-1\.5 {
- padding-top: 0.375rem;
- padding-bottom: 0.375rem;
-}
-
-.py-2 {
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
-}
-
-.py-3 {
- padding-top: 0.75rem;
- padding-bottom: 0.75rem;
-}
-
-.py-6 {
- padding-top: 1.5rem;
- padding-bottom: 1.5rem;
-}
-
-.text-left {
- text-align: left;
-}
-
-.text-center {
- text-align: center;
-}
-
-.text-lg {
- font-size: 1.125rem;
- line-height: 1.75rem;
-}
-
-.text-sm {
- font-size: 0.875rem;
- line-height: 1.25rem;
-}
-
-.text-xs {
- font-size: 0.75rem;
- line-height: 1rem;
-}
-
-.font-medium {
- font-weight: 500;
-}
-
-.font-semibold {
- font-weight: 600;
-}
-
-.leading-none {
- line-height: 1;
-}
-
-.tracking-tight {
- letter-spacing: -0.025em;
-}
-
-.tracking-widest {
- letter-spacing: 0.1em;
-}
-
-.text-blue-100 {
- --tw-text-opacity: 1;
- color: rgb(219 234 254 / var(--tw-text-opacity, 1));
-}
-
-.text-destructive-foreground {
- color: hsl(var(--destructive-foreground));
-}
-
-.text-foreground {
- color: hsl(var(--foreground));
-}
-
-.text-gray-100 {
- --tw-text-opacity: 1;
- color: rgb(243 244 246 / var(--tw-text-opacity, 1));
-}
-
-.text-gray-200 {
- --tw-text-opacity: 1;
- color: rgb(229 231 235 / var(--tw-text-opacity, 1));
-}
-
-.text-muted-foreground {
- color: hsl(var(--muted-foreground));
-}
-
-.text-popover-foreground {
- color: hsl(var(--popover-foreground));
-}
-
-.text-primary {
- color: hsl(var(--primary));
-}
-
-.text-primary-foreground {
- color: hsl(var(--primary-foreground));
-}
-
-.text-secondary-foreground {
- color: hsl(var(--secondary-foreground));
-}
-
-.text-white {
- --tw-text-opacity: 1;
- color: rgb(255 255 255 / var(--tw-text-opacity, 1));
-}
-
-.text-white\/70 {
- color: rgb(255 255 255 / 0.7);
-}
-
-.text-white\/90 {
- color: rgb(255 255 255 / 0.9);
-}
-
-.underline-offset-4 {
- text-underline-offset: 4px;
-}
-
-.placeholder-white\/50::-moz-placeholder {
- color: rgb(255 255 255 / 0.5);
-}
-
-.placeholder-white\/50::placeholder {
- color: rgb(255 255 255 / 0.5);
-}
-
-.opacity-50 {
- opacity: 0.5;
-}
-
-.opacity-60 {
- opacity: 0.6;
-}
-
-.opacity-70 {
- opacity: 0.7;
-}
-
-.shadow {
- --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
- --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
- box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.shadow-lg {
- --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
- --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
- box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.shadow-md {
- --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
- --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
- box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.shadow-sm {
- --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
- --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
- box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.outline-none {
- outline: 2px solid transparent;
- outline-offset: 2px;
-}
-
-.outline {
- outline-style: solid;
-}
-
-.ring-offset-background {
- --tw-ring-offset-color: hsl(var(--background));
-}
-
-.filter {
- filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
-}
-
-.transition-all {
- transition-property: all;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
-}
-
-.transition-colors {
- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
-}
-
-.transition-opacity {
- transition-property: opacity;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
-}
-
-.transition-transform {
- transition-property: transform;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
-}
-
-.duration-150 {
- transition-duration: 150ms;
-}
-
-.duration-200 {
- transition-duration: 200ms;
-}
-
-.placeholder\:text-muted-foreground::-moz-placeholder {
- color: hsl(var(--muted-foreground));
-}
-
-.placeholder\:text-muted-foreground::placeholder {
- color: hsl(var(--muted-foreground));
-}
-
-.hover\:bg-accent:hover {
- background-color: hsl(var(--accent));
-}
-
-.hover\:bg-destructive\/80:hover {
- background-color: hsl(var(--destructive) / 0.8);
-}
-
-.hover\:bg-destructive\/90:hover {
- background-color: hsl(var(--destructive) / 0.9);
-}
-
-.hover\:bg-gray-800:hover {
- --tw-bg-opacity: 1;
- background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1));
-}
-
-.hover\:bg-primary\/80:hover {
- background-color: hsl(var(--primary) / 0.8);
-}
-
-.hover\:bg-primary\/90:hover {
- background-color: hsl(var(--primary) / 0.9);
-}
-
-.hover\:bg-secondary\/80:hover {
- background-color: hsl(var(--secondary) / 0.8);
-}
-
-.hover\:bg-white\/20:hover {
- background-color: rgb(255 255 255 / 0.2);
-}
-
-.hover\:text-accent-foreground:hover {
- color: hsl(var(--accent-foreground));
-}
-
-.hover\:text-white:hover {
- --tw-text-opacity: 1;
- color: rgb(255 255 255 / var(--tw-text-opacity, 1));
-}
-
-.hover\:underline:hover {
- text-decoration-line: underline;
-}
-
-.hover\:opacity-100:hover {
- opacity: 1;
-}
-
-.focus\:border-blue-500:focus {
- --tw-border-opacity: 1;
- border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
-}
-
-.focus\:outline-none:focus {
- outline: 2px solid transparent;
- outline-offset: 2px;
-}
-
-.focus\:ring-2:focus {
- --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
- --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
- box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
-}
-
-.focus\:ring-ring:focus {
- --tw-ring-color: hsl(var(--ring));
-}
-
-.focus\:ring-offset-2:focus {
- --tw-ring-offset-width: 2px;
-}
-
-.focus-visible\:outline-none:focus-visible {
- outline: 2px solid transparent;
- outline-offset: 2px;
-}
-
-.focus-visible\:ring-1:focus-visible {
- --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
- --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
- box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
-}
-
-.focus-visible\:ring-ring:focus-visible {
- --tw-ring-color: hsl(var(--ring));
-}
-
-.disabled\:pointer-events-none:disabled {
- pointer-events: none;
-}
-
-.disabled\:cursor-not-allowed:disabled {
- cursor: not-allowed;
-}
-
-.disabled\:opacity-50:disabled {
- opacity: 0.5;
-}
-
-.data-\[disabled\=true\]\:pointer-events-none[data-disabled="true"] {
- pointer-events: none;
-}
-
-.data-\[selected\=true\]\:bg-accent[data-selected="true"] {
- background-color: hsl(var(--accent));
-}
-
-.data-\[state\=open\]\:bg-accent[data-state="open"] {
- background-color: hsl(var(--accent));
-}
-
-.data-\[selected\=true\]\:text-accent-foreground[data-selected="true"] {
- color: hsl(var(--accent-foreground));
-}
-
-.data-\[state\=open\]\:text-muted-foreground[data-state="open"] {
- color: hsl(var(--muted-foreground));
-}
-
-.data-\[disabled\=true\]\:opacity-50[data-disabled="true"] {
- opacity: 0.5;
-}
-
-@media (min-width: 640px) {
- .sm\:flex-row {
- flex-direction: row;
- }
-
- .sm\:justify-end {
- justify-content: flex-end;
- }
-
- .sm\:space-x-2 > :not([hidden]) ~ :not([hidden]) {
- --tw-space-x-reverse: 0;
- margin-right: calc(0.5rem * var(--tw-space-x-reverse));
- margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
- }
-
- .sm\:rounded-lg {
- border-radius: var(--radius);
- }
-
- .sm\:text-left {
- text-align: left;
- }
-}
-
-.\[\&_\[cmdk-group-heading\]\]\:px-2 [cmdk-group-heading] {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-
-.\[\&_\[cmdk-group-heading\]\]\:py-1\.5 [cmdk-group-heading] {
- padding-top: 0.375rem;
- padding-bottom: 0.375rem;
-}
-
-.\[\&_\[cmdk-group-heading\]\]\:text-xs [cmdk-group-heading] {
- font-size: 0.75rem;
- line-height: 1rem;
-}
-
-.\[\&_\[cmdk-group-heading\]\]\:font-medium [cmdk-group-heading] {
- font-weight: 500;
-}
-
-.\[\&_\[cmdk-group-heading\]\]\:text-muted-foreground [cmdk-group-heading] {
- color: hsl(var(--muted-foreground));
-}
-
-.\[\&_\[cmdk-group\]\:not\(\[hidden\]\)_\~\[cmdk-group\]\]\:pt-0 [cmdk-group]:not([hidden]) ~[cmdk-group] {
- padding-top: 0px;
-}
-
-.\[\&_\[cmdk-group\]\]\:px-2 [cmdk-group] {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-
-.\[\&_\[cmdk-input-wrapper\]_svg\]\:h-5 [cmdk-input-wrapper] svg {
- height: 1.25rem;
-}
-
-.\[\&_\[cmdk-input-wrapper\]_svg\]\:w-5 [cmdk-input-wrapper] svg {
- width: 1.25rem;
-}
-
-.\[\&_\[cmdk-input\]\]\:h-12 [cmdk-input] {
- height: 3rem;
-}
-
-.\[\&_\[cmdk-item\]\]\:px-2 [cmdk-item] {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-
-.\[\&_\[cmdk-item\]\]\:py-3 [cmdk-item] {
- padding-top: 0.75rem;
- padding-bottom: 0.75rem;
-}
-
-.\[\&_\[cmdk-item\]_svg\]\:h-5 [cmdk-item] svg {
- height: 1.25rem;
-}
-
-.\[\&_\[cmdk-item\]_svg\]\:w-5 [cmdk-item] svg {
- width: 1.25rem;
-}
-
-.\[\&_svg\]\:pointer-events-none svg {
- pointer-events: none;
-}
-
-.\[\&_svg\]\:size-4 svg {
- width: 1rem;
- height: 1rem;
-}
-
-.\[\&_svg\]\:shrink-0 svg {
- flex-shrink: 0;
-}
-` \ No newline at end of file
diff --git a/apps/extension/src/background.ts b/apps/extension/src/background.ts
deleted file mode 100644
index b02d027f..00000000
--- a/apps/extension/src/background.ts
+++ /dev/null
@@ -1,225 +0,0 @@
-import { getBaseURL } from "@/lib/utils";
-import {
- messageListener,
- MessageType,
- registerMessageHandler,
-} from "./helpers";
-import { handleExportXBookmarks, setupTwitterHeaderListener } from "./twitter";
-
-type TabState = {
- isActive: boolean;
-};
-
-const tabStates = new Map<number, TabState>();
-
-const checkIfLoggedIn = async () => {
- const baseURL = await getBaseURL();
- const response = await fetch(`${baseURL}/backend/v1/session`);
-
- return response.status == 200;
-};
-
-// When extension is installed
-chrome.runtime.onInstalled.addListener(async (details) => {
- const isLoggedIn = await checkIfLoggedIn();
-
- const baseURL = await getBaseURL();
- if (!isLoggedIn) {
- chrome.tabs.create({ url: `${baseURL}/signin` });
- }
-
- // TODO: show extension help page
-});
-
-// Clean up tab state when tab is closed
-chrome.tabs.onRemoved.addListener((tabId) => {
- tabStates.delete(tabId);
-});
-
-// communication with content script
-chrome.action.onClicked.addListener(async (tab) => {
- if (!tab.id) return;
-
- try {
- // Check if we can inject into this tab
- if (
- !tab.url ||
- tab.url.startsWith("chrome://") ||
- tab.url.startsWith("edge://") ||
- tab.url.startsWith("about:")
- ) {
- alert("Cannot modify Chrome system pages");
- return;
- }
-
- const baseURL = await getBaseURL();
-
- const isLoggedIn = await checkIfLoggedIn();
- if (!isLoggedIn) {
- chrome.tabs.create({ url: `${baseURL}/signin` });
- return;
- }
-
- const currentState = tabStates.get(tab.id) || { isActive: false };
- const newState = { isActive: !currentState.isActive };
-
- chrome.scripting.executeScript({
- target: { tabId: tab.id },
- files: ["scripts/content.js"],
- });
-
- // Update state
- tabStates.set(tab.id, newState);
-
- // Update icon title
- await chrome.action.setTitle({
- tabId: tab.id,
- title: newState.isActive ? "Disable SuperMemory" : "Enable SuperMemory",
- });
- } catch (error) {
- console.error("Failed to toggle content:", error);
- }
-});
-
-chrome.runtime.onMessage.addListener(messageListener);
-
-registerMessageHandler<MessageType>(
- "GET_SPACES",
- async (message, sender, sendResponse) => {
- // Handle getting spaces
- const baseURL = await getBaseURL();
- const response = await fetch(`${baseURL}/backend/v1/spaces`);
- const data = await response.json();
- sendResponse(data);
- }
-);
-
-registerMessageHandler<MessageType>(
- "EXPORT_TWITTER_BOOKMARKS",
- async (message, sender, sendResponse) => {
- handleExportXBookmarks();
- sendResponse({ status: "started" });
- }
-);
-
-registerMessageHandler<MessageType>(
- "SAVE_PAGE",
- async (message, sender, sendResponse) => {
- if (!message.payload) {
- sendResponse({ error: "No payload" });
- return;
- }
-
- const baseURL = await getBaseURL();
-
- const isLoggedIn = await checkIfLoggedIn();
- if (!isLoggedIn) {
- sendResponse({ error: "Not logged in" });
- chrome.tabs.create({ url: `${baseURL}/signin` });
- return;
- }
-
- console.log(message.payload);
-
- const response = await fetch(`${baseURL}/backend/v1/add`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- spaces: message.payload.spaces || [],
- content: message.payload.url,
- prefetched: message.payload.prefetched,
- }),
- });
- if (response.status !== 200) {
- sendResponse({ error: "Failed to save page", status: response.status });
- return;
- }
- const data = await response.json();
- // Handle saving highlight
- sendResponse({ success: true, data, status: response.status });
- }
-);
-
-registerMessageHandler<MessageType>(
- "ACTIVATE_CONTENT",
- async (message, sender, sendResponse) => {
- console.log("Activating content");
- chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
- const currentTab = tabs[0];
- if (currentTab.id) {
- try {
- await chrome.scripting.executeScript({
- target: { tabId: currentTab.id },
- files: ["scripts/content.js"],
- });
-
- } catch (error) {
- console.error("Error injecting content script:", error);
- }
- }
- });
- sendResponse({ success: true });
- }
-);
-
-registerMessageHandler<MessageType>(
- "SYNC_CHROME_BOOKMARKS",
- async (message, sender, sendResponse) => {
- // activate the chrome bookmarks syncing.
-
- // TODO: We probably want to sync bookmarks from the extension to the web app.
- chrome.bookmarks.onCreated.addListener((id, bookmark) => {
- console.log("Bookmark created:", bookmark);
- });
- }
-);
-
-registerMessageHandler<MessageType>(
- "IMPORT_CHROME_BOOKMARKS",
- async (message, sender, sendResponse) => {
- // activate the chrome bookmarks importing.
- // first get all chrome bookmarks
- chrome.bookmarks.getRecent(100, (bookmarks) => {
- console.log("Bookmarks:", bookmarks);
- });
- }
-);
-
-// External message listener
-chrome.runtime.onMessageExternal.addListener(
- async (request, sender, sendResponse) => {
- if (request.action === "exportBookmarks") {
- handleExportXBookmarks();
- sendResponse({ status: "exported" });
- return true;
- }
- if (request.action === "importBookmarks") {
- const baseURL = await getBaseURL();
- chrome.bookmarks.getRecent(100, async (bookmarks) => {
- for (const { url } of bookmarks) {
- console.log("Importing bookmark:", url);
- const r = await fetch(`${baseURL}/backend/v1/add`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ content: url, spaces: [] }),
- });
-
- const response = await r.json();
- console.log("Response:", response);
- }
- sendResponse({ status: "imported", bookmarks });
- });
- return true;
- }
- if (request.action === "ping") {
- sendResponse({ status: "pong" });
- return true;
- }
- }
-);
-
-setupTwitterHeaderListener();
diff --git a/apps/extension/src/content.tsx b/apps/extension/src/content.tsx
deleted file mode 100644
index 6848bf37..00000000
--- a/apps/extension/src/content.tsx
+++ /dev/null
@@ -1,350 +0,0 @@
-import { useEffect, useState, useRef } from "react";
-import { Readability } from "@mozilla/readability";
-
-const DEBUG = true;
-
-const log = (message: string, ...args: any[]) => {
- if (DEBUG) {
- console.log("[content]", message, ...args);
- }
-};
-
-export default function SupermemoryContent({
- onClose: originalOnClose,
-}: {
- onClose: () => void;
-}) {
- const [progress, setProgress] = useState(0);
- const [showSaving, setShowSaving] = useState(true);
- const abortControllerRef = useRef(new AbortController());
- const [toastMessage, setToastMessage] = useState<{
- success: boolean;
- message: string;
- } | null>(null);
- const isAbortedRef = useRef(false);
- const [isImportingBookmarks, setIsImportingBookmarks] = useState(false);
- const [importProgress, setImportProgress] = useState(0);
- const [closeTimer, setCloseTimer] = useState(0);
- const closeTimerRef = useRef<ReturnType<typeof setInterval>>();
- const [importStatus, setImportStatus] = useState<{
- status: number;
- message: string;
- } | null>(null);
- const [importComplete, setImportComplete] = useState(false);
-
- useEffect(() => {
- const isTwitter = window.location.hostname.match(/^(twitter\.com|x\.com)$/);
- if (isTwitter) {
- chrome.storage.local.get(["attemptingImportCurrently"], (result) => {
- if (result.attemptingImportCurrently) {
- setIsImportingBookmarks(true);
- setShowSaving(true);
- }
- });
- }
- }, []);
-
- useEffect(() => {
- const messageHandler = (
- message: any,
- sender: chrome.runtime.MessageSender,
- sendResponse: (response?: any) => void
- ) => {
- if (message.type === "IMPORT_PROGRESS_UPDATE") {
- setImportProgress(message.payload.progress);
- setShowSaving(true);
-
- switch (message.payload.status) {
- case 429:
- setImportStatus({
- status: 429,
- message: "Rate limited by Twitter. Waiting to retry...",
- });
- break;
- case 409:
- setImportStatus({
- status: 409,
- message: "Some tweets were already saved",
- });
- break;
- case 500:
- setImportStatus({
- status: 500,
- message: "Error importing some tweets",
- });
- break;
- case 102:
- setImportStatus({
- status: 102,
- message: "Processing tweets...",
- });
- break;
- default:
- setImportStatus(null);
- }
- } else if (message.type === "IMPORT_COMPLETE") {
- console.log("IMPORT_COMPLETE called");
- setImportComplete(true);
- setImportStatus(null);
- setToastMessage({
- success: true,
- message: "Bookmarks imported successfully",
- });
-
- // Send message to all supermemory.ai tabs
- chrome.tabs.query({ url: "*://*.supermemory.ai/*" }, (tabs) => {
- tabs.forEach((tab) => {
- if (tab.id) {
- chrome.tabs.sendMessage(tab.id, {
- type: "TWITTER_IMPORT_COMPLETE",
- });
- }
- });
- });
-
- // Start close timer after 2 seconds of showing success
- setTimeout(() => {
- if (!isImportingBookmarks) {
- startCloseTimer();
- }
- }, 1000);
- }
- };
-
- chrome.runtime.onMessage.addListener(messageHandler);
- return () => chrome.runtime.onMessage.removeListener(messageHandler);
- }, [isImportingBookmarks]);
-
- // Clear toast message after delay
- useEffect(() => {
- if (toastMessage && !importComplete) {
- const timer = setTimeout(() => {
- if (!isAbortedRef.current) {
- setToastMessage(null);
- }
- }, 3000); // Show toast for 3 seconds
- return () => clearTimeout(timer);
- }
- }, [toastMessage, importComplete]);
-
- const startCloseTimer = () => {
- if (closeTimerRef.current) {
- clearInterval(closeTimerRef.current);
- }
- setCloseTimer(0);
-
- let count = 0;
- closeTimerRef.current = setInterval(() => {
- if (!isAbortedRef.current) {
- count += 10;
- setCloseTimer(count);
-
- if (count >= 100) {
- if (closeTimerRef.current) {
- clearInterval(closeTimerRef.current);
- }
- handleClose();
- }
- }
- }, 200); // 20 steps of 10% over 2 seconds
- };
-
- const pauseCloseTimer = () => {
- if (closeTimerRef.current) {
- clearInterval(closeTimerRef.current);
- }
- };
-
- const savePage = async () => {
- if (isAbortedRef.current) return;
-
- const documentClone = document.cloneNode(true) as Document;
- const mainContent = new Readability(documentClone).parse();
-
- try {
- const response = await chrome.runtime.sendMessage({
- type: "SAVE_PAGE",
- payload: {
- description: mainContent?.excerpt,
- url: window.location.href,
- prefetched: {
- contentToVectorize: mainContent?.textContent,
- contentToSave: mainContent?.content,
- title: mainContent?.title,
- type: "page",
- },
- },
- });
-
- if (!isAbortedRef.current) {
- if (response.success) {
- setToastMessage({
- success: true,
- message: "Page saved successfully",
- });
- // Wait for toast to show before starting close timer
- setTimeout(() => {
- if (!isAbortedRef.current) {
- startCloseTimer();
- }
- }, 1000);
- } else {
- setToastMessage({
- success: false,
- message:
- response.status === 409
- ? "Content already exists"
- : "Failed to save page",
- });
- }
- }
-
- return response;
- } catch (error) {
- console.error("Error saving page:", error);
- setToastMessage({ success: false, message: "Failed to save page" });
- }
- };
-
- const handleClose = () => {
- isAbortedRef.current = true;
- abortControllerRef.current.abort();
- if (closeTimerRef.current) {
- clearInterval(closeTimerRef.current);
- }
- setProgress(0);
- setShowSaving(false);
- originalOnClose();
- };
-
- useEffect(() => {
- if (!showSaving || isImportingBookmarks) return;
- savePage();
- }, [showSaving]);
-
- if (!showSaving) return null;
-
- return (
- <>
- <div
- className="fixed right-4 top-4 bg-gradient-to-br from-gray-800 to-gray-900 rounded-xl text-white shadow-lg"
- style={{
- width: "400px",
- maxWidth: "calc(100vw - 40px)",
- }}
- >
- <link
- rel="stylesheet"
- href={`${chrome.runtime.getURL("globals.css")}`}
- />
- <div className="p-4 space-y-4">
- <div className="flex items-center justify-between">
- <div className="flex flex-col gap-0.5">
- <span className="text-sm font-medium text-gray-100">
- {isImportingBookmarks
- ? importComplete
- ? "Twitter Bookmarks Imported!"
- : "Importing Twitter Bookmarks"
- : "Saved to Supermemory"}
- </span>
- {toastMessage && (
- <span className="text-xs text-gray-200">
- {toastMessage.message}
- </span>
- )}
- {importStatus && (
- <span className="text-xs text-gray-200">
- {importStatus.message}
- </span>
- )}
- {isImportingBookmarks && !importComplete && (
- <div className="flex items-center gap-3 mt-2">
- <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-white"></div>
- <span className="text-2xl font-bold text-white">
- {importProgress}
- </span>
- <span className="text-xs text-gray-300 mt-1">
- tweets processed
- </span>
- </div>
- )}
- {importComplete && (
- <span className="text-lg font-semibold text-green-400">
- Successfully imported {importProgress} tweets
- </span>
- )}
- </div>
- <button
- onClick={handleClose}
- className={`text-xs font-medium px-2.5 py-1.5 ${
- importComplete
- ? "bg-green-500 hover:bg-green-600"
- : "bg-white/10 hover:bg-white/20"
- } text-white/90 hover:text-white rounded-md transition-all duration-150 flex items-center gap-1`}
- >
- {importComplete ? (
- <svg
- className="w-3 h-3"
- viewBox="0 0 24 24"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- >
- <path
- d="M20 6L9 17L4 12"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- />
- </svg>
- ) : (
- <svg
- className="w-3 h-3"
- viewBox="0 0 24 24"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- >
- <path
- d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
- stroke="currentColor"
- strokeWidth="2"
- />
- <path
- d="M15 9L9 15M9 9L15 15"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- />
- </svg>
- )}
- {importComplete
- ? "Done"
- : isImportingBookmarks
- ? "Close"
- : "Cancel"}
- </button>
- </div>
- </div>
-
- {!isImportingBookmarks && (progress > 0 || closeTimer > 0) && (
- <div className="w-full h-1 bg-gray-700 rounded-b-xl overflow-hidden">
- <div
- className={`h-full transition-all duration-200 rounded-b-xl ${
- importStatus?.status === 429
- ? "bg-yellow-500"
- : importStatus?.status === 500
- ? "bg-red-500"
- : importComplete
- ? "bg-green-500"
- : "bg-blue-500"
- }`}
- style={{
- width: `${isImportingBookmarks ? importProgress : closeTimer}%`,
- }}
- />
- </div>
- )}
- </div>
- </>
- );
-}
diff --git a/apps/extension/src/helpers.ts b/apps/extension/src/helpers.ts
deleted file mode 100644
index 96cab7e0..00000000
--- a/apps/extension/src/helpers.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-// Define message types
-export type MessageType =
- | {
- type: "SAVE_PAGE";
- payload: {
- html: string;
- url: string;
- spaces: string[];
- description: string;
- prefetched: {
- contentToVectorize: string;
- contentToSave: string;
- title: string;
- type: string;
- };
- };
- }
- | { type: "GET_SPACES"; payload: undefined }
- | { type: "EXPORT_TWITTER_BOOKMARKS"; payload: undefined }
- | { type: "ACTIVATE_CONTENT"; payload: undefined }
- | { type: "SYNC_CHROME_BOOKMARKS"; payload: undefined }
- | { type: "IMPORT_CHROME_BOOKMARKS"; payload: undefined };
-
-// Add more message types as needed
-
-// Type for message handlers
-export type MessageHandler<T extends MessageType> = (
- message: T,
- sender: chrome.runtime.MessageSender,
- sendResponse: (response?: any) => void
-) => void | Promise<void>;
-
-// Message handlers registry
-export const messageHandlers = new Map<
- MessageType["type"],
- MessageHandler<MessageType>
->();
-
-// Register message handlers
-export function registerMessageHandler<T extends MessageType>(
- type: T["type"],
- handler: MessageHandler<T>
-) {
- messageHandlers.set(type, handler as MessageHandler<MessageType>);
-}
-// Main message listener
-export const messageListener = (
- message: MessageType,
- sender: chrome.runtime.MessageSender,
- sendResponse: (response?: any) => void
-) => {
- const handler = messageHandlers.get(message.type);
- if (handler) {
- const result = handler(message, sender, sendResponse);
- if (result instanceof Promise) {
- result.catch(console.error);
- return true; // Keep message channel open for async response
- }
- } else {
- console.warn(`No handler registered for message type: ${message.type}`);
- }
- return false;
-};
-
-chrome.runtime.onMessage.addListener(messageListener);
diff --git a/apps/extension/src/twitter.constants.ts b/apps/extension/src/twitter.constants.ts
deleted file mode 100644
index 543337f7..00000000
--- a/apps/extension/src/twitter.constants.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-export const features = {
- graphql_timeline_v2_bookmark_timeline: true,
- rweb_tipjar_consumption_enabled: true,
- responsive_web_graphql_exclude_directive_enabled: true,
- verified_phone_label_enabled: false,
- creator_subscriptions_tweet_preview_api_enabled: true,
- responsive_web_graphql_timeline_navigation_enabled: true,
- responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
- communities_web_enable_tweet_community_results_fetch: true,
- c9s_tweet_anatomy_moderator_badge_enabled: true,
- articles_preview_enabled: true,
- tweetypie_unmention_optimization_enabled: true,
- responsive_web_edit_tweet_api_enabled: true,
- graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
- view_counts_everywhere_api_enabled: true,
- longform_notetweets_consumption_enabled: true,
- responsive_web_twitter_article_tweet_consumption_enabled: true,
- tweet_awards_web_tipping_enabled: false,
- creator_subscriptions_quote_tweet_preview_enabled: false,
- freedom_of_speech_not_reach_fetch_enabled: true,
- standardized_nudges_misinfo: true,
- tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
- rweb_video_timestamps_enabled: true,
- longform_notetweets_rich_text_read_enabled: true,
- longform_notetweets_inline_media_enabled: true,
- responsive_web_enhance_cards_enabled: false,
- profile_label_improvements_pcf_label_in_post_enabled: false,
- premium_content_api_read_enabled: true,
- responsive_web_grok_analyze_button_fetch_trends_enabled: true,
- responsive_web_grok_analyze_post_followups_enabled: true,
- responsive_web_grok_share_attachment_enabled: true,
- }; \ No newline at end of file
diff --git a/apps/extension/src/twitter.ts b/apps/extension/src/twitter.ts
deleted file mode 100644
index 09118973..00000000
--- a/apps/extension/src/twitter.ts
+++ /dev/null
@@ -1,470 +0,0 @@
-import { getBaseURL } from "@/lib/utils";
-import { features } from "./twitter.constants";
-
-const DEBUG = true;
-
-const log = (message: string, ...args: any[]) => {
- if (DEBUG) {
- console.log("[twitter]", message, ...args);
- }
-};
-
-const logError = (message: string, error?: any) => {
- // Always log errors
- console.error(message, error);
-};
-
-export const waitForRequiredData = () => {
- return new Promise((resolve) => {
- const checkData = () => {
- chrome.storage.local.get(
- ["bookmarksApiId", "cookie", "csrf", "auth"],
- (result) => {
- if (
- result.bookmarksApiId &&
- result.cookie &&
- result.csrf &&
- result.auth
- ) {
- log("Got all data needed to fetch bookmarks");
- resolve(true);
- } else {
- setTimeout(checkData, 100); // Check again after 100ms
- }
- }
- );
- };
- checkData();
- });
-};
-
-export const handleExportXBookmarks = async () => {
- log("Received export request from popup");
-
- // First, create the Twitter tab
- const tab = await chrome.tabs.create({
- url: "https://x.com/i/bookmarks/all",
- });
-
- // Wait for the tab to finish loading
- await new Promise((resolve) => {
- const checkTab = () => {
- if (tab.id) {
- chrome.tabs.get(tab.id, (updatedTab) => {
- if (updatedTab.status === "complete") {
- resolve(true);
- } else {
- setTimeout(checkTab, 100);
- }
- });
- }
- };
- checkTab();
- });
-
- void chrome.storage.local.set({ attemptingImportCurrently: true });
-
- chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
- const currentTab = tabs[0];
- if (currentTab.id) {
- try {
- await chrome.scripting.executeScript({
- target: { tabId: currentTab.id },
- files: ["scripts/content.js"],
- });
- } catch (error) {
- console.error("Error injecting content script:", error);
- }
- }
- });
-
- // Wait for required data and start the bookmark export
- await waitForRequiredData();
- await sendProgressUpdate(0, 200);
-
- await getBookmarks();
-
- return { status: 200 };
-};
-
-const getNextCursor = (entries: any[]) => {
- const cursorEntry = entries.find((entry) =>
- entry.entryId.startsWith("cursor-bottom-")
- );
- return cursorEntry ? cursorEntry.content.value : null;
-};
-
-type MessageType = "IMPORT_PROGRESS_UPDATE" | "IMPORT_COMPLETE";
-
-const sendMessageToTwitterTabs = async (
- message: {
- type: MessageType;
- payload: any;
- },
- retries = 3
-) => {
- try {
- log(`Sending message of type ${message.type}:`, message.payload);
-
- const tabs = await chrome.tabs.query({
- url: ["*://*.twitter.com/*", "*://*.x.com/*"],
- });
-
- if (tabs.length === 0) {
- if (retries > 0) {
- // Wait and retry if no matching tab found
- await new Promise((resolve) => setTimeout(resolve, 1000));
- return sendMessageToTwitterTabs(message, retries - 1);
- }
- throw new Error("No Twitter tab found");
- }
-
- const messagePromises = tabs.map((tab) => {
- if (!tab.id) return Promise.resolve();
- return chrome.tabs.sendMessage(tab.id, message).catch((error) => {
- logError(`Failed to send message to tab ${tab.id}:`, error);
- });
- });
-
- await Promise.all(messagePromises);
- log(`Message sent to ${tabs.length} tabs`);
- } catch (error) {
- logError(`Error sending message of type ${message.type}:`, error);
- }
-};
-
-const sendProgressUpdate = (progress: number, status: number) => {
- return sendMessageToTwitterTabs({
- type: "IMPORT_PROGRESS_UPDATE",
- payload: { progress, status },
- });
-};
-
-const sendImportComplete = () => {
- return sendMessageToTwitterTabs({
- type: "IMPORT_COMPLETE",
- payload: { success: true },
- });
-};
-
-const getBookmarks = async (cursor = "", totalImported = 0, allTweets = []) => {
- try {
- const getSessionData = (): Promise<{
- cookie: string;
- csrf: string;
- auth: string;
- bookmarksApiId: string;
- }> => {
- return new Promise((resolve) => {
- const checkData = () => {
- chrome.storage.local.get(
- ["cookie", "csrf", "auth", "bookmarksApiId"],
- (result) => {
- if (
- result.cookie &&
- result.csrf &&
- result.auth &&
- result.bookmarksApiId
- ) {
- resolve({
- cookie: result.cookie,
- csrf: result.csrf,
- auth: result.auth,
- bookmarksApiId: result.bookmarksApiId,
- });
- } else {
- setTimeout(checkData, 3000); // Check again after 3 seconds
- }
- }
- );
- };
- checkData();
- });
- };
-
- const sessionResult = await getSessionData();
- const baseURL = await getBaseURL();
-
- const headers = new Headers();
- headers.append("Cookie", sessionResult.cookie);
- headers.append("X-Csrf-token", sessionResult.csrf);
- headers.append("Authorization", sessionResult.auth);
-
- const variables = {
- count: 100,
- cursor: cursor,
- includePromotedContent: false,
- };
- const API_URL = `https://x.com/i/api/graphql/${
- sessionResult.bookmarksApiId
- }/Bookmarks?features=${encodeURIComponent(
- JSON.stringify(features)
- )}&variables=${encodeURIComponent(JSON.stringify(variables))}` as const;
-
- const response = await fetch(API_URL, {
- method: "GET",
- headers: headers,
- redirect: "follow",
- });
-
- if (response.status === 429) {
- log("Rate limited, waiting 60 seconds before retrying...");
- await sendProgressUpdate(totalImported, 429);
- await new Promise((resolve) => setTimeout(resolve, 60000));
- return getBookmarks(cursor, totalImported, allTweets);
- }
-
- let data = (await response.json()) as any;
-
- // Handle 400 error with missing features
- if (response.status === 400 && data.errors?.[0]?.message) {
- const errorMsg = data.errors[0].message;
- const missingFeatures = errorMsg.match(
- /following features cannot be null: (.*?)$/
- )?.[1];
-
- if (missingFeatures) {
- const featuresList = missingFeatures.split(", ");
- const updatedFeatures = { ...features };
-
- featuresList.forEach((feature: string) => {
- updatedFeatures[feature as keyof typeof updatedFeatures] = true;
- });
-
- // Retry with updated features
- const newUrl = `https://x.com/i/api/graphql/${
- sessionResult.bookmarksApiId
- }/Bookmarks?features=${encodeURIComponent(
- JSON.stringify(updatedFeatures)
- )}&variables=${encodeURIComponent(JSON.stringify(variables))}`;
-
- const retryResponse = await fetch(newUrl, {
- method: "GET",
- headers: headers,
- redirect: "follow",
- });
-
- const retryData = await retryResponse.json();
- data = retryData;
- }
- }
-
- const entries =
- data.data?.bookmark_timeline_v2?.timeline?.instructions?.[0]?.entries ||
- [];
-
- const tweetEntries = entries.filter((entry: { entryId: string }) =>
- entry.entryId.startsWith("tweet-")
- );
-
- // Process tweets and create URLs
- const tweetUrls = tweetEntries
- .map((tweet: any) => {
- const tweetId = tweet?.entryId?.split("-")[1];
- const username =
- tweet?.content?.itemContent?.tweet_results?.result?.core?.user_results
- ?.result?.legacy?.screen_name;
-
- if (!tweetId) return null;
- return `https://x.com/${username || "supermemoryai"}/status/${tweetId}`;
- })
- .filter(Boolean);
-
- // Send progress update before processing batch
- await sendProgressUpdate(totalImported, 102); // 102 = Processing batch
-
- // Send all tweets in parallel
- const addRequests = tweetUrls.map((tweetUrl: string) =>
- fetch(`${baseURL}/backend/v1/add`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- content: tweetUrl,
- }),
- })
- );
-
- const results = await Promise.all(addRequests);
-
- // Track different response statuses
- const statusCounts = results.reduce((acc: { [key: number]: number }, r) => {
- const status = r.status;
- acc[status] = (acc[status] || 0) + 1;
- return acc;
- }, {});
-
- // Log status counts
- Object.entries(statusCounts).forEach(([status, count]) => {
- log(`Status ${status}: ${count} tweets`);
- });
-
- // If we have any 409s, send that in the progress update
- if (statusCounts[409]) {
- await sendProgressUpdate(totalImported, 409);
- }
-
- // Consider request successful even if some tweets already exist (409)
- const failedRequests = results.filter(
- (r) => !r.ok && r.status !== 409
- ).length;
- if (failedRequests > 0) {
- await sendProgressUpdate(totalImported, 500); // 500 = Some requests failed
- }
-
- allTweets = allTweets.concat(tweetEntries);
- const newTweetsCount = tweetEntries.length;
- totalImported += newTweetsCount;
-
- // Update progress based on total tweets found so far
- const estimatedTotalTweets = Math.max(totalImported, 200); // Adjust estimate as we find more
- await sendProgressUpdate(
- totalImported,
- failedRequests > 0 ? 500 : statusCounts[409] ? 409 : 200
- );
-
- log(
- `Processing batch: ${newTweetsCount} new tweets, total: ${totalImported}`
- );
- const nextCursor = getNextCursor(entries);
-
- if (nextCursor && newTweetsCount > 0) {
- // Add delay between batches to avoid rate limiting
- await new Promise((resolve) => setTimeout(resolve, 1500));
- return getBookmarks(nextCursor, totalImported, allTweets);
- } else {
- // Final success update
- await sendImportComplete();
-
- chrome.storage.local.set({ bookmarks: allTweets }, () => {
- log("Bookmarks stored in local storage");
- });
- const timestamp = new Date().toISOString();
- chrome.storage.local.get(["successful_exports"], (result) => {
- const successful_exports = result.successful_exports || [];
- successful_exports.push({
- timestamp,
- tweetCount: allTweets.length,
- });
- chrome.storage.local.set({ successful_exports }, () => {
- log(`Export completed: ${allTweets.length} tweets`);
- });
- });
- void chrome.storage.local.set({ attemptingImportCurrently: false });
-
- // After successful import completion:
- window.postMessage(
- { type: "TWITTER_IMPORT_COMPLETE" },
- window.location.origin
- );
- }
- } catch (error) {
- logError("Error fetching bookmarks:", error);
- void chrome.storage.local.set({ attemptingImportCurrently: false });
- }
-};
-
-export const setupTwitterHeaderListener = () => {
- chrome.webRequest.onBeforeSendHeaders.addListener(
- (details) => {
- try {
- // Validate input
- if (!details || !details.url) {
- console.error("Invalid details object received");
- return;
- }
-
- // Check if URL is from Twitter/X
- if (
- !(
- details.url.includes("x.com") || details.url.includes("twitter.com")
- )
- ) {
- return;
- }
-
- // Ensure requestHeaders exists
- if (!details.requestHeaders) {
- console.error("No request headers found");
- return;
- }
-
- // Check if stuff is already stored
- chrome.storage.local.get(
- ["bookmarksApiId", "cookie", "csrf", "auth"],
- (result) => {
- try {
- // Check if the URL matches the pattern for bookmarks API
- const bookmarksUrlPattern =
- /https:\/\/x\.com\/i\/api\/graphql\/([^/]+)\/Bookmarks\?/;
- const match = details.url.match(bookmarksUrlPattern);
-
- if (match?.[1] && !result.bookmarksApiId) {
- const bookmarksApiId = match[1];
- chrome.storage.local.set({ bookmarksApiId }, () => {
- if (chrome.runtime.lastError) {
- console.error(
- "Error storing bookmarksApiId:",
- chrome.runtime.lastError
- );
- return;
- }
- console.log(`Stored bookmarksApiId: ${bookmarksApiId}`);
- });
- }
-
- // Extract headers with error handling
- const getHeaderValue = (name: string): string => {
- const header = details.requestHeaders?.find(
- (h) => h.name.toLowerCase() === name.toLowerCase()
- );
- return header?.value || "";
- };
-
- const auth = getHeaderValue("authorization");
- const cookie = getHeaderValue("cookie");
- const csrf = getHeaderValue("x-csrf-token");
-
- // // Validate required headers
- // if (!auth || !cookie || !csrf) {
- // console.warn("Missing required headers", {
- // hasAuth: !!auth,
- // hasCookie: !!cookie,
- // hasCsrf: !!csrf,
- // });
- // return;
- // }
-
- // Only update storage if values have changed
- if (
- result.cookie !== cookie ||
- result.csrf !== csrf ||
- result.auth !== auth
- ) {
- chrome.storage.local.set({ cookie, csrf, auth }, () => {
- if (chrome.runtime.lastError) {
- console.error(
- "Error updating credentials:",
- chrome.runtime.lastError
- );
- return;
- }
- console.log(
- "Updated cookie, csrf, and auth in local storage"
- );
- });
- }
- } catch (err) {
- console.error("Error processing request headers:", err);
- }
- }
- );
- } catch (err) {
- console.error("Top level error in onBeforeSendHeaders listener:", err);
- }
- },
- { urls: ["*://x.com/*", "*://twitter.com/*"] },
- ["requestHeaders", "extraHeaders"]
- );
-};
diff --git a/apps/extension/tailwind.config.cjs b/apps/extension/tailwind.config.cjs
deleted file mode 100644
index 466e9b93..00000000
--- a/apps/extension/tailwind.config.cjs
+++ /dev/null
@@ -1,50 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-export default {
- darkMode: ["class"],
- content: ["./src/**/*.{ts,tsx}", "./ui/**/*.{ts,tsx}"],
- theme: {
- extend: {
- colors: {
- border: "hsl(var(--border))",
- input: "hsl(var(--input))",
- ring: "hsl(var(--ring))",
- background: "hsl(var(--background))",
- foreground: "hsl(var(--foreground))",
- primary: {
- DEFAULT: "hsl(var(--primary))",
- foreground: "hsl(var(--primary-foreground))",
- },
- secondary: {
- DEFAULT: "hsl(var(--secondary))",
- foreground: "hsl(var(--secondary-foreground))",
- },
- destructive: {
- DEFAULT: "hsl(var(--destructive))",
- foreground: "hsl(var(--destructive-foreground))",
- },
- muted: {
- DEFAULT: "hsl(var(--muted))",
- foreground: "hsl(var(--muted-foreground))",
- },
- accent: {
- DEFAULT: "hsl(var(--accent))",
- foreground: "hsl(var(--accent-foreground))",
- },
- popover: {
- DEFAULT: "hsl(var(--popover))",
- foreground: "hsl(var(--popover-foreground))",
- },
- card: {
- DEFAULT: "hsl(var(--card))",
- foreground: "hsl(var(--card-foreground))",
- },
- },
- borderRadius: {
- lg: `var(--radius)`,
- md: `calc(var(--radius) - 2px)`,
- sm: "calc(var(--radius) - 4px)",
- },
- },
- },
- plugins: [import("tailwindcss-animate")],
-}
diff --git a/apps/extension/tsconfig.json b/apps/extension/tsconfig.json
deleted file mode 100644
index ade28a04..00000000
--- a/apps/extension/tsconfig.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2020",
- "module": "ES2020",
- "strict": true,
- "esModuleInterop": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
- "moduleResolution": "node",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "noEmit": true,
- "jsx": "preserve",
- "types": ["chrome"],
- "baseUrl": ".",
- "paths": {
- "@/*": ["./*"]
- }
- },
- "include": ["src/**/*.ts", "src/**/*.tsx", "ui/**/*.tsx", "src/content.tsx"],
- "exclude": ["node_modules"]
-}
diff --git a/apps/extension/ui/hooks/use-spaces.tsx b/apps/extension/ui/hooks/use-spaces.tsx
deleted file mode 100644
index a7b5f4df..00000000
--- a/apps/extension/ui/hooks/use-spaces.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import { Space } from "@supermemory/db/schema";
-import { useState, useEffect, useCallback } from "react";
-import { toast } from "sonner";
-import { getBaseURL } from "@/lib/utils";
-
-export type ExtraSpaceMetaData = {
- permissions: {
- canRead: boolean;
- canEdit: boolean;
- isOwner: boolean;
- };
- owner: {
- id: string;
- name: string;
- email: string;
- profileImage: string;
- } | null;
- favorited: boolean;
-};
-
-type SpaceResponse = {
- spaces: (Space & ExtraSpaceMetaData)[];
-};
-
-type CreateSpaceResponse = {
- message: string;
- space: {
- name: string;
- uuid: string;
- isPublic: boolean;
- };
-};
-
-async function fetchSpaces(): Promise<SpaceResponse> {
- return new Promise((resolve, reject) => {
- chrome.runtime.sendMessage(
- { type: "GET_SPACES", payload: undefined },
- (response) => {
- if (chrome.runtime.lastError) {
- reject(new Error(chrome.runtime.lastError.message));
- return;
- }
- resolve(response);
- }
- );
- });
-}
-
-async function createSpace(data: {
- spaceName: string;
- isPublic: boolean;
-}): Promise<CreateSpaceResponse> {
- const baseURL = await getBaseURL();
- const response = await fetch(`${baseURL}/backend/v1/space/create`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(data),
- });
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(
- (error as { error: string }).error || "Failed to create space"
- );
- }
-
- return response.json();
-}
-
-async function makeFavorite(spaceId: string) {
- const baseURL = await getBaseURL();
- const response = await fetch(
- `${baseURL}/backend/v1/space/favorite/${spaceId}`,
- {
- method: "POST",
- }
- );
-
- if (!response.ok) {
- throw new Error("Failed to make favorite");
- }
-
- return response.json();
-}
-
-export function useSpaces() {
- const [spaces, setSpaces] = useState<(Space & ExtraSpaceMetaData)[]>([]);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState<Error | null>(null);
- const [isCreating, setIsCreating] = useState(false);
- const [createError, setCreateError] = useState<Error | null>(null);
-
- const loadSpaces = useCallback(async () => {
- try {
- setIsLoading(true);
- const response = await fetchSpaces();
- setSpaces(response.spaces);
- setError(null);
- } catch (err) {
- console.error("Error fetching spaces:", err); // Add error logging
- setError(
- err instanceof Error ? err : new Error("Failed to fetch spaces")
- );
- } finally {
- setIsLoading(false);
- }
- }, []); // Empty dependency array since we don't use any external values
-
- useEffect(() => {
- loadSpaces().catch((err) => {
- console.error("Failed to load spaces:", err); // Add error handling
- });
- }, []); // Use empty dependency array instead of [loadSpaces]
-
- const handleCreateSpace = async (data: {
- spaceName: string;
- isPublic: boolean;
- }) => {
- try {
- setIsCreating(true);
- setCreateError(null);
- const response = await createSpace(data);
- toast.success(response.message);
- loadSpaces(); // Refresh spaces after creation
- } catch (err) {
- const error =
- err instanceof Error ? err : new Error("Failed to create space");
- console.error("Failed to create space", error);
- toast.error(error.message);
- setCreateError(error);
- } finally {
- setIsCreating(false);
- }
- };
-
- return {
- spaces,
- isLoading,
- error,
- refetch: loadSpaces,
- createSpace: handleCreateSpace,
- isCreating,
- createError,
- };
-}
diff --git a/apps/extension/ui/shadcn/badge.tsx b/apps/extension/ui/shadcn/badge.tsx
deleted file mode 100644
index e87d62bf..00000000
--- a/apps/extension/ui/shadcn/badge.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
-
-const badgeVariants = cva(
- "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
- {
- variants: {
- variant: {
- default:
- "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
- secondary:
- "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
- destructive:
- "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
- outline: "text-foreground",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-)
-
-export interface BadgeProps
- extends React.HTMLAttributes<HTMLDivElement>,
- VariantProps<typeof badgeVariants> {}
-
-function Badge({ className, variant, ...props }: BadgeProps) {
- return (
- <div className={cn(badgeVariants({ variant }), className)} {...props} />
- )
-}
-
-export { Badge, badgeVariants }
diff --git a/apps/extension/ui/shadcn/button.tsx b/apps/extension/ui/shadcn/button.tsx
deleted file mode 100644
index 65d4fcd9..00000000
--- a/apps/extension/ui/shadcn/button.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
-
-const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
- {
- variants: {
- variant: {
- default:
- "bg-primary text-primary-foreground shadow hover:bg-primary/90",
- destructive:
- "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
- outline:
- "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
- secondary:
- "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
- ghost: "hover:bg-accent hover:text-accent-foreground",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default: "h-9 px-4 py-2",
- sm: "h-8 rounded-md px-3 text-xs",
- lg: "h-10 rounded-md px-8",
- icon: "h-9 w-9",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
-)
-
-export interface ButtonProps
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
- VariantProps<typeof buttonVariants> {
- asChild?: boolean
-}
-
-const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : "button"
- return (
- <Comp
- className={cn(buttonVariants({ variant, size, className }))}
- ref={ref}
- {...props}
- />
- )
- }
-)
-Button.displayName = "Button"
-
-export { Button, buttonVariants }
diff --git a/apps/extension/ui/shadcn/command.tsx b/apps/extension/ui/shadcn/command.tsx
deleted file mode 100644
index 3bb14d39..00000000
--- a/apps/extension/ui/shadcn/command.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import * as React from "react"
-import { type DialogProps } from "@radix-ui/react-dialog"
-import { Command as CommandPrimitive } from "cmdk"
-import { Search } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-import { Dialog, DialogContent } from "@/ui/shadcn/dialog"
-
-const Command = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive>
->(({ className, ...props }, ref) => (
- <CommandPrimitive
- ref={ref}
- className={cn(
- "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
- className
- )}
- {...props}
- />
-))
-Command.displayName = CommandPrimitive.displayName
-
-const CommandDialog = ({ children, ...props }: DialogProps) => {
- return (
- <Dialog {...props}>
- <DialogContent className="overflow-hidden p-0">
- <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
- {children}
- </Command>
- </DialogContent>
- </Dialog>
- )
-}
-
-const CommandInput = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Input>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
->(({ className, ...props }, ref) => (
- <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
- <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
- <CommandPrimitive.Input
- ref={ref}
- className={cn(
- "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
- className
- )}
- {...props}
- />
- </div>
-))
-
-CommandInput.displayName = CommandPrimitive.Input.displayName
-
-const CommandList = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.List>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
->(({ className, ...props }, ref) => (
- <CommandPrimitive.List
- ref={ref}
- className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
- {...props}
- />
-))
-
-CommandList.displayName = CommandPrimitive.List.displayName
-
-const CommandEmpty = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Empty>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
->((props, ref) => (
- <CommandPrimitive.Empty
- ref={ref}
- className="py-6 text-center text-sm"
- {...props}
- />
-))
-
-CommandEmpty.displayName = CommandPrimitive.Empty.displayName
-
-const CommandGroup = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Group>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
->(({ className, ...props }, ref) => (
- <CommandPrimitive.Group
- ref={ref}
- className={cn(
- "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
- className
- )}
- {...props}
- />
-))
-
-CommandGroup.displayName = CommandPrimitive.Group.displayName
-
-const CommandSeparator = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Separator>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
->(({ className, ...props }, ref) => (
- <CommandPrimitive.Separator
- ref={ref}
- className={cn("-mx-1 h-px bg-border", className)}
- {...props}
- />
-))
-CommandSeparator.displayName = CommandPrimitive.Separator.displayName
-
-const CommandItem = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
->(({ className, ...props }, ref) => (
- <CommandPrimitive.Item
- ref={ref}
- className={cn(
- "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
- className
- )}
- {...props}
- />
-))
-
-CommandItem.displayName = CommandPrimitive.Item.displayName
-
-const CommandShortcut = ({
- className,
- ...props
-}: React.HTMLAttributes<HTMLSpanElement>) => {
- return (
- <span
- className={cn(
- "ml-auto text-xs tracking-widest text-muted-foreground",
- className
- )}
- {...props}
- />
- )
-}
-CommandShortcut.displayName = "CommandShortcut"
-
-export {
- Command,
- CommandDialog,
- CommandInput,
- CommandList,
- CommandEmpty,
- CommandGroup,
- CommandItem,
- CommandShortcut,
- CommandSeparator,
-}
diff --git a/apps/extension/ui/shadcn/dialog.tsx b/apps/extension/ui/shadcn/dialog.tsx
deleted file mode 100644
index 1647513e..00000000
--- a/apps/extension/ui/shadcn/dialog.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as DialogPrimitive from "@radix-ui/react-dialog"
-import { X } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-
-const Dialog = DialogPrimitive.Root
-
-const DialogTrigger = DialogPrimitive.Trigger
-
-const DialogPortal = DialogPrimitive.Portal
-
-const DialogClose = DialogPrimitive.Close
-
-const DialogOverlay = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Overlay>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
->(({ className, ...props }, ref) => (
- <DialogPrimitive.Overlay
- ref={ref}
- className={cn(
- "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
- className
- )}
- {...props}
- />
-))
-DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
-
-const DialogContent = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
->(({ className, children, ...props }, ref) => (
- <DialogPortal>
- <DialogOverlay />
- <DialogPrimitive.Content
- ref={ref}
- className={cn(
- "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
- className
- )}
- {...props}
- >
- {children}
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
- <X className="h-4 w-4" />
- <span className="sr-only">Close</span>
- </DialogPrimitive.Close>
- </DialogPrimitive.Content>
- </DialogPortal>
-))
-DialogContent.displayName = DialogPrimitive.Content.displayName
-
-const DialogHeader = ({
- className,
- ...props
-}: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn(
- "flex flex-col space-y-1.5 text-center sm:text-left",
- className
- )}
- {...props}
- />
-)
-DialogHeader.displayName = "DialogHeader"
-
-const DialogFooter = ({
- className,
- ...props
-}: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn(
- "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
- className
- )}
- {...props}
- />
-)
-DialogFooter.displayName = "DialogFooter"
-
-const DialogTitle = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Title>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
->(({ className, ...props }, ref) => (
- <DialogPrimitive.Title
- ref={ref}
- className={cn(
- "text-lg font-semibold leading-none tracking-tight",
- className
- )}
- {...props}
- />
-))
-DialogTitle.displayName = DialogPrimitive.Title.displayName
-
-const DialogDescription = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Description>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
->(({ className, ...props }, ref) => (
- <DialogPrimitive.Description
- ref={ref}
- className={cn("text-sm text-muted-foreground", className)}
- {...props}
- />
-))
-DialogDescription.displayName = DialogPrimitive.Description.displayName
-
-export {
- Dialog,
- DialogPortal,
- DialogOverlay,
- DialogTrigger,
- DialogClose,
- DialogContent,
- DialogHeader,
- DialogFooter,
- DialogTitle,
- DialogDescription,
-}
diff --git a/apps/extension/ui/shadcn/popover.tsx b/apps/extension/ui/shadcn/popover.tsx
deleted file mode 100644
index eafc78a4..00000000
--- a/apps/extension/ui/shadcn/popover.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import * as React from "react"
-import * as PopoverPrimitive from "@radix-ui/react-popover"
-
-import { cn } from "@/lib/utils"
-
-const Popover = PopoverPrimitive.Root
-
-const PopoverTrigger = PopoverPrimitive.Trigger
-
-const PopoverAnchor = PopoverPrimitive.Anchor
-
-const PopoverContent = React.forwardRef<
- React.ElementRef<typeof PopoverPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
->(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
- <PopoverPrimitive.Portal container={document.querySelector("#supermemory-root")}>
- <PopoverPrimitive.Content
- ref={ref}
- align={align}
- sideOffset={sideOffset}
- className={cn(
- "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- className
- )}
- {...props}
- />
- </PopoverPrimitive.Portal>
-))
-PopoverContent.displayName = PopoverPrimitive.Content.displayName
-
-export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
diff --git a/apps/extension/ui/spaces-selector.tsx b/apps/extension/ui/spaces-selector.tsx
deleted file mode 100644
index be5e0eac..00000000
--- a/apps/extension/ui/spaces-selector.tsx
+++ /dev/null
@@ -1,217 +0,0 @@
-import { useSpaces } from "./hooks/use-spaces";
-import { useState, useEffect, useRef } from "react";
-import { cn } from "@/lib/utils";
-
-interface SpacesSelectorProps {
- onSelecting?: (isSelecting: boolean) => void;
- onSpacesChange?: (spaces: any[]) => void;
- passRef?: React.RefObject<HTMLDivElement>;
-}
-
-export default function SpacesSelector({
- onSelecting,
- onSpacesChange,
- passRef,
-}: SpacesSelectorProps) {
- const { spaces } = useSpaces();
- const [isOpen, setIsOpen] = useState(false);
- const [selectedSpaces, setSelectedSpaces] = useState<any[]>([]);
- const [searchQuery, setSearchQuery] = useState("");
- const dropdownRef = useRef<HTMLDivElement>(null);
- const dropdownContentRef = useRef<HTMLDivElement>(null);
-
- const toggleSpace = (space: any) => {
- const isSelected = selectedSpaces.some((s) => s.id === space.id);
- const newSelectedSpaces = isSelected
- ? selectedSpaces.filter((s) => s.id !== space.id)
- : [...selectedSpaces, space];
-
- setSelectedSpaces(newSelectedSpaces);
- onSpacesChange?.(newSelectedSpaces);
- };
-
- const removeSpace = (spaceId: string) => {
- const newSelectedSpaces = selectedSpaces.filter((s) => s.id !== spaceId);
- setSelectedSpaces(newSelectedSpaces);
- onSpacesChange?.(newSelectedSpaces);
- };
-
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- // Get the shadow root
- const shadowRoot = dropdownRef.current?.getRootNode() as ShadowRoot;
- const target = event.composedPath()[0] as Node;
-
- // Check if click is inside either the trigger or dropdown content
- const isInsideTrigger = dropdownRef.current?.contains(target);
- const isInsideDropdown = dropdownContentRef.current?.contains(target);
-
- if (!isInsideTrigger && !isInsideDropdown) {
- setIsOpen(false);
- onSelecting?.(false);
- }
- };
-
- // Listen on the window to catch all clicks, including those in shadow DOM
- window.addEventListener("click", handleClickOutside, true);
- return () => {
- window.removeEventListener("click", handleClickOutside, true);
- // cleanup all states
- setIsOpen(false);
- setSelectedSpaces([]);
- setSearchQuery("");
- };
- }, [onSelecting]);
-
- const filteredSpaces = spaces?.filter((space) =>
- space.name.toLowerCase().includes(searchQuery.toLowerCase())
- );
-
- const toggleDropdown = () => {
- setIsOpen(!isOpen);
- onSelecting?.(!isOpen);
- };
-
- return (
- <div ref={passRef} className="flex-1 space-y-2">
- <div className="relative" ref={dropdownRef}>
- <button
- onClick={toggleDropdown}
- className={cn(
- "w-full flex items-center gap-2 px-3 py-2 text-sm rounded-md transition-all duration-150",
- "bg-white/10 hover:bg-white/20 text-white",
- isOpen && "bg-white/20",
- "justify-between"
- )}
- >
- <div className="flex items-center gap-2">
- <span className="w-2.5 h-2.5 rounded-sm bg-blue-500" />
- <span className="max-w-[200px] truncate">
- {selectedSpaces.length === 0 && "Select spaces to save to..."}
- {selectedSpaces.length === 1 && selectedSpaces[0].name}
- {selectedSpaces.length > 1 &&
- `${selectedSpaces.length} spaces selected`}
- </span>
- </div>
- <svg
- className={cn(
- "w-4 h-4 opacity-60 transition-transform duration-200",
- isOpen && "transform rotate-180"
- )}
- fill="none"
- viewBox="0 0 24 24"
- stroke="currentColor"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M19 9l-7 7-7-7"
- />
- </svg>
- </button>
-
- {isOpen && (
- <div
- ref={dropdownContentRef}
- className="absolute left-0 right-0 top-full mt-1 bg-gray-900 rounded-md shadow-lg border border-white/20 py-1 z-50"
- >
- <div className="px-2 py-1.5">
- <input
- type="text"
- placeholder="Search spaces..."
- value={searchQuery}
- onChange={(e) => setSearchQuery(e.target.value)}
- className="w-full px-2 py-1.5 text-sm bg-gray-800 rounded border border-white/20 text-white placeholder-white/50 focus:outline-none focus:border-blue-500"
- autoFocus
- />
- </div>
-
- <div className="max-h-[240px] overflow-y-auto">
- {filteredSpaces?.length === 0 && (
- <div className="px-3 py-2 text-sm text-white/70">
- No spaces found
- </div>
- )}
-
- {filteredSpaces?.map((space) => {
- const isSelected = selectedSpaces.some(
- (s) => s.id === space.id
- );
- return (
- <button
- key={space.id}
- onClick={() => toggleSpace(space)}
- className={cn(
- "w-full px-3 py-2 text-left text-sm flex items-center gap-2",
- "text-white hover:bg-gray-800",
- isSelected && "bg-gray-800"
- )}
- >
- <div
- className={cn(
- "w-4 h-4 rounded border flex items-center justify-center flex-shrink-0",
- isSelected
- ? "border-blue-500 bg-blue-500"
- : "border-white/50"
- )}
- >
- {isSelected && (
- <svg
- className="w-3 h-3 text-white"
- fill="none"
- viewBox="0 0 24 24"
- stroke="currentColor"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M5 13l4 4L19 7"
- />
- </svg>
- )}
- </div>
- <span className="truncate">{space.name}</span>
- </button>
- );
- })}
- </div>
- </div>
- )}
- </div>
-
- {selectedSpaces.length > 0 && (
- <div className="flex flex-wrap gap-1.5 items-center">
- {selectedSpaces.map((space) => (
- <div
- key={space.id}
- className="group flex items-center gap-1 bg-gray-800 rounded-md px-2 py-1 text-xs"
- >
- <span className="w-1.5 h-1.5 rounded-sm bg-blue-400" />
- <span className="text-blue-100">{space.name}</span>
- <button
- onClick={() => removeSpace(space.id)}
- className="opacity-60 hover:opacity-100 transition-opacity"
- >
- <svg
- className="w-3 h-3"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M6 18L18 6M6 6l12 12"
- />
- </svg>
- </button>
- </div>
- ))}
- </div>
- )}
- </div>
- );
-}
diff --git a/apps/web/.env.example b/apps/web/.env.example
new file mode 100644
index 00000000..969d7377
--- /dev/null
+++ b/apps/web/.env.example
@@ -0,0 +1,2 @@
+NEXT_PUBLIC_BACKEND_URL=https://api.supermemory.ai
+NEXT_PUBLIC_POSTHOG_KEY= \ No newline at end of file
diff --git a/apps/web/.gitignore b/apps/web/.gitignore
index e42a8020..4f328319 100644
--- a/apps/web/.gitignore
+++ b/apps/web/.gitignore
@@ -1,18 +1,37 @@
-node_modules
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-/.cache
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
/build
-.env
-.dev.vars
-.wrangler
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# env files (can opt-in for commiting if needed)
+.env*
+!.env.example
-.env.*
-.env
-.dev.vars
-*.vars
-bun.lockb
+# vercel
+.vercel
-# Million Lint
-.million
-database.sql
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/apps/web/.prettierrc b/apps/web/.prettierrc
deleted file mode 100644
index be27a4b9..00000000
--- a/apps/web/.prettierrc
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "printWidth": 100,
- "singleQuote": false,
- "semi": true,
- "useTabs": true,
- "plugins": ["prettier-plugin-tailwindcss", "@trivago/prettier-plugin-sort-imports"],
- "importOrder": [
- "^react",
- "^@remix-run/(.*)$",
- "^@ui/(.*)$",
- "^components/(.*)$",
- "^lib/(.*)$",
- "^[./]",
- "<THIRD_PARTY_MODULES>"
- ],
- "importOrderSeparation": true,
- "importOrderSortSpecifiers": true,
- "importOrderGroupNamespaceSpecifiers": true
-}
diff --git a/apps/web/README.md b/apps/web/README.md
index dec7f30b..9cd849fb 100644
--- a/apps/web/README.md
+++ b/apps/web/README.md
@@ -1,47 +1 @@
-# Welcome to Remix + Cloudflare!
-
-- 📖 [Remix docs](https://remix.run/docs)
-- 📖 [Remix Cloudflare docs](https://remix.run/guides/vite#cloudflare)
-
-## Development
-
-Run the dev server:
-
-```sh
-npm run dev
-```
-
-To run Wrangler:
-
-```sh
-npm run build
-npm run start
-```
-
-## Typegen
-
-Generate types for your Cloudflare bindings in `wrangler.toml`:
-
-```sh
-npm run typegen
-```
-
-You will need to rerun typegen whenever you make changes to `wrangler.toml`.
-
-## Deployment
-
-First, build your app for production:
-
-```sh
-npm run build
-```
-
-Then, deploy your app to Cloudflare Pages:
-
-```sh
-npm run deploy
-```
-
-## Styling
-
-This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever css framework you prefer. See the [Vite docs on css](https://vitejs.dev/guide/features.html#css) for more information.
+# supermemory Consumer \ No newline at end of file
diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx
new file mode 100644
index 00000000..e2f72129
--- /dev/null
+++ b/apps/web/app/(auth)/login/page.tsx
@@ -0,0 +1,12 @@
+import { LoginPage } from "@repo/ui/pages/login"
+
+export default function Page() {
+ return (
+ <LoginPage
+ texts={[
+ "Your memory, everywhere, with you.",
+ "Private, secure, and reliable.",
+ ]}
+ />
+ )
+}
diff --git a/apps/web/app/api/emails/welcome/route.tsx b/apps/web/app/api/emails/welcome/route.tsx
new file mode 100644
index 00000000..48883d6b
--- /dev/null
+++ b/apps/web/app/api/emails/welcome/route.tsx
@@ -0,0 +1,19 @@
+/** biome-ignore-all lint/performance/noImgElement: Not Next.js environment */
+import { ImageResponse } from "next/og";
+
+export async function GET() {
+ return new ImageResponse(
+ <div tw="w-full h-full flex flex-col justify-center items-center">
+ <img
+ src="https://pub-1be2b1df2c7e456f8e21149e972f4caf.r2.dev/bust.png"
+ alt="Google Logo"
+ height={367}
+ width={369}
+ />
+ </div>,
+ {
+ width: 1200,
+ height: 630,
+ },
+ );
+}
diff --git a/apps/web/app/components/Chat.tsx b/apps/web/app/components/Chat.tsx
deleted file mode 100644
index 214d41ea..00000000
--- a/apps/web/app/components/Chat.tsx
+++ /dev/null
@@ -1,329 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-import Markdown from "react-markdown";
-
-import ChatInputForm from "./ChatInputForm";
-import Navbar from "./Navbar";
-import SharedCard from "./memories/SharedCard";
-
-import { User } from "@supermemory/shared/types";
-import { CoreMessage } from "ai";
-import { AnimatePresence, motion } from "framer-motion";
-import { ChevronDown, ChevronUp } from "lucide-react";
-import { useChatStream } from "~/lib/hooks/use-chat-stream";
-import { Memory } from "~/lib/types/memory";
-
-interface ChatProps {
- user: User;
- chatMessages: CoreMessage[];
- initialThreadUuid?: string;
-}
-
-function Chat({ user, chatMessages, initialThreadUuid }: ChatProps) {
- const {
- threadUuid,
- chatMessages: chatMessagesStreamed,
- input,
- setInput,
- sendMessage,
- isLoading,
- } = useChatStream(chatMessages, initialThreadUuid);
-
- const [expandedMessageIndexes, setExpandedMessageIndexes] = useState<number[]>([]);
- const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
- const messagesEndRef = useRef<HTMLDivElement>(null);
- const scrollContainerRef = useRef<HTMLDivElement>(null);
- const [streamingText, setStreamingText] = useState("");
-
- const hasAnnotations = chatMessagesStreamed.some(
- (message) => message.role === "assistant" && message.annotations?.length,
- );
-
- const toggleExpand = (index: number) => {
- setExpandedMessageIndexes((prev) =>
- prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index],
- );
- };
-
- const scrollToBottom = () => {
- if (messagesEndRef.current && shouldAutoScroll) {
- messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
- }
- };
-
- useEffect(() => {
- scrollToBottom();
- }, [chatMessagesStreamed]);
-
- useEffect(() => {
- const lastMessage = chatMessagesStreamed[chatMessagesStreamed.length - 1];
- if (lastMessage?.role === "assistant" && isLoading) {
- setStreamingText(lastMessage.content as string);
- } else {
- setStreamingText("");
- }
- }, [chatMessagesStreamed, isLoading]);
-
- const handleScroll = () => {
- if (!scrollContainerRef.current) return;
-
- const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
- const isAtBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 10;
- setShouldAutoScroll(isAtBottom);
- };
-
- const renderAttachments = (attachments: any[]) => (
- <div className="flex flex-wrap gap-4 mb-4">
- {attachments.map((attachment) => (
- <motion.div
- key={attachment.name}
- className="relative"
- initial={{ scale: 0.9, opacity: 0 }}
- animate={{ scale: 1, opacity: 1 }}
- transition={{ duration: 0.2 }}
- >
- {attachment.url.endsWith(".png") ||
- attachment.url.endsWith(".jpg") ||
- attachment.url.endsWith(".jpeg") ? (
- <img
- src={attachment.url}
- alt={attachment.name}
- className="max-w-[300px] rounded-lg shadow-sm"
- />
- ) : (
- <div className="p-4 border rounded-lg">
- <a
- href={attachment.url}
- className="text-blue-500 hover:underline"
- target="_blank"
- rel="noopener noreferrer"
- >
- {attachment.name}
- </a>
- </div>
- )}
- </motion.div>
- ))}
- </div>
- );
-
- const renderMessageContent = (content: string | any, isLatestAndLoading: boolean) => (
- <motion.div
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{ duration: 0.5 }}
- >
- <Markdown className="prose dark:prose-invert prose-lg w-full">
- {isLatestAndLoading ? streamingText : typeof content === "string" ? content.replace(/<context>[\s\S]*?<\/context>/g, "") : content}
- </Markdown>
- </motion.div>
- );
-
- const groupAnnotationsByHost = (annotations: Memory[]) => {
- return annotations.reduce(
- (acc, curr) => {
- let host = "";
- try {
- const url = new URL(curr.url || "");
- host = url.host;
- } catch {
- host = "unknown";
- }
- if (!acc[host]) acc[host] = [];
- acc[host].push(curr);
- return acc;
- },
- {} as Record<string, Memory[]>,
- );
- };
-
- const renderAnnotations = (messageAnnotations: Memory[], index: number, isMobile = false) => {
- const isExpanded = expandedMessageIndexes.includes(index);
- const groupedAnnotations = groupAnnotationsByHost(messageAnnotations);
-
- return (
- <div
- className={
- isMobile ? "lg:hidden mb-4" : "hidden lg:block w-[320px] flex-shrink-0 -translate-y-12"
- }
- >
- <AnimatePresence>
- {!isExpanded ? (
- <motion.button
- onClick={() => toggleExpand(index)}
- className={`text-${isMobile ? "xs" : "sm"} text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1`}
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
- >
- <ChevronDown className={`w-${isMobile ? "3" : "4"} h-${isMobile ? "3" : "4"}`} />
- {messageAnnotations.length} relevant{" "}
- {messageAnnotations.length === 1 ? "item" : "items"}
- </motion.button>
- ) : (
- <motion.div
- className="space-y-4"
- initial={{ opacity: 0, [isMobile ? "y" : "x"]: isMobile ? 10 : 20 }}
- animate={{ opacity: 1, [isMobile ? "y" : "x"]: 0 }}
- exit={{ opacity: 0, [isMobile ? "y" : "x"]: isMobile ? -10 : 20 }}
- >
- <div className={`text-${isMobile ? "xs" : "sm"} font-medium text-muted-foreground`}>
- Related Context
- </div>
- <div className={isMobile ? "flex gap-2 overflow-x-auto pb-2" : "space-y-6"}>
- {isMobile
- ? messageAnnotations.map((annotation, i) => (
- <motion.div
- key={i}
- className="flex-shrink-0 w-[200px] scale-90"
- initial={{ opacity: 0, y: 10 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ duration: 0.2, delay: i * 0.1 }}
- >
- <SharedCard data={annotation} />
- </motion.div>
- ))
- : Object.entries(groupedAnnotations).map(([host, items], i) => (
- <div key={host} className="space-y-2">
- {items.length > 1 && (
- <div className="text-xs text-muted-foreground">
- {host} ({items.length} items)
- </div>
- )}
- <div className="space-y-2">
- {items.map((annotation, j) => (
- <motion.div
- key={j}
- initial={{ opacity: 0, y: 10 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ duration: 0.2, delay: j * 0.1 }}
- >
- <SharedCard data={annotation} />
- </motion.div>
- ))}
- </div>
- </div>
- ))}
- </div>
- <motion.button
- onClick={() => toggleExpand(index)}
- className={`flex items-center gap-${isMobile ? "1" : "2"} text-${isMobile ? "xs" : "sm"} text-muted-foreground hover:text-foreground transition-colors`}
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
- >
- <ChevronUp className={`w-${isMobile ? "3" : "4"} h-${isMobile ? "3" : "4"}`} />
- Show less
- </motion.button>
- </motion.div>
- )}
- </AnimatePresence>
- </div>
- );
- };
-
- return (
- <div>
- <Navbar user={user} />
- <div
- className="p-4 font-geist md:p-24 md:pt-16 h-[calc(100vh-64px)] overflow-y-auto"
- ref={scrollContainerRef}
- onScroll={handleScroll}
- >
- <div className="grid grid-cols-12 gap-8">
- <div
- className={`col-span-12 ${hasAnnotations ? "lg:col-start-2" : "lg:col-start-3"} relative`}
- >
- <div className="space-y-8 pb-24">
- {chatMessagesStreamed.map((message, index) => {
- const isLatestAndLoading = index === chatMessagesStreamed.length - 1 && isLoading && message.role === "assistant";
-
- if (message.role === "user") {
- return (
- <motion.div
- key={index}
- initial={{ opacity: 0, y: 20 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ duration: 0.3 }}
- >
- {message.experimental_attachments &&
- renderAttachments(message.experimental_attachments)}
- <Markdown className={"text-xl"}>{message.content}</Markdown>
- </motion.div>
- );
- }
-
- if (message.role === "assistant") {
- const messageAnnotations = message.annotations?.[0]
- ? (message.annotations[0] as unknown as Memory[])
- : undefined;
-
- return (
- <motion.div
- key={index}
- className="relative w-full"
- initial={{ opacity: 0, y: 20 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ duration: 0.3 }}
- >
- <>
- {messageAnnotations &&
- messageAnnotations.length > 0 &&
- renderAnnotations(messageAnnotations, index, true)}
- <div className="flex gap-8">
- <div className="flex-1">{renderMessageContent(message.content, isLatestAndLoading)}</div>
- {messageAnnotations &&
- messageAnnotations.length > 0 &&
- renderAnnotations(messageAnnotations, index)}
- </div>
- </>
- </motion.div>
- );
- }
- return null;
- })}
-
- {isLoading && !chatMessagesStreamed[chatMessagesStreamed.length - 1]?.content && (
- <motion.div
- initial={{ opacity: 0, y: 20 }}
- animate={{ opacity: 1, y: 0 }}
- className="flex gap-8"
- >
- <div className="flex-1">
- <div className="animate-pulse space-y-4">
- {[3 / 4, 1 / 2, 2 / 3, 1 / 3].map((width, i) => (
- <div key={i} className={`h-6 bg-muted rounded w-${width}`} />
- ))}
- </div>
- </div>
- {hasAnnotations && (
- <div className="hidden lg:block w-[320px] flex-shrink-0 -translate-y-12">
- <div className="animate-pulse space-y-4">
- {[...Array(2)].map((_, i) => (
- <div key={i} className="h-[160px] bg-muted rounded" />
- ))}
- </div>
- </div>
- )}
- </motion.div>
- )}
- <div ref={messagesEndRef} />
- </div>
-
- <div
- className={`fixed bottom-8 ${hasAnnotations ? "lg:w-[calc(66.666667%-2rem)]" : "lg:w-[calc(66.666667%)]"} w-[calc(100%-2rem)] left-1/2 -translate-x-1/2`}
- >
- <ChatInputForm
- submit={sendMessage}
- user={user}
- input={input}
- setInput={setInput}
- isLoading={isLoading}
- mini
- />
- </div>
- </div>
- </div>
- </div>
- </div>
- );
-}
-
-export default Chat;
diff --git a/apps/web/app/components/ChatInputForm.tsx b/apps/web/app/components/ChatInputForm.tsx
deleted file mode 100644
index 80b077a9..00000000
--- a/apps/web/app/components/ChatInputForm.tsx
+++ /dev/null
@@ -1,502 +0,0 @@
-import { KeyboardEvent, useCallback, useRef, useState } from "react";
-
-import { useFetcher, useLoaderData } from "@remix-run/react";
-
-import { useUploadFile } from "../lib/hooks/use-upload-file";
-import SpacesSelector from "./memories/SpacesSelector";
-import { Button } from "./ui/button";
-import { Textarea } from "./ui/textarea";
-
-import { SpaceIcon } from "@supermemory/shared/icons";
-import { cn } from "~/lib/utils";
-import { loader } from "~/routes/_index";
-
-function MemoryInputForm({
- user,
- input,
- setInput,
- submit: externalSubmit,
- mini = false,
- fileURLs = [],
- setFileURLs,
- isLoading = false,
-}: {
- user: ReturnType<typeof useLoaderData<typeof loader>>["user"];
- input: string;
- setInput: React.Dispatch<React.SetStateAction<string>>;
- submit: () => void;
- mini?: boolean;
- fileURLs?: string[];
- setFileURLs?: React.Dispatch<React.SetStateAction<string[]>>;
- isLoading?: boolean;
-}) {
- const [previews, setPreviews] = useState<string[]>([]);
- const { uploadFile, isUploading } = useUploadFile();
- const fetcher = useFetcher();
- const fileInputRef = useRef<HTMLInputElement>(null);
- const [isDragActive, setIsDragActive] = useState(false);
- const [selectedSpaces, setSelectedSpaces] = useState<string[]>([]);
-
- const submit = useCallback(() => {
- if (input.trim() || fileURLs.length > 0) {
- if (!isLoading) {
- externalSubmit();
- setInput("");
- setFileURLs?.([]);
- setPreviews([]);
- }
- }
- }, [externalSubmit, input, fileURLs.length, setInput, isLoading]);
-
- const handlePaste = useCallback(
- (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
- const items = e.clipboardData.items;
- for (const item of items) {
- if (item.type.startsWith("image/") || item.type === "application/pdf") {
- const file = item.getAsFile();
- if (file) {
- if (isUploading || fileURLs.length !== previews.length) {
- console.log(
- "Cannot upload file: Upload in progress or previous upload not completed",
- );
- return;
- }
- if (fileURLs.length >= 5) {
- console.log("Maximum file limit reached");
- return;
- }
- handleFileUpload(file);
- break; // Only handle one file per paste
- }
- }
- }
- },
- [isUploading, fileURLs.length, previews.length],
- );
-
- const handleKeyDown = useCallback(
- (e: KeyboardEvent<HTMLTextAreaElement>) => {
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- submit();
- }
- },
- [submit],
- );
-
- const handleAttachClick = useCallback(() => {
- if (isUploading || fileURLs.length !== previews.length) {
- console.log("Cannot attach file: Upload in progress or previous upload not completed");
- return;
- }
- if (fileInputRef.current) {
- fileInputRef.current.click();
- // For mobile Safari, we need to focus and blur to ensure the file picker opens
- fileInputRef.current.focus();
- fileInputRef.current.blur();
- }
- }, [isUploading, fileURLs.length, previews.length]);
-
- const handleFileChange = useCallback(
- (e: React.ChangeEvent<HTMLInputElement>) => {
- const files = Array.from(e.target.files || []);
- const remainingSlots = 5 - fileURLs.length;
- files.slice(0, remainingSlots).forEach(handleFileUpload);
- e.target.value = "";
- },
- [fileURLs.length],
- );
-
- const handleFileUpload = useCallback(
- async (file: File) => {
- if (fileURLs.length >= 5) {
- console.log("Maximum file limit reached");
- return;
- }
-
- if (
- file.type !== "image/jpeg" &&
- file.type !== "image/png" &&
- file.type !== "application/pdf"
- ) {
- console.error("Unsupported file type:", file.type);
- return;
- }
-
- const reader = new FileReader();
- reader.onload = (ev) => {
- const previewURL = ev.target?.result as string;
- if (previews.includes(previewURL)) {
- console.log("Duplicate file detected. Skipping upload.");
- return;
- }
- setPreviews((prev) => [...prev, previewURL]);
- };
- reader.readAsDataURL(file);
-
- try {
- const { url: fileURL, error } = await uploadFile(file);
- if (error) {
- console.error("File upload failed:", error);
- setPreviews((prev) => prev.filter((_, i) => i !== fileURLs.length));
- return;
- }
- if (fileURL) {
- const encodedURL = encodeURIComponent(fileURL);
- if (fileURLs.includes(encodedURL)) {
- console.log("Duplicate file URL detected. Skipping.");
- return;
- }
- setFileURLs?.((prev) => [...prev, encodedURL]);
- } else {
- console.error("File upload failed:", fileURL);
- }
- } catch (error) {
- console.error("File upload failed:", error);
- }
- },
- [fileURLs, previews, uploadFile],
- );
-
- const removeFile = useCallback((index: number) => {
- setFileURLs?.((prev) => prev.filter((_, i) => i !== index));
- setPreviews((prev) => prev.filter((_, i) => i !== index));
- }, []);
-
- const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
- e.preventDefault();
- e.stopPropagation();
- setIsDragActive(true);
- }, []);
-
- const handleDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
- e.preventDefault();
- e.stopPropagation();
- const relatedTarget = e.relatedTarget as Node | null;
- if (!relatedTarget || !e.currentTarget.contains(relatedTarget)) {
- setIsDragActive(false);
- }
- }, []);
-
- const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
- e.preventDefault();
- e.stopPropagation();
- }, []);
-
- const handleDrop = useCallback(
- (e: React.DragEvent<HTMLDivElement>) => {
- e.preventDefault();
- e.stopPropagation();
- setIsDragActive(false);
-
- const files = Array.from(e.dataTransfer.files);
- const remainingSlots = 5 - fileURLs.length;
- files.slice(0, remainingSlots).forEach(handleFileUpload);
- },
- [fileURLs.length, handleFileUpload],
- );
-
- return (
- <div
- className={cn(
- "rounded-2xl border border-gray-300 dark:border-neutral-700 bg-background shadow-lg focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-500 dark:focus-within:ring-blue-700",
- mini ? "fixed bottom-0 left-0 right-0 m-2 z-50" : "relative",
- )}
- onDragEnter={handleDragEnter}
- onDragLeave={handleDragLeave}
- onDragOver={handleDragOver}
- onDrop={handleDrop}
- >
- <div
- className={cn(
- "transition-colors duration-200 ease-in-out rounded-t-2xl relative",
- isDragActive ? "bg-blue-50" : "bg-white dark:bg-neutral-700",
- mini && "flex flex-col md:flex-row items-center rounded-2xl",
- )}
- >
- <input
- type="file"
- accept="image/jpeg,image/png,application/pdf"
- ref={fileInputRef}
- onChange={handleFileChange}
- className="hidden"
- capture="environment"
- multiple
- />
- <Textarea
- rows={1}
- placeholder="Ask your supermemory..."
- className={cn(
- "text-lg w-full rounded-t-2xl border-none px-4 md:px-8 py-4 md:py-6 shadow-none outline-none placeholder:text-neutral-500 dark:placeholder:text-neutral-400 focus-within:outline-none min-h-[60px] resize-none",
- mini && "rounded-2xl",
- )}
- value={input}
- onChange={(e) => setInput(e.target.value)}
- onKeyDown={handleKeyDown}
- onPaste={handlePaste}
- name="input"
- />
- {mini && (
- <div className="flex items-center gap-2 p-2 md:px-4 w-full md:w-auto justify-end border-t md:border-t-0 border-gray-200 dark:border-neutral-600">
- <Button
- variant="outline"
- className="flex items-center gap-2 text-secondary-foreground hover:bg-gray-100 dark:hover:bg-neutral-600"
- onClick={handleAttachClick}
- disabled={fileURLs.length >= 5 || isUploading || fileURLs.length !== previews.length}
- aria-label="Attach file"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 20 20"
- fill="currentColor"
- className="w-5 h-5"
- >
- <path
- fillRule="evenodd"
- d="M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z"
- clipRule="evenodd"
- />
- </svg>
- </Button>
-
- <Button
- onClick={submit}
- type="button"
- aria-label="Send message"
- className="bg-blue-500 text-white hover:bg-blue-600 dark:hover:bg-blue-700"
- disabled={
- isUploading ||
- fetcher.state !== "idle" ||
- (input.trim() === "" && fileURLs.length === 0) ||
- fileURLs.length !== previews.length ||
- isLoading
- }
- >
- {isLoading ? (
- <svg
- className="animate-spin h-5 w-5"
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- >
- <circle
- className="opacity-25"
- cx="12"
- cy="12"
- r="10"
- stroke="currentColor"
- strokeWidth="4"
- ></circle>
- <path
- className="opacity-75"
- fill="currentColor"
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
- ></path>
- </svg>
- ) : (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 20 20"
- fill="currentColor"
- className="w-5 h-5"
- >
- <path d="M3.105 2.289a.75.75 0 00-.826.95l1.414 4.925A1.5 1.5 0 005.135 9.25h6.115a.75.75 0 010 1.5H5.135a1.5 1.5 0 00-1.442 1.086l-1.414 4.926a.75.75 0 00.826.95 28.896 28.896 0 0015.293-7.154.75.75 0 000-1.115A28.897 28.897 0 003.105 2.289z" />
- </svg>
- )}
- </Button>
- </div>
- )}
- {isDragActive && (
- <div className="absolute inset-0 flex items-center justify-center bg-blue-100 bg-opacity-50 rounded-2xl pointer-events-none">
- <p className="font-semibold text-blue-600">Drop files here...</p>
- </div>
- )}
- </div>
-
- {previews.length > 0 && (
- <div className="flex flex-wrap gap-2 p-4 border-t border-gray-200">
- {fileURLs.map((fileURL, index) => {
- const isPDF = previews[index].startsWith("data:application/pdf");
- return (
- <div
- key={index}
- className={cn(
- "relative group",
- fileURLs.length !== previews.length && "animate-pulse",
- )}
- >
- {isPDF ? (
- <a
- href={decodeURIComponent(fileURL)}
- target="_blank"
- rel="noopener noreferrer"
- className="flex items-center justify-center gap-2 border border-border rounded-md p-2"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="size-6"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
- />
- </svg>
- <span className="sr-only">Open PDF</span>
-
- {decodeURIComponent(fileURL).split("/").pop()}
- </a>
- ) : (
- <img
- src={decodeURIComponent(fileURL)}
- alt={`Preview ${index + 1}`}
- className="h-16 w-16 md:h-24 md:w-24 object-cover rounded-lg"
- />
- )}
- <button
- onClick={() => removeFile(index)}
- className="absolute top-1 right-1 bg-black/50 text-white rounded-full p-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- className="h-4 w-4"
- viewBox="0 0 20 20"
- fill="currentColor"
- >
- <path
- fillRule="evenodd"
- d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
- clipRule="evenodd"
- />
- </svg>
- </button>
- <input
- name={`uploadedFile-${index}`}
- type="file"
- className="hidden"
- src={decodeURIComponent(fileURL)}
- alt={`Preview ${index + 1}`}
- />
- </div>
- );
- })}
- {fileURLs.length !== previews.length && previews[fileURLs.length] && (
- <>
- {previews[fileURLs.length].startsWith("data:application/pdf") ? (
- <div className="flex items-center justify-center gap-2 border border-border rounded-md p-2 bg-gray-200 opacity-50">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="size-6 text-gray-400"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
- />
- </svg>
- <span className="sr-only">Open PDF</span>
- <div>Uploading PDF...</div>
- </div>
- ) : (
- <img
- src={previews[fileURLs.length]}
- alt={`Preview ${fileURLs.length + 1}`}
- className="h-16 w-16 md:h-24 md:w-24 object-cover rounded-lg animate-pulse opacity-50"
- />
- )}
- </>
- )}
- {fileURLs.length !== previews.length && !previews[fileURLs.length] && (
- <div className="h-16 w-16 md:h-24 md:w-24 rounded-lg animate-pulse bg-gray-200" />
- )}
- </div>
- )}
-
- {!mini && (
- <div className="flex flex-row justify-between items-center px-4 md:px-6 py-4 bg-gray-50 dark:bg-neutral-700 rounded-b-2xl gap-2 md:gap-0">
- <Button
- variant="outline"
- className="flex items-center gap-2 text-secondary-foreground hover:bg-gray-100 dark:hover:bg-neutral-600"
- onClick={handleAttachClick}
- disabled={fileURLs.length >= 5 || isUploading || fileURLs.length !== previews.length}
- aria-label="Attach file"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 20 20"
- fill="currentColor"
- className="w-5 h-5"
- >
- <path
- fillRule="evenodd"
- d="M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z"
- clipRule="evenodd"
- />
- </svg>
- <span className="hidden md:block">Attach ({fileURLs.length}/5)</span>
- </Button>
-
- <div className="flex items-center gap-4 w-full md:w-auto">
- <SpacesSelector selectedSpaces={selectedSpaces} onChange={setSelectedSpaces} />
-
- <Button
- onClick={submit}
- type="button"
- aria-label="Send message"
- className="flex-1 md:flex-none bg-blue-500 text-white hover:bg-blue-600 dark:hover:bg-blue-700"
- disabled={
- isUploading ||
- fetcher.state !== "idle" ||
- (input.trim() === "" && fileURLs.length === 0) ||
- fileURLs.length !== previews.length ||
- isLoading
- }
- >
- {isLoading ? (
- <svg
- className="animate-spin h-5 w-5"
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- >
- <circle
- className="opacity-25"
- cx="12"
- cy="12"
- r="10"
- stroke="currentColor"
- strokeWidth="4"
- ></circle>
- <path
- className="opacity-75"
- fill="currentColor"
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
- ></path>
- </svg>
- ) : (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 20 20"
- fill="currentColor"
- className="w-5 h-5"
- >
- <path d="M3.105 2.289a.75.75 0 00-.826.95l1.414 4.925A1.5 1.5 0 005.135 9.25h6.115a.75.75 0 010 1.5H5.135a1.5 1.5 0 00-1.442 1.086l-1.414 4.926a.75.75 0 00.826.95 28.896 28.896 0 0015.293-7.154.75.75 0 000-1.115A28.897 28.897 0 003.105 2.289z" />
- </svg>
- )}
- </Button>
- </div>
- </div>
- )}
- </div>
- );
-}
-
-export default MemoryInputForm;
diff --git a/apps/web/app/components/Histories.tsx b/apps/web/app/components/Histories.tsx
deleted file mode 100644
index 5b86c6d3..00000000
--- a/apps/web/app/components/Histories.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from "react";
-
-import { Clock } from "lucide-react";
-
-function Histories({
- historyMessages,
-}: {
- historyMessages: { chatId: string; firstMessage: string }[];
-}) {
- return (
- <div className="mt-12 max-w-lg hidden md:block">
- <div className="flex items-center gap-2 text-sm text-neutral-500 mb-2">
- <Clock className="w-4 h-4" />
- <span>Recently Asked</span>
- </div>
- <div className="space-y-2">
- {historyMessages.map((history) => (
- <a
- key={history.chatId}
- href={`/chat/${history.chatId}`}
- className="flex items-center gap-2 p-2 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-800 cursor-pointer text-sm text-neutral-600 dark:text-neutral-300"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="size-4 flex-shrink-0"
- >
- <path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
- </svg>
-
- <span className="line-clamp-1 truncate">{history.firstMessage}</span>
- </a>
- ))}
- </div>
- </div>
- );
-}
-
-export default Histories;
diff --git a/apps/web/app/components/Landing/Feature.tsx b/apps/web/app/components/Landing/Feature.tsx
deleted file mode 100644
index e46fa41e..00000000
--- a/apps/web/app/components/Landing/Feature.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-"use client";
-export default function Feature2() {
- return (
- <div className="flex flex-col gap-2 justify-center items-center">
- <svg
- className="w-[40%] h-[40%] mx-auto"
- viewBox="0 0 604 283"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- >
- <defs>
- <linearGradient id="pulseGradient1" x1="0%" y1="0%" x2="0%" y2="100%">
- <stop offset="0%" stop-color="rgba(255,255,255,0)"></stop>
- <stop offset="4%" stop-color="rgba(255,255,255,0.5)"></stop>
- <stop offset="8%" stop-color="rgba(255,255,255,0)"></stop>
- <stop offset="70%" stop-color="rgba(255,255,255,0)"></stop>
- <stop offset="74%" stop-color="rgba(255,255,255,0.5)"></stop>
- <stop offset="8%" stop-color="rgba(255,255,255,0)"></stop>
- <animate
- attributeName="y1"
- from="0%"
- to="100%"
- dur="2s"
- repeatCount="indefinite"
- begin="0s"
- ></animate>
- <animate
- attributeName="y2"
- from="100%"
- to="200%"
- dur="2s"
- repeatCount="indefinite"
- begin="0s"
- ></animate>
- </linearGradient>
- <linearGradient id="pulseGradient2" x1="0%" y1="0%" x2="0%" y2="100%">
- <stop offset="0%" stop-color="rgba(255,255,255,0)"></stop>
- <stop offset="4%" stop-color="rgba(255,255,255,0.5)"></stop>
- <stop offset="8%" stop-color="rgba(255,255,255,0)"></stop>
- <stop offset="70%" stop-color="rgba(255,255,255,0)"></stop>
- <stop offset="74%" stop-color="rgba(255,255,255,0.5)"></stop>
- <stop offset="78%" stop-color="rgba(255,255,255,0)"></stop>
- <animate
- attributeName="y1"
- from="0%"
- to="100%"
- dur="2s"
- repeatCount="indefinite"
- begin="0.25s"
- ></animate>
- <animate
- attributeName="y2"
- from="100%"
- to="200%"
- dur="2s"
- repeatCount="indefinite"
- begin="0.25s"
- ></animate>
- </linearGradient>
- <linearGradient id="pulseGradient3" x1="0%" y1="0%" x2="0%" y2="100%">
- <stop offset="0%" stop-color="rgba(255,255,255,0)"></stop>
- <stop offset="4%" stop-color="rgba(255,255,255,0.5)"></stop>
- <stop offset="8%" stop-color="rgba(255,255,255,0)"></stop>
- <stop offset="70%" stop-color="rgba(255,255,255,0)"></stop>
- <stop offset="74%" stop-color="rgba(255,255,255,0.5)"></stop>
- <stop offset="78%" stop-color="rgba(255,255,255,0)"></stop>
- <animate
- attributeName="y1"
- from="0%"
- to="100%"
- dur="2s"
- repeatCount="indefinite"
- begin="0.5s"
- ></animate>
- <animate
- attributeName="y2"
- from="100%"
- to="200%"
- dur="2s"
- repeatCount="indefinite"
- begin="0.5s"
- ></animate>
- </linearGradient>
- <linearGradient id="pulseGradient4" x1="0%" y1="0%" x2="0%" y2="100%">
- <stop offset="0%" stop-color="rgba(255,255,255,0)"></stop>
- <stop offset="4%" stop-color="rgba(255,255,255,0.5)"></stop>
- <stop offset="8%" stop-color="rgba(255,255,255,0)"></stop>
- <stop offset="70%" stop-color="rgba(255,255,255,0)"></stop>
- <stop offset="74%" stop-color="rgba(255,255,255,0.5)"></stop>
- <stop offset="78%" stop-color="rgba(255,255,255,0)"></stop>
- <animate
- attributeName="y1"
- from="0%"
- to="100%"
- dur="2s"
- repeatCount="indefinite"
- begin="0.75s"
- ></animate>
- <animate
- attributeName="y2"
- from="100%"
- to="200%"
- dur="2s"
- repeatCount="indefinite"
- begin="0.75s"
- ></animate>
- </linearGradient>
- <linearGradient id="blueBase" x1="0%" y1="0%" x2="0%" y2="100%">
- <stop offset="0%" stop-color="rgba(48, 165, 255, 0)"></stop>
- <stop offset="100%" stop-color="#30A5FF"></stop>
- </linearGradient>
- <linearGradient id="yellowBase" x1="0%" y1="0%" x2="0%" y2="100%">
- <stop offset="0%" stop-color="rgba(255, 186, 23, 0)"></stop>
- <stop offset="100%" stop-color="#FFBA17"></stop>
- </linearGradient>
- <linearGradient id="redBase" x1="0%" y1="0%" x2="0%" y2="100%">
- <stop offset="0%" stop-color="rgba(255, 134, 111, 0)"></stop>
- <stop offset="100%" stop-color="#FF866F"></stop>
- </linearGradient>
- <linearGradient id="purpleBase" x1="0%" y1="0%" x2="0%" y2="100%">
- <stop offset="0%" stop-color="rgba(151, 0, 244, 0)"></stop>
- <stop offset="100%" stop-color="#9700F4"></stop>
- </linearGradient>
- </defs>
- <path d="M3 0C3 157 280 90 280 282" stroke="url(#blueBase)" stroke-width="3"></path>
- <path d="M200 0C200 157 294 90 294 282" stroke="url(#yellowBase)" stroke-width="3"></path>
- <path d="M400 0C400 157 307 90 307 282" stroke="url(#redBase)" stroke-width="3"></path>
- <path d="M601 0C601 157 320 90 320 282" stroke="url(#purpleBase)" stroke-width="3"></path>
- <path d="M3 0C3 157 280 90 280 282" stroke="url(#pulseGradient1)" stroke-width="3"></path>
- <path
- d="M200 0C200 157 294 90 294 282"
- stroke="url(#pulseGradient2)"
- stroke-width="3"
- ></path>
- <path
- d="M400 0C400 157 307 90 307 282"
- stroke="url(#pulseGradient3)"
- stroke-width="3"
- ></path>
- <path
- d="M601 0C601 157 320 90 320 282"
- stroke="url(#pulseGradient4)"
- stroke-width="3"
- ></path>
- </svg>
- <div className="w-full mx-auto text-center">
- <h2 className="text-3xl md:text-4xl mb-1">Meet Supermemory.</h2>
- <h3 className="text-3xl md:text-4xl mb-8">Your second brain for knowledge.</h3>
- <p className="text-gray-600 max-w-3xl mx-auto">
- Save the things you like, and over time, build the knowledge base of your dreams.
- <br />
- Go down rabbit holes, make connections, search what's important to you.
- </p>
- </div>
- </div>
- );
-}
diff --git a/apps/web/app/components/Landing/Footer.tsx b/apps/web/app/components/Landing/Footer.tsx
deleted file mode 100644
index 3f0b3f3e..00000000
--- a/apps/web/app/components/Landing/Footer.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import { PlusGrid, PlusGridItem, PlusGridRow } from "./plus-grid";
-
-function SitemapHeading({ children }: { children: React.ReactNode }) {
- return <h3 className="text-sm/6 font-medium text-gray-950/50 dark:text-gray-200">{children}</h3>;
-}
-
-function SitemapLinks({ children }: { children: React.ReactNode }) {
- return <ul className="mt-6 space-y-4 text-sm/6">{children}</ul>;
-}
-
-function SitemapLink(props: React.ComponentPropsWithoutRef<"a">) {
- return (
- <li>
- <a
- {...props}
- className="font-medium text-gray-950 data-[hover]:text-gray-950/75 dark:text-gray-400"
- />
- </li>
- );
-}
-
-function Sitemap() {
- return (
- <>
- <div>
- <SitemapHeading>Product</SitemapHeading>
- <SitemapLinks>
- <SitemapLink href="https://docs.supermemory.ai">Documentation</SitemapLink>
- <SitemapLink href="https://supermemory.ai/extension">Chrome Extension</SitemapLink>
- <SitemapLink href="/shortcut">iOS Shortcut</SitemapLink>
- </SitemapLinks>
- </div>
- <div>
- <SitemapHeading>Community</SitemapHeading>
- <SitemapLinks>
- <SitemapLink href="https://discord.gg/b3BgKWpbtR">Discord</SitemapLink>
- <SitemapLink href="https://github.com/supermemoryai/supermemory/issues">
- Report Issue
- </SitemapLink>
- <SitemapLink href="mailto:[email protected]">Get Help</SitemapLink>
- </SitemapLinks>
- </div>
- <div>
- <SitemapHeading>Legal</SitemapHeading>
- <SitemapLinks>
- <SitemapLink href="https://supermemory.ai/tos">Terms of Service</SitemapLink>
- <SitemapLink href="https://supermemory.ai/privacy">Privacy Policy</SitemapLink>
- </SitemapLinks>
- </div>
- </>
- );
-}
-
-function SocialIconX(props: React.ComponentPropsWithoutRef<"svg">) {
- return (
- <svg viewBox="0 0 16 16" fill="currentColor" {...props}>
- <path d="M12.6 0h2.454l-5.36 6.778L16 16h-4.937l-3.867-5.594L2.771 16H.316l5.733-7.25L0 0h5.063l3.495 5.114L12.6 0zm-.86 14.376h1.36L4.323 1.539H2.865l8.875 12.837z" />
- </svg>
- );
-}
-
-function SocialIconGitHub(props: React.ComponentPropsWithoutRef<"svg">) {
- return (
- <svg viewBox="0 0 16 16" fill="currentColor" {...props}>
- <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
- </svg>
- );
-}
-
-function SocialLinks() {
- return (
- <>
- <a
- href="https://github.com/supermemoryai"
- target="_blank"
- aria-label="Visit us on GitHub"
- className="text-gray-950 data-[hover]:text-gray-950/75"
- >
- <SocialIconGitHub className="size-4" />
- </a>
- <a
- href="https://x.com/supermemoryai"
- target="_blank"
- aria-label="Visit us on X"
- className="text-gray-950 data-[hover]:text-gray-950/75"
- >
- <SocialIconX className="size-4" />
- </a>
- </>
- );
-}
-
-function Copyright() {
- return (
- <div className="text-sm/6 text-gray-950 dark:text-gray-100">
- &copy; {new Date().getFullYear()} Supermemory, Inc.
- </div>
- );
-}
-
-export default function Footer() {
- return (
- <footer className="mt-16 font-dm">
- <div className="absolute inset-2 rounded-4xl" />
- <div className="px-6 lg:px-8">
- <div className="mx-auto max-w-2xl lg:max-w-7xl">
- <PlusGrid className="pb-16">
- <PlusGridRow>
- <div className="grid grid-cols-2 gap-y-10 pb-6 lg:grid-cols-6 lg:gap-8">
- <div className="col-span-2 flex">
- <PlusGridItem className="pt-6 lg:pb-6">
- <h1 className="text-2xl font-semibold tracking-tighter dark:text-gray-300">
- Supermemory
- </h1>
- <p className="text-gray-500">
- Supermemory is a free, open-source AI knowlege platform.
- </p>
- </PlusGridItem>
- </div>
- <div className="col-span-2 grid grid-cols-2 gap-x-8 gap-y-12 lg:col-span-4 lg:grid-cols-subgrid lg:pt-6">
- <Sitemap />
- </div>
- </div>
- </PlusGridRow>
- <PlusGridRow className="flex justify-between">
- <div>
- <PlusGridItem className="py-3">
- <Copyright />
- </PlusGridItem>
- </div>
- <div className="flex">
- <PlusGridItem className="flex items-center gap-8 py-3">
- <SocialLinks />
- </PlusGridItem>
- </div>
- </PlusGridRow>
- </PlusGrid>
- </div>
- </div>
- </footer>
- );
-}
diff --git a/apps/web/app/components/Landing/Hero.tsx b/apps/web/app/components/Landing/Hero.tsx
deleted file mode 100644
index 353d0211..00000000
--- a/apps/web/app/components/Landing/Hero.tsx
+++ /dev/null
@@ -1,299 +0,0 @@
-"use client";
-
-import { DiscordIcon, GithubIcon } from "../icons/IntegrationIcons";
-import { Logo } from "../icons/Logo";
-
-interface PlusPatternBackgroundProps {
- plusSize?: number;
- plusColor?: string;
- backgroundColor?: string;
- className?: string;
- style?: React.CSSProperties;
- fade?: boolean;
- [key: string]: any;
-}
-
-export const BackgroundPlus: React.FC<PlusPatternBackgroundProps> = ({
- plusColor = "#CCE5FF",
- backgroundColor = "transparent",
- plusSize = 60,
- className,
- fade = true,
- style,
- ...props
-}) => {
- const encodedPlusColor = encodeURIComponent(plusColor);
-
- const maskStyle: React.CSSProperties = fade
- ? {
- maskImage: "radial-gradient(circle, white 10%, transparent 90%)",
- WebkitMaskImage: "radial-gradient(circle, white 10%, transparent 90%)",
- }
- : {};
-
- const backgroundStyle: React.CSSProperties = {
- backgroundColor,
- backgroundImage: `url("data:image/svg+xml,%3Csvg width='${plusSize}' height='${plusSize}' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='${encodedPlusColor}' fill-opacity='0.5'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
- ...maskStyle,
- ...style,
- };
-
- return (
- <div
- className={`absolute inset-0 h-full w-full opacity-50 ${className}`}
- style={backgroundStyle}
- {...props}
- ></div>
- );
-};
-
-export default function Hero() {
- return (
- <div className="relative z-[10] min-h-screen overflow-hidden">
- <div className="fixed bottom-0 left-0 right-0 flex justify-center z-[45] pointer-events-none">
- <div
- className="h-48 w-[95%] overflow-x-hidden bg-[#3B82F6] bg-opacity-100 md:bg-opacity-70 blur-[400px]"
- style={{ transform: "rotate(-30deg)" }}
- />
- </div>
- <BackgroundPlus />
-
- {/* Header */}
- <header className="fixed top-0 left-0 right-0 bg-white/80 backdrop-blur-md z-50 border-b border-gray-100/20">
- <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
- <div className="flex justify-between items-center h-16 md:h-20">
- {/* Left section */}
- <div className="flex items-center space-x-8">
- <div className="inline-flex gap-2 items-center">
- <Logo />
- <span className="text-lg lg:text-xl font-medium bg-clip-text text-transparent bg-gradient-to-r from-gray-900 to-gray-600 dark:from-white dark:to-gray-400">
- supermemory.ai
- </span>
- </div>
- <nav className="hidden lg:flex items-center space-x-8">
- {/* Products dropdown commented out for now
- <Popover className="relative">
- ...
- </Popover>
- */}
-
- <a
- href="https://docs.supermemory.ai"
- className="text-gray-600 hover:text-gray-900 transition-colors"
- >
- Docs
- </a>
- </nav>
- </div>
-
- {/* Right section */}
- <div className="flex items-center space-x-6">
- <div className="hidden sm:flex items-center space-x-6">
- <a
- href="https://git.new/memory"
- className="text-gray-600 hover:text-gray-900 transition-colors"
- >
- <GithubIcon className="h-6 w-6" />
- </a>
- <a
- href="https://discord.gg/b3BgKWpbtR"
- className="text-gray-600 hover:text-gray-900 transition-colors"
- >
- <DiscordIcon className="h-6 w-6" />
- </a>
- </div>
- <div className="flex items-center space-x-4">
- <a
- href="/signin"
- className="[box-shadow:0_-20px_80px_-20px_#CCE5FF_inset] bg-[#1E3A8A] text-white px-5 py-2.5 rounded-lg hover:bg-opacity-90 transition-all duration-200 hover:translate-y-[-1px]"
- >
- Get started
- </a>
- </div>
- </div>
- </div>
- </div>
- </header>
-
- {/* Hero Section */}
- <main className="pt-32 md:pt-40 relative z-[20] pb-24">
- <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
- <div className="grid lg:grid-cols-2 gap-8 items-start">
- {/* Hero Content */}
- <div className="text-left max-w-xl mx-auto lg:mx-0">
- {/* Announcement Banner */}
- <div className="flex mb-10">
- <div className="inline-flex items-center space-x-3 bg-white/90 rounded-full px-5 py-2.5 shadow-sm hover:shadow-md transition-all duration-200">
- <span className="bg-[#3B82F6] text-white text-xs px-2.5 py-1 rounded-full font-medium">
- NEW
- </span>
- <span className="text-gray-600">Top OSS Repository in 2024</span>
- <a
- href="https://runacap.com/ross-index/q3-2024/"
- className="text-[#1E3A8A] font-medium hover:text-[#3B82F6] transition-colors"
- >
- Read more →
- </a>
- </div>
- </div>
- <h1 className="text-5xl md:text-6xl font-bold text-gray-900 tracking-tight leading-[1.1]">
- AI for all your knowledge.
- </h1>
- <p className="text-xl text-gray-600 mt-6 mb-8 leading-relaxed">
- Supermemory helps you collect, organize, and recall all your knowledge.
- {/* list of notable features */}
- <ul className="list-none space-y-3 mt-6">
- <li className="flex items-center space-x-3">
- <svg
- className="h-5 w-5 text-[#3B82F6]"
- fill="none"
- viewBox="0 0 24 24"
- stroke="currentColor"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M5 13l4 4L19 7"
- />
- </svg>
- <span>Connect with your existing tools and bookmarks</span>
- </li>
- <li className="flex items-center space-x-3">
- <svg
- className="h-5 w-5 text-[#3B82F6]"
- fill="none"
- viewBox="0 0 24 24"
- stroke="currentColor"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M5 13l4 4L19 7"
- />
- </svg>
- <span>Chat and find with AI & actually use your knowledge</span>
- </li>
- <li className="flex items-center space-x-3">
- <svg
- className="h-5 w-5 text-[#3B82F6]"
- fill="none"
- viewBox="0 0 24 24"
- stroke="currentColor"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M5 13l4 4L19 7"
- />
- </svg>
- <span>Share your knowledge with your friends and colleagues</span>
- </li>
- </ul>
- </p>
- <div className="flex flex-col space-y-8">
- <div className="flex flex-col sm:flex-row items-start sm:items-center space-y-4 sm:space-y-0 sm:space-x-6">
- <a
- href="/signin"
- className="w-full sm:w-auto [box-shadow:0_-20px_80px_-20px_#CCE5FF_inset] bg-gradient-to-tr from-[#1E3A8A] to-[#3B82F6] text-white px-8 py-4 rounded-xl hover:shadow-lg hover:translate-y-[-2px] transition-all duration-200 text-center font-medium"
- >
- Get started for free
- </a>
- <div className="flex items-center space-x-6 text-sm text-gray-600">
- <a
- href="https://git.new/memory"
- className="flex items-center hover:text-[#1E3A8A] transition-colors group"
- >
- <GithubIcon className="h-5 w-5 mr-2 group-hover:scale-110 transition-transform" />
- GitHub
- </a>
- <a
- href="https://docs.supermemory.ai"
- className="flex items-center hover:text-[#1E3A8A] transition-colors group"
- >
- <svg
- className="h-5 w-5 mr-2 group-hover:scale-110 transition-transform"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
- />
- </svg>
- Documentation
- </a>
- </div>
- </div>
-
- <div className="flex items-center space-x-4">
- <img
- src="/product-of-the-day.png"
- className="w-44 hover:opacity-90 transition-opacity"
- alt="Product of the Day on Product Hunt"
- />
- </div>
- </div>
- </div>
-
- {/* Video Section */}
- <div className="w-full mt-24">
- <div
- style={{ position: "relative", paddingTop: "56.25%" }}
- className="rounded-2xl overflow-hidden shadow-2xl hover:shadow-3xl transition-shadow duration-300"
- >
- <iframe
- src="https://customer-5xczlbkyq4f9ejha.cloudflarestream.com/111c4828c3587348bc703e67bfca9682/iframe?muted=true&poster=https%3A%2F%2Fcustomer-5xczlbkyq4f9ejha.cloudflarestream.com%2F111c4828c3587348bc703e67bfca9682%2Fthumbnails%2Fthumbnail.jpg%3Ftime%3D%26height%3D600"
- loading="lazy"
- style={{
- border: "none",
- position: "absolute",
- top: 0,
- left: 0,
- height: "100%",
- width: "100%",
- }}
- allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
- allowFullScreen={true}
- ></iframe>
- </div>
- </div>
- </div>
-
- {/* Integration Tags */}
- <div className="mt-32">
- <div className="text-gray-900 font-medium mb-8 text-center text-lg">
- Integrate with your favorite tools
- </div>
- <div className="flex flex-wrap justify-center gap-4">
- {[
- "Notion",
- "Twitter",
- "Obsidian",
- "Reddit",
- "LinkedIn",
- "Chrome Extension",
- "iOS App",
- "Slack",
- // "Google Drive",
- // "Microsoft Teams"
- ].map((tool) => (
- <div
- key={tool}
- className="bg-white/90 rounded-full px-5 py-2.5 shadow-sm hover:shadow-md hover:bg-white hover:translate-y-[-1px] transition-all duration-200 cursor-pointer"
- >
- {tool}
- </div>
- ))}
- </div>
- </div>
- </div>
- </main>
- </div>
- );
-}
diff --git a/apps/web/app/components/Landing/Note.tsx b/apps/web/app/components/Landing/Note.tsx
deleted file mode 100644
index 8fe2d4e1..00000000
--- a/apps/web/app/components/Landing/Note.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-export default function Note() {
- return (
- <div className="bg-gradient-to-b from-white to-gray-50 py-24">
- <div className="px-6 lg:px-8">
- <div className="mx-auto max-w-2xl lg:max-w-7xl">
- <div className="max-w-4xl mx-auto">
- <div className="flex flex-col items-center mb-12">
- <div className="bg-gray-300 w-40 h-1 rounded-full mb-2" />
- <div className="text-sm text-gray-500">Today</div>
- </div>
-
- <div className="flex justify-center">
- <div className="relative max-w-2xl w-full">
- {/* Profile Image */}
- <div className="absolute -top-12 left-4">
- <img
- src="https://pbs.twimg.com/profile_images/1813041528278843392/u50EIuLZ_400x400.jpg"
- alt="Dhravya Shah"
- className="w-16 h-16 rounded-full border-4 border-white shadow-lg"
- />
- </div>
-
- {/* Message Bubble */}
- <div className="bg-gradient-to-br from-blue-500 to-blue-600 text-white rounded-3xl px-8 py-6 shadow-lg">
- <p className="text-lg leading-relaxed space-y-4">
- <span className="block">👋 Hey there! I'm Dhravya</span>
-
- <span className="block">
- I'm a college student who built Supermemory as a weekend project. What started
- as a simple idea has grown into something I'm really proud of, thanks to
- amazing support from the open-source community! 🚀
- </span>
-
- <span className="block">
- When you see "we" on the website - that's actually just me! 😅 I maintain and
- build everything myself, supported by wonderful donors and grants that help
- keep this project free and open source.
- </span>
-
- <span className="block">
- In this AI-driven world, I believe in augmenting human knowledge rather than
- replacing it. My goal is simple: build something that genuinely helps people
- learn and grow. 💡
- </span>
-
- <span className="block">
- If you'd like to follow my journey, you can find me on{" "}
- <a href="https://x.com/dhravyashah" className="underline hover:text-blue-100">
- Twitter
- </a>{" "}
- and{" "}
- <a href="https://git.new/memory" className="underline hover:text-blue-100">
- GitHub
- </a>
- . And if you believe in what we're building, consider{" "}
- <a
- href="https://github.com/sponsors/dhravya"
- className="underline hover:text-blue-100"
- >
- supporting Supermemory's development
- </a>{" "}
- ❤️
- </span>
- </p>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- );
-}
diff --git a/apps/web/app/components/Landing/Private.tsx b/apps/web/app/components/Landing/Private.tsx
deleted file mode 100644
index 7e944887..00000000
--- a/apps/web/app/components/Landing/Private.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Eye, Lock, ShieldCheck } from "lucide-react";
-
-export default function Private() {
- const privacyFeatures = [
- { icon: ShieldCheck, text: "End-to-end encryption" },
- { icon: Lock, text: "Self-hosted option available" },
- { icon: Eye, text: "Zero knowledge architecture" },
- ];
-
- return (
- <div className="min-h-full my-7 flex items-center justify-center p-4">
- <div className="max-w-[1000px] px-2 md:px-10 w-full bg-[#F5F7FF] rounded-3xl p-16 shadow-[0_2px_40px_rgba(0,0,0,0.05)]">
- <div className="space-y-6 text-center">
- <h1 className="text-[40px] leading-[1.2] font-medium tracking-[-0.02em] text-[#111111]">
- Your knowledge stays
- <br />
- private and secure
- </h1>
-
- <p className="text-[#666666] text-lg leading-relaxed max-w-[600px] mx-auto">
- We take privacy seriously. Your data is fully encrypted, never shared with third
- parties. Even on the hosted version, we securely store your data in our own servers.
- </p>
-
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8 pt-8 max-w-[700px] mx-auto">
- {privacyFeatures.map((feature, index) => (
- <div key={index} className="flex flex-col items-center gap-3">
- <div className="w-12 h-12 rounded-xl bg-white shadow-[0_2px_8px_rgba(0,0,0,0.05)] flex items-center justify-center">
- <feature.icon className="w-6 h-6 text-[#3B82F6]" />
- </div>
- <span className="text-sm text-gray-700 font-medium">{feature.text}</span>
- </div>
- ))}
- </div>
-
- <div className="pt-10 flex flex-wrap justify-center gap-4">
- <a
- href="https://docs.supermemory.ai/self-hosting"
- className="inline-flex items-center gap-2 bg-[#1E3A8A] text-white py-3 px-6 rounded-lg hover:bg-opacity-90 transition-all duration-200 hover:translate-y-[-1px]"
- >
- Self-host Supermemory
- </a>
-
- <a
- href="https://docs.supermemory.ai/essentials/architecture"
- className="inline-flex items-center gap-2 bg-white text-gray-700 py-3 px-6 rounded-lg hover:bg-gray-50 transition-all duration-200 hover:translate-y-[-1px]"
- >
- Our architecture
- </a>
-
- <a
- href="https://git.new/memory"
- className="inline-flex items-center gap-2 bg-white text-gray-700 py-3 px-6 rounded-lg hover:bg-gray-50 transition-all duration-200 hover:translate-y-[-1px]"
- >
- Check out the code
- </a>
- </div>
- </div>
- </div>
- </div>
- );
-}
diff --git a/apps/web/app/components/Landing/index.tsx b/apps/web/app/components/Landing/index.tsx
deleted file mode 100644
index 0a432ec2..00000000
--- a/apps/web/app/components/Landing/index.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import Feature2 from "./Feature";
-import Footer from "./Footer";
-import Hero from "./Hero";
-import Note from "./Note";
-import Private from "./Private";
-
-export default function Landing() {
- return (
- <div className="overflow-hidden">
- <Hero />
- <Feature2 />
- <Private />
- <Note />
- <Footer />
- </div>
- );
-}
diff --git a/apps/web/app/components/Landing/plus-grid.tsx b/apps/web/app/components/Landing/plus-grid.tsx
deleted file mode 100644
index 752ccefc..00000000
--- a/apps/web/app/components/Landing/plus-grid.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import { clsx } from "clsx";
-
-export function PlusGrid({
- className = "",
- children,
-}: {
- className?: string;
- children: React.ReactNode;
-}) {
- return <div className={className}>{children}</div>;
-}
-
-export function PlusGridRow({
- className = "",
- children,
-}: {
- className?: string;
- children: React.ReactNode;
-}) {
- return (
- <div
- className={clsx(
- className,
- "group/row relative isolate pt-[calc(theme(spacing.2)+1px)] last:pb-[calc(theme(spacing.2)+1px)]",
- )}
- >
- <div
- aria-hidden="true"
- className="absolute inset-y-0 left-1/2 -z-10 w-screen -translate-x-1/2"
- >
- <div className="absolute inset-x-0 top-0 border-t border-black/10 dark:border-white/10"></div>
- <div className="absolute inset-x-0 top-2 border-t border-black/10 dark:border-white/10"></div>
- <div className="absolute inset-x-0 bottom-0 hidden border-b border-black/10 group-last/row:block dark:border-white/10"></div>
- <div className="b absolute inset-x-0 bottom-2 hidden border-b border-black/10 group-last/row:block dark:border-white/10"></div>
- </div>
- {children}
- </div>
- );
-}
-
-export function PlusGridItem({
- className = "",
- children,
-}: {
- className?: string;
- children: React.ReactNode;
-}) {
- return (
- <div className={clsx(className, "group/item relative")}>
- <PlusGridIcon placement="top left" className="hidden group-first/item:block" />
- <PlusGridIcon placement="top right" />
- <PlusGridIcon
- placement="bottom left"
- className="hidden group-last/row:group-first/item:block"
- />
- <PlusGridIcon placement="bottom right" className="hidden group-last/row:block" />
- {children}
- </div>
- );
-}
-
-export function PlusGridIcon({
- className = "",
- placement,
-}: {
- className?: string;
- placement: `${"top" | "bottom"} ${"right" | "left"}`;
-}) {
- let [yAxis, xAxis] = placement.split(" ");
-
- let yClass = yAxis === "top" ? "-top-2" : "-bottom-2";
- let xClass = xAxis === "left" ? "-left-2" : "-right-2";
-
- return (
- <svg
- viewBox="0 0 15 15"
- aria-hidden="true"
- className={clsx(
- className,
- "absolute size-[15px] fill-black/5 dark:fill-white/10",
- yClass,
- xClass,
- )}
- >
- <path d="M8 0H7V7H0V8H7V15H8V8H15V7H8V0Z" />
- </svg>
- );
-}
diff --git a/apps/web/app/components/Navbar.tsx b/apps/web/app/components/Navbar.tsx
deleted file mode 100644
index dc5b8485..00000000
--- a/apps/web/app/components/Navbar.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import { Form } from "@remix-run/react";
-
-import { Logo } from "./icons/Logo";
-import { AddMemoryModal } from "./memories/AddMemory";
-import { Button } from "./ui/button";
-
-import { User } from "@supermemory/shared/types";
-import {
- ChevronDown,
- Cloud,
- Github,
- LifeBuoy,
- LogOut,
- Moon,
- Sun,
- User as UserIcon,
-} from "lucide-react";
-import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuShortcut,
- DropdownMenuTrigger,
-} from "~/components/ui/dropdown-menu";
-import { useKeyboardShortcut } from "~/lib/hooks/use-keyboard";
-import { Theme, useTheme } from "~/lib/theme-provider";
-
-function Navbar({ user }: { user?: User }) {
- const [theme, setTheme] = useTheme();
-
- const toggleTheme = (e: Event) => {
- e.preventDefault();
- setTheme(theme === Theme.LIGHT ? Theme.DARK : Theme.LIGHT);
- };
-
- useKeyboardShortcut(
- ["c"],
- () => {
- console.log("Add Memory");
- document.getElementById("add-memory-button")?.click();
- },
- "Add Memory",
- );
-
- return (
- <div className="w-full sticky top-0 z-50 bg-background text-foreground flex items-center justify-between h-16 px-6">
- <a href="/" className="flex items-center gap-2">
- <Logo className="dark:fill-foreground" />
- </a>
-
- <div className="flex items-center gap-2">
- {user ? (
- <>
- <AddMemoryModal>
- <Button
- id="add-memory-button"
- size={"sm"}
- variant="secondary"
- className="flex items-center gap-2"
- >
- <span className="text-xs bg-slate-300 dark:bg-slate-700 px-1 py-0.5 rounded-md">
- C
- </span>
- Add Memory
- </Button>
- </AddMemoryModal>
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <div className="flex items-center gap-2 cursor-pointer">
- <ChevronDown className="text-muted-foreground" />
- <Avatar className="w-8 h-8">
- <AvatarImage
- src={user.profilePictureUrl ?? ""}
- alt={user.firstName ?? "Profile picture"}
- />
- <AvatarFallback>
- {user.firstName?.charAt(0) ?? "?"}
- {user.lastName?.charAt(0) ?? "?"}
- </AvatarFallback>
- </Avatar>
- </div>
- </DropdownMenuTrigger>
- <DropdownMenuContent className="w-56 mr-8">
- <DropdownMenuLabel className="text-wrap break-words">
- {user.firstName} {user.lastName} <br />
- <span className="text-sm text-muted-foreground">{user.email}</span>
- </DropdownMenuLabel>
- <DropdownMenuSeparator />
- <DropdownMenuGroup>
- {/* TODO: Add profile modal */}
- {/* <ProfileModal>
- <DropdownMenuItem>
- <UserIcon className="mr-2 h-4 w-4" />
- <span>Profile</span>
- </DropdownMenuItem>
- </ProfileModal> */}
- <DropdownMenuItem onSelect={toggleTheme}>
- {theme === Theme.LIGHT ? (
- <Sun className="mr-2 h-4 w-4" />
- ) : (
- <Moon className="mr-2 h-4 w-4" />
- )}
- <span>{theme === Theme.LIGHT ? "Dark mode" : "Light mode"}</span>
- </DropdownMenuItem>
- <DropdownMenuItem asChild>
- <a href="https://portal.productboard.com/8rhspck6pdelv78mptczaena" target="_blank" rel="noreferrer">
- <LifeBuoy className="mr-2 h-4 w-4" />
- <span>Support</span>
- </a>
- </DropdownMenuItem>
- <DropdownMenuItem asChild>
- <a href="https://api.supermemory.ai" target="_blank" rel="noreferrer">
- <Cloud className="mr-2 h-4 w-4" />
- <span>API</span>
- </a>
- </DropdownMenuItem>
- </DropdownMenuGroup>
- <DropdownMenuSeparator />
- <DropdownMenuItem asChild>
- <a
- href="https://github.com/supermemoryai/supermemory"
- className="flex items-center"
- >
- <Github className="mr-2 h-4 w-4" />
- <span>GitHub</span>
- </a>
- </DropdownMenuItem>
- <DropdownMenuSeparator />
- <DropdownMenuItem asChild>
- <Form action="/action/sign-out" method="post">
- <button className="w-full flex items-center gap-2" type="submit">
- <LogOut className="mr-2 h-4 w-4" /> Log out
- </button>
- </Form>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </>
- ) : (
- <div className="flex gap-2">
- <Button variant="secondary" asChild>
- <a href="/signin">Login to SuperMemory</a>
- </Button>
- </div>
- )}
- </div>
- </div>
- );
-}
-
-export default Navbar;
diff --git a/apps/web/app/components/Reminders.tsx b/apps/web/app/components/Reminders.tsx
deleted file mode 100644
index 5de30e25..00000000
--- a/apps/web/app/components/Reminders.tsx
+++ /dev/null
@@ -1,354 +0,0 @@
-import React, { useEffect, useState } from "react";
-import Markdown from "react-markdown";
-
-import { useNavigate } from "@remix-run/react";
-
-import image from "./gradients/gradient1.png";
-import { AddMemoryModal } from "./memories/AddMemory";
-import { Button } from "./ui/button";
-
-import { MemoryIcon, SpaceIcon } from "@supermemory/shared/icons";
-import { motion } from "framer-motion";
-import { BookIcon, BookOpen, ChevronDownIcon, SparkleIcon } from "lucide-react";
-import { Theme, useTheme } from "~/lib/theme-provider";
-
-function Reminder({ content, contentId }: { content: string; contentId: string }) {
- const [theme] = useTheme();
- const navigate = useNavigate();
- const [isExpanded, setIsExpanded] = useState(false);
- const [isOverflowing, setIsOverflowing] = useState(false);
- const contentRef = React.useRef<HTMLDivElement>(null);
-
- useEffect(() => {
- if (contentRef.current) {
- setIsOverflowing(contentRef.current.scrollHeight > contentRef.current.clientHeight);
- }
- }, [content]);
-
- if (typeof window === "undefined") return null;
-
- return (
- <div
- style={{
- backgroundImage:
- theme === Theme.DARK ? "linear-gradient(to right, #414345, #232526)" : `url(${image})`,
- backgroundSize: "cover",
- backgroundPosition: "right",
- }}
- className="border border-slate-500 rounded-2xl md:rounded-3xl flex flex-col gap-2 p-4 md:p-6 md:px-8 dark:bg-opacity-50 h-[500px]"
- >
- <div className="flex items-center gap-2 text-neutral-600 dark:text-neutral-300">
- <SparkleIcon className="w-3 h-3 md:w-4 md:h-4 flex-shrink-0" />
- <h2 className="text-xs md:text-sm truncate">Remember this? A topic you forgot.</h2>
- </div>
-
- <div ref={contentRef} className={`relative ${isExpanded ? "flex-1" : "h-[400px]"}`}>
- <div className={`${!isExpanded && "absolute inset-0"} overflow-hidden`}>
- <Markdown className="mt-2 prose prose-lg dark:prose-invert leading-tight break-words line-clamp-[15] prose-h1:text-2xl prose-h1:font-semibold prose-h1:tracking-tight prose-h2:text-lg prose-h3:text-base">
- {content}
- </Markdown>
- </div>
- {!isExpanded && isOverflowing && (
- <div className="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-white dark:from-gray-900 to-transparent" />
- )}
- </div>
-
- {isOverflowing && (
- <Button
- variant="ghost"
- size="sm"
- onClick={() => setIsExpanded(!isExpanded)}
- className="w-full text-xs flex items-center gap-1 py-1"
- >
- {isExpanded ? "Show less" : "Show more"}
- <ChevronDownIcon
- className={`w-4 h-4 transition-transform ${isExpanded ? "rotate-180" : ""}`}
- />
- </Button>
- )}
-
- <div className="grid grid-cols-2 gap-2 md:gap-4 mt-auto">
- <Button
- onClick={() => navigate(`/content/${contentId}`)}
- className="dark flex items-center justify-center gap-1 md:gap-2 border text-xs md:text-sm py-2 min-h-[36px]"
- >
- <BookIcon
- fill="var(--gray-10)"
- stroke="var(--gray-5)"
- className="w-3 h-3 md:w-4 md:h-4 flex-shrink-0"
- />
- <span className="truncate">
- View <span className="hidden md:inline">content</span>
- </span>
- </Button>
- <Button className="dark flex items-center justify-center gap-1 md:gap-2 border text-xs md:text-sm py-2 min-h-[36px]">
- <SpaceIcon className="w-3 h-3 md:w-4 md:h-4 flex-shrink-0" />
- <span className="truncate">
- Show <span className="hidden md:inline">related</span>
- </span>
- </Button>
- </div>
- </div>
- );
-}
-
-function LoadingReminder() {
- return (
- <div className="border border-slate-500 rounded-2xl md:rounded-3xl flex flex-col gap-2 p-4 md:p-6 md:px-8 dark:bg-opacity-50 h-[500px] animate-pulse bg-gray-100 dark:bg-gray-800">
- <div className="flex items-center gap-2">
- <div className="w-4 h-4 bg-gray-300 dark:bg-gray-600 rounded-full" />
- <div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-48" />
- </div>
- <div className="flex-1 bg-gray-200 dark:bg-gray-700 rounded-lg mt-2" />
- <div className="grid grid-cols-2 gap-2 md:gap-4">
- <div className="h-9 bg-gray-300 dark:bg-gray-600 rounded" />
- <div className="h-9 bg-gray-300 dark:bg-gray-600 rounded" />
- </div>
- </div>
- );
-}
-
-function Reminders() {
- const [suggestedLearnings, setSuggestedLearnings] = useState<Array<Record<string, string>>>([]);
- const [currentIndex, setCurrentIndex] = useState(0);
- const [isLoading, setIsLoading] = useState(true);
- const navigate = useNavigate();
-
- useEffect(() => {
- fetch(`/backend/v1/suggested-learnings`, {
- credentials: "include",
- })
- .then((res) => res.json() as Promise<{ suggestedLearnings: Array<Record<string, string>> }>)
- .then((data) => {
- setSuggestedLearnings(data.suggestedLearnings);
- setIsLoading(false);
- })
- .catch((err) => {
- console.error("Failed to fetch suggested learnings:", err);
- setIsLoading(false);
- });
- }, []);
-
- if (typeof window === "undefined") return null;
-
- const handleDragEnd = (event: any, info: any) => {
- const DRAG_THRESHOLD = window?.innerWidth * 0.15;
-
- if (Math.abs(info.offset.x) > DRAG_THRESHOLD) {
- const direction = info.offset.x > 0 ? -1 : 1;
- handleNavigate(direction);
- }
- };
-
- const handleNavigate = (direction: number) => {
- setCurrentIndex((prevIndex) => {
- const newIndex = prevIndex + direction;
- if (newIndex < 0) return suggestedLearnings.length - 1;
- if (newIndex >= suggestedLearnings.length) return 0;
- return newIndex;
- });
- };
-
- if (!isLoading && (!suggestedLearnings || suggestedLearnings.length === 0)) {
- return (
- <div className="relative h-[600px] w-full flex items-center justify-center">
- <div className="text-center max-w-lg mx-auto px-4">
- <motion.div
- initial={{ opacity: 0, y: 20 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ duration: 0.5 }}
- >
- <SparkleIcon className="w-12 h-12 mx-auto mb-6 text-blue-500" />
- <h2 className="text-2xl font-bold mb-4 bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent">
- Your Memory Hub Awaits
- </h2>
- <p className="text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
- Start saving content to get bite-sized memory snippets that keep you on top of your
- game. We'll transform your notes into smart reminders that help you retain knowledge
- effortlessly.
- </p>
- <AddMemoryModal>
- <Button className="bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:from-blue-600 hover:to-purple-600 transition-all">
- <BookOpen className="w-4 h-4 mr-2" />
- Add Your First Memory
- </Button>
- </AddMemoryModal>
- </motion.div>
- </div>
- </div>
- );
- }
-
- return (
- <div
- className="relative h-[600px] w-full overflow-x-hidden md:overflow-visible overflow-y-hidden"
- role="region"
- aria-label="Memory reminders stack"
- >
- <div className="relative w-full h-full translate-y-16">
- {isLoading
- ? // Show 3 loading cards when loading
- [...Array(3)]?.map((_, index) => {
- const xOffset = index * (window?.innerWidth < 768 ? 12 : 16);
- const yOffset = index * (window?.innerWidth < 768 ? 12 : 16);
- const opacity = Math.max(1 - index * 0.15, 0.4);
- const scale = 1 - index * (window?.innerWidth < 768 ? 0.02 : 0.03);
-
- return (
- <motion.div
- key={`loading-${index}`}
- className="absolute w-full left-0 top-0 px-4 md:px-0"
- style={{ zIndex: 3 - index }}
- animate={{
- x: xOffset,
- y: yOffset,
- opacity,
- scale,
- }}
- >
- <LoadingReminder />
- </motion.div>
- );
- })
- : suggestedLearnings?.map((learning, index) => {
- const position =
- (index - currentIndex + suggestedLearnings.length) % suggestedLearnings.length;
-
- if (position >= 3) return null;
-
- if (typeof window === "undefined") return null;
-
- const contentId = Object.keys(learning)[0];
- const content = learning[contentId];
-
- const zIndex = suggestedLearnings.length - position;
- const xOffset = position * (window?.innerWidth < 768 ? 12 : 16);
- const yOffset = position * (window?.innerWidth < 768 ? 12 : 16);
- const opacity = Math.max(1 - position * 0.15, 0.4);
- const scale = 1 - position * (window?.innerWidth < 768 ? 0.02 : 0.03);
-
- return (
- <motion.div
- key={index}
- className="absolute w-full left-0 top-0 cursor-grab active:cursor-grabbing px-4 md:px-0"
- style={{ zIndex }}
- animate={{
- x: xOffset,
- y: yOffset,
- opacity,
- scale,
- }}
- transition={{
- type: "spring",
- stiffness: 200,
- damping: 25,
- mass: 1.4,
- restDelta: 0.0001,
- restSpeed: 0.0001,
- }}
- drag={position === 0 ? "x" : false}
- dragConstraints={{ left: 0, right: 0 }}
- dragElastic={0.7}
- dragTransition={{
- bounceStiffness: 200,
- bounceDamping: 25,
- power: 0.3,
- }}
- onDragEnd={position === 0 ? handleDragEnd : undefined}
- whileHover={
- position === 0 && window?.innerWidth >= 768
- ? {
- transition: {
- duration: 0.3,
- ease: [0.4, 0, 0.2, 1],
- },
- }
- : undefined
- }
- role="group"
- aria-label={`Memory card ${index + 1} of ${suggestedLearnings.length}`}
- >
- <Reminder content={content} contentId={contentId} />
- </motion.div>
- );
- })}
- </div>
-
- {/* Navigation controls - Hidden on mobile since we use swipe */}
- {!isLoading && suggestedLearnings.length > 0 && (
- <div className="hidden md:flex absolute bottom-[-48px] left-1/2 transform -translate-x-1/2 items-center gap-4 z-50">
- <motion.button
- whileHover={{ scale: 1.1 }}
- whileTap={{ scale: 0.95 }}
- transition={{ type: "spring", stiffness: 400, damping: 17 }}
- onClick={() => handleNavigate(-1)}
- className="p-2 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700"
- aria-label="Previous card"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="24"
- height="24"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- >
- <path d="m15 18-6-6 6-6" />
- </svg>
- </motion.button>
-
- <div className="flex gap-2">
- {suggestedLearnings?.map((_, index) => (
- <motion.button
- key={index}
- whileHover={{ scale: 1.2 }}
- whileTap={{ scale: 0.9 }}
- transition={{ type: "spring", stiffness: 400, damping: 17 }}
- onClick={() => handleNavigate(index - currentIndex)}
- className={`w-1.5 h-1.5 rounded-full transition-colors ${
- index === currentIndex ? "bg-blue-500" : "bg-gray-300"
- } hover:bg-blue-400`}
- aria-label={`Go to card ${index + 1}`}
- />
- ))}
- </div>
-
- <motion.button
- whileHover={{ scale: 1.1 }}
- whileTap={{ scale: 0.95 }}
- transition={{ type: "spring", stiffness: 400, damping: 17 }}
- onClick={() => handleNavigate(1)}
- className="p-2 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700"
- aria-label="Next card"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="24"
- height="24"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- >
- <path d="m9 18 6-6-6-6" />
- </svg>
- </motion.button>
- </div>
- )}
-
- {/* Mobile swipe indicator */}
- {!isLoading && suggestedLearnings.length > 0 && (
- <div className="md:hidden absolute bottom-[-24px] left-1/2 transform -translate-x-1/2 text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
- Swipe to navigate • {currentIndex + 1}/{suggestedLearnings.length}
- </div>
- )}
- </div>
- );
-}
-
-export default Reminders;
diff --git a/apps/web/app/components/Suggestions.tsx b/apps/web/app/components/Suggestions.tsx
deleted file mode 100644
index 56c40eda..00000000
--- a/apps/web/app/components/Suggestions.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from "react";
-import { cn } from "~/lib/utils";
-import { Button } from "~/components/ui/button";
-import { CircleHelp } from "lucide-react";
-
-interface SuggestionProps {
- text: string;
- onClick: () => void;
-}
-
-const SingleSuggestion: React.FC<SuggestionProps> = ({ text, onClick }) => {
- return (
- <Button
- variant="secondary"
- className="text-muted-foreground w-full md:w-auto text-xs md:text-sm text-left h-auto whitespace-normal"
- onClick={onClick}
- >
- <CircleHelp className="w-4 h-4 mr-2 flex-shrink-0" />
- <span className="line-clamp-2">{text}</span>
- </Button>
- );
-};
-
-interface SuggestionsProps {
- items: string[];
- onSelect: (item: string) => void;
-}
-
-const Suggestions: React.FC<SuggestionsProps> = ({ items, onSelect }) => {
- return (
- <div className={cn("grid grid-cols-1 md:flex md:flex-wrap gap-2 mt-4")}>
- {items?.map((item, index) => (
- <SingleSuggestion
- key={index}
- text={item}
- onClick={() => onSelect(item)}
- />
- ))}
- </div>
- );
-};
-
-export default Suggestions;
diff --git a/apps/web/app/components/editor/plate-editor.tsx b/apps/web/app/components/editor/plate-editor.tsx
deleted file mode 100644
index ed9237de..00000000
--- a/apps/web/app/components/editor/plate-editor.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { DndProvider } from "react-dnd";
-import { HTML5Backend } from "react-dnd-html5-backend";
-
-import { Plate } from "@udecode/plate-common/react";
-import { useCreateEditor } from "~/components/editor/use-create-editor";
-import { Editor, EditorContainer } from "~/components/plate-ui/editor";
-
-export function PlateEditor() {
- const editor = useCreateEditor();
-
- return (
- <DndProvider backend={HTML5Backend}>
- <Plate editor={editor}>
- <EditorContainer className="w-full border z-[99999]">
- <Editor variant="default" />
- </EditorContainer>
- </Plate>
- </DndProvider>
- );
-}
diff --git a/apps/web/app/components/editor/plate-types.ts b/apps/web/app/components/editor/plate-types.ts
deleted file mode 100644
index 75e79cfc..00000000
--- a/apps/web/app/components/editor/plate-types.ts
+++ /dev/null
@@ -1,221 +0,0 @@
-'use client';
-
-import type React from 'react';
-
-import type { BlockquotePlugin } from '@udecode/plate-block-quote/react';
-import type {
- CodeBlockPlugin,
- CodeLinePlugin,
-} from '@udecode/plate-code-block/react';
-import type { TCommentText } from '@udecode/plate-comments';
-import type { TElement, TText } from '@udecode/plate-common';
-import type { ParagraphPlugin } from '@udecode/plate-common/react';
-import type { TExcalidrawElement } from '@udecode/plate-excalidraw';
-import type { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react';
-import type { HEADING_KEYS } from '@udecode/plate-heading';
-import type { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
-import type { TLinkElement } from '@udecode/plate-link';
-import type { LinkPlugin } from '@udecode/plate-link/react';
-import type { TImageElement, TMediaEmbedElement } from '@udecode/plate-media';
-import type { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react';
-import type {
- TMentionElement,
- TMentionInputElement,
-} from '@udecode/plate-mention';
-import type {
- MentionInputPlugin,
- MentionPlugin,
-} from '@udecode/plate-mention/react';
-import type { TTableElement } from '@udecode/plate-table';
-import type {
- TableCellPlugin,
- TablePlugin,
- TableRowPlugin,
-} from '@udecode/plate-table/react';
-import type { TToggleElement } from '@udecode/plate-toggle';
-import type { TogglePlugin } from '@udecode/plate-toggle/react';
-
-/** Text */
-
-export type EmptyText = {
- text: '';
-};
-
-export type PlainText = {
- text: string;
-};
-
-export interface RichText extends TText, TCommentText {
- backgroundColor?: React.CSSProperties['backgroundColor'];
- bold?: boolean;
- code?: boolean;
- color?: React.CSSProperties['color'];
- fontFamily?: React.CSSProperties['fontFamily'];
- fontSize?: React.CSSProperties['fontSize'];
- fontWeight?: React.CSSProperties['fontWeight'];
- italic?: boolean;
- kbd?: boolean;
- strikethrough?: boolean;
- subscript?: boolean;
- underline?: boolean;
-}
-
-/** Inline Elements */
-
-export interface MyLinkElement extends TLinkElement {
- children: RichText[];
- type: typeof LinkPlugin.key;
-}
-
-export interface MyMentionInputElement extends TMentionInputElement {
- children: [PlainText];
- type: typeof MentionInputPlugin.key;
-}
-
-export interface MyMentionElement extends TMentionElement {
- children: [EmptyText];
- type: typeof MentionPlugin.key;
-}
-
-export type MyInlineElement =
- | MyLinkElement
- | MyMentionElement
- | MyMentionInputElement;
-
-export type MyInlineDescendant = MyInlineElement | RichText;
-
-export type MyInlineChildren = MyInlineDescendant[];
-
-/** Block props */
-
-export interface MyIndentProps {
- indent?: number;
-}
-
-export interface MyIndentListProps extends MyIndentProps {
- listRestart?: number;
- listStart?: number;
- listStyleType?: string;
-}
-
-export interface MyLineHeightProps {
- lineHeight?: React.CSSProperties['lineHeight'];
-}
-
-export interface MyAlignProps {
- align?: React.CSSProperties['textAlign'];
-}
-
-export interface MyBlockElement
- extends TElement,
- MyIndentListProps,
- MyLineHeightProps {
- id?: string;
-}
-
-/** Blocks */
-
-export interface MyParagraphElement extends MyBlockElement {
- children: MyInlineChildren;
- type: typeof ParagraphPlugin.key;
-}
-
-export interface MyH1Element extends MyBlockElement {
- children: MyInlineChildren;
- type: typeof HEADING_KEYS.h1;
-}
-
-export interface MyH2Element extends MyBlockElement {
- children: MyInlineChildren;
- type: typeof HEADING_KEYS.h2;
-}
-
-export interface MyH3Element extends MyBlockElement {
- children: MyInlineChildren;
- type: typeof HEADING_KEYS.h3;
-}
-
-export interface MyBlockquoteElement extends MyBlockElement {
- children: MyInlineChildren;
- type: typeof BlockquotePlugin.key;
-}
-
-export interface MyCodeBlockElement extends MyBlockElement {
- children: MyCodeLineElement[];
- type: typeof CodeBlockPlugin.key;
-}
-
-export interface MyCodeLineElement extends TElement {
- children: PlainText[];
- type: typeof CodeLinePlugin.key;
-}
-
-export interface MyTableElement extends TTableElement, MyBlockElement {
- children: MyTableRowElement[];
- type: typeof TablePlugin.key;
-}
-
-export interface MyTableRowElement extends TElement {
- children: MyTableCellElement[];
- type: typeof TableRowPlugin.key;
-}
-
-export interface MyTableCellElement extends TElement {
- children: MyNestableBlock[];
- type: typeof TableCellPlugin.key;
-}
-
-export interface MyToggleElement extends TToggleElement, MyBlockElement {
- children: MyInlineChildren;
- type: typeof TogglePlugin.key;
-}
-
-export interface MyImageElement extends TImageElement, MyBlockElement {
- children: [EmptyText];
- type: typeof ImagePlugin.key;
-}
-
-export interface MyMediaEmbedElement
- extends TMediaEmbedElement,
- MyBlockElement {
- children: [EmptyText];
- type: typeof MediaEmbedPlugin.key;
-}
-
-export interface MyHrElement extends MyBlockElement {
- children: [EmptyText];
- type: typeof HorizontalRulePlugin.key;
-}
-
-export interface MyExcalidrawElement
- extends TExcalidrawElement,
- MyBlockElement {
- children: [EmptyText];
- type: typeof ExcalidrawPlugin.key;
-}
-
-export type MyNestableBlock = MyParagraphElement;
-
-export type MyRootBlock =
- | MyBlockquoteElement
- | MyCodeBlockElement
- | MyExcalidrawElement
- | MyH1Element
- | MyH2Element
- | MyH3Element
- | MyHrElement
- | MyImageElement
- | MyMediaEmbedElement
- | MyParagraphElement
- | MyTableElement
- | MyToggleElement;
-
-export type MyValue = MyRootBlock[];
-
-// export type MyElement = ElementOf<MyEditor>;
-
-// export type MyBlock = Exclude<MyElement, MyInlineElement>;
-
-// export type MyEditor = ReturnType<typeof useCreateEditor>;
-
-// export const useEditor = () => useEditorRef<MyEditor>();
diff --git a/apps/web/app/components/editor/plugins/ai-plugins.tsx b/apps/web/app/components/editor/plugins/ai-plugins.tsx
deleted file mode 100644
index 727e0148..00000000
--- a/apps/web/app/components/editor/plugins/ai-plugins.tsx
+++ /dev/null
@@ -1,195 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { withProps } from '@udecode/cn';
-import { AIChatPlugin, AIPlugin } from '@udecode/plate-ai/react';
-import {
- BoldPlugin,
- CodePlugin,
- ItalicPlugin,
- StrikethroughPlugin,
- UnderlinePlugin,
-} from '@udecode/plate-basic-marks/react';
-import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
-import {
- CodeBlockPlugin,
- CodeLinePlugin,
- CodeSyntaxPlugin,
-} from '@udecode/plate-code-block/react';
-import {
- ParagraphPlugin,
- PlateLeaf,
- createPlateEditor,
-} from '@udecode/plate-common/react';
-import { HEADING_KEYS } from '@udecode/plate-heading';
-import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
-import { LinkPlugin } from '@udecode/plate-link/react';
-import { MarkdownPlugin } from '@udecode/plate-markdown';
-import { BlockSelectionPlugin } from '@udecode/plate-selection/react';
-
-import { AIMenu } from '~/components/plate-ui/ai-menu';
-import { BlockquoteElement } from '~/components/plate-ui/blockquote-element';
-import { CodeBlockElement } from '~/components/plate-ui/code-block-element';
-import { CodeLeaf } from '~/components/plate-ui/code-leaf';
-import { CodeLineElement } from '~/components/plate-ui/code-line-element';
-import { CodeSyntaxLeaf } from '~/components/plate-ui/code-syntax-leaf';
-import { HeadingElement } from '~/components/plate-ui/heading-element';
-import { HrElement } from '~/components/plate-ui/hr-element';
-import { LinkElement } from '~/components/plate-ui/link-element';
-import { ParagraphElement } from '~/components/plate-ui/paragraph-element';
-
-import { basicNodesPlugins } from './basic-nodes-plugins';
-import { indentListPlugins } from './indent-list-plugins';
-import { linkPlugin } from './link-plugin';
-
-const createAIEditor = () => {
- const editor = createPlateEditor({
- id: 'ai',
- override: {
- components: {
- [BlockquotePlugin.key]: BlockquoteElement,
- [BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }),
- [CodeBlockPlugin.key]: CodeBlockElement,
- [CodeLinePlugin.key]: CodeLineElement,
- [CodePlugin.key]: CodeLeaf,
- [CodeSyntaxPlugin.key]: CodeSyntaxLeaf,
- [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }),
- [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }),
- [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }),
- [HorizontalRulePlugin.key]: HrElement,
- [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }),
- [LinkPlugin.key]: LinkElement,
- [ParagraphPlugin.key]: ParagraphElement,
- [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }),
- [UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }),
- },
- },
- plugins: [
- ParagraphPlugin,
- ...basicNodesPlugins,
- HorizontalRulePlugin,
- linkPlugin,
- ...indentListPlugins,
- MarkdownPlugin.configure({ options: { indentList: true } }),
- // FIXME
- BlockSelectionPlugin.configure({
- api: {},
- extendEditor: null,
- options: {},
- render: {},
- useHooks: null,
- handlers: {},
- }),
- ],
- value: [{ children: [{ text: '' }], type: 'p' }],
- });
-
- return editor;
-};
-
-const systemCommon = `\
-You are an advanced AI-powered note-taking assistant, designed to enhance productivity and creativity in note management.
-Respond directly to user prompts with clear, concise, and relevant content. Maintain a neutral, helpful tone.
-
-Rules:
-- <Document> is the entire note the user is working on.
-- <Reminder> is a reminder of how you should reply to INSTRUCTIONS. It does not apply to questions.
-- Anything else is the user prompt.
-- Your response should be tailored to the user's prompt, providing precise assistance to optimize note management.
-- For INSTRUCTIONS: Follow the <Reminder> exactly. Provide ONLY the content to be inserted or replaced. No explanations or comments.
-- For QUESTIONS: Provide a helpful and concise answer. You may include brief explanations if necessary.
-- CRITICAL: Distinguish between INSTRUCTIONS and QUESTIONS. Instructions typically ask you to modify or add content. Questions ask for information or clarification.
-`;
-
-const systemDefault = `\
-${systemCommon}
-- <Block> is the current block of text the user is working on.
-- Ensure your output can seamlessly fit into the existing <Block> structure.
-- CRITICAL: Provide only a single block of text. DO NOT create multiple paragraphs or separate blocks.
-<Block>
-{block}
-</Block>
-`;
-
-const systemSelecting = `\
-${systemCommon}
-- <Block> is the block of text containing the user's selection, providing context.
-- Ensure your output can seamlessly fit into the existing <Block> structure.
-- <Selection> is the specific text the user has selected in the block and wants to modify or ask about.
-- Consider the context provided by <Block>, but only modify <Selection>. Your response should be a direct replacement for <Selection>.
-<Block>
-{block}
-</Block>
-<Selection>
-{selection}
-</Selection>
-`;
-
-const systemBlockSelecting = `\
-${systemCommon}
-- <Selection> represents the full blocks of text the user has selected and wants to modify or ask about.
-- Your response should be a direct replacement for the entire <Selection>.
-- Maintain the overall structure and formatting of the selected blocks, unless explicitly instructed otherwise.
-- CRITICAL: Provide only the content to replace <Selection>. Do not add additional blocks or change the block structure unless specifically requested.
-<Selection>
-{block}
-</Selection>
-`;
-
-const userDefault = `<Reminder>
-CRITICAL: DO NOT use block formatting. You can only use inline formatting.
-CRITICAL: DO NOT start new lines or paragraphs.
-NEVER write <Block>.
-</Reminder>
-{prompt}`;
-
-const userSelecting = `<Reminder>
-If this is a question, provide a helpful and concise answer about <Selection>.
-If this is an instruction, provide ONLY the text to replace <Selection>. No explanations.
-Ensure it fits seamlessly within <Block>. If <Block> is empty, write ONE random sentence.
-NEVER write <Block> or <Selection>.
-</Reminder>
-{prompt} about <Selection>`;
-
-const userBlockSelecting = `<Reminder>
-If this is a question, provide a helpful and concise answer about <Selection>.
-If this is an instruction, provide ONLY the content to replace the entire <Selection>. No explanations.
-Maintain the overall structure unless instructed otherwise.
-NEVER write <Block> or <Selection>.
-</Reminder>
-{prompt} about <Selection>`;
-
-export const PROMPT_TEMPLATES = {
- systemBlockSelecting,
- systemDefault,
- systemSelecting,
- userBlockSelecting,
- userDefault,
- userSelecting,
-};
-
-export const aiPlugins = [
- MarkdownPlugin.configure({ options: { indentList: true } }),
- AIPlugin,
- AIChatPlugin.configure({
- options: {
- createAIEditor,
- promptTemplate: ({ isBlockSelecting, isSelecting }) => {
- return isBlockSelecting
- ? PROMPT_TEMPLATES.userBlockSelecting
- : isSelecting
- ? PROMPT_TEMPLATES.userSelecting
- : PROMPT_TEMPLATES.userDefault;
- },
- systemTemplate: ({ isBlockSelecting, isSelecting }) => {
- return isBlockSelecting
- ? PROMPT_TEMPLATES.systemBlockSelecting
- : isSelecting
- ? PROMPT_TEMPLATES.systemSelecting
- : PROMPT_TEMPLATES.systemDefault;
- },
- },
- render: { afterEditable: () => <AIMenu /> },
- }),
-] as const;
diff --git a/apps/web/app/components/editor/plugins/align-plugin.ts b/apps/web/app/components/editor/plugins/align-plugin.ts
deleted file mode 100644
index 24a7e284..00000000
--- a/apps/web/app/components/editor/plugins/align-plugin.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-'use client';
-
-import { AlignPlugin } from '@udecode/plate-alignment/react';
-import { ParagraphPlugin } from '@udecode/plate-common/react';
-import { HEADING_LEVELS } from '@udecode/plate-heading';
-import { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react';
-
-export const alignPlugin = AlignPlugin.extend({
- inject: {
- targetPlugins: [
- ParagraphPlugin.key,
- ...HEADING_LEVELS,
- MediaEmbedPlugin.key,
- ImagePlugin.key,
- ],
- },
-});
diff --git a/apps/web/app/components/editor/plugins/autoformat-plugin.ts b/apps/web/app/components/editor/plugins/autoformat-plugin.ts
deleted file mode 100644
index ca80d06a..00000000
--- a/apps/web/app/components/editor/plugins/autoformat-plugin.ts
+++ /dev/null
@@ -1,273 +0,0 @@
-'use client';
-
-import type { AutoformatRule } from '@udecode/plate-autoformat';
-import type { SlateEditor } from '@udecode/plate-common';
-
-import {
- autoformatArrow,
- autoformatLegal,
- autoformatLegalHtml,
- autoformatMath,
- autoformatPunctuation,
- autoformatSmartQuotes,
-} from '@udecode/plate-autoformat';
-import { AutoformatPlugin } from '@udecode/plate-autoformat/react';
-import {
- BoldPlugin,
- CodePlugin,
- ItalicPlugin,
- StrikethroughPlugin,
- SubscriptPlugin,
- SuperscriptPlugin,
- UnderlinePlugin,
-} from '@udecode/plate-basic-marks/react';
-import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
-import { insertEmptyCodeBlock } from '@udecode/plate-code-block';
-import {
- CodeBlockPlugin,
- CodeLinePlugin,
-} from '@udecode/plate-code-block/react';
-import {
- getParentNode,
- insertNodes,
- isElement,
- isType,
- setNodes,
-} from '@udecode/plate-common';
-import { ParagraphPlugin } from '@udecode/plate-common/react';
-import { HEADING_KEYS } from '@udecode/plate-heading';
-import { HighlightPlugin } from '@udecode/plate-highlight/react';
-import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
-import {
- INDENT_LIST_KEYS,
- ListStyleType,
- toggleIndentList,
-} from '@udecode/plate-indent-list';
-import { TogglePlugin, openNextToggles } from '@udecode/plate-toggle/react';
-
-export const format = (editor: SlateEditor, customFormatting: any) => {
- if (editor.selection) {
- const parentEntry = getParentNode(editor, editor.selection);
-
- if (!parentEntry) return;
-
- const [node] = parentEntry;
-
- if (
- isElement(node) &&
- !isType(editor, node, CodeBlockPlugin.key) &&
- !isType(editor, node, CodeLinePlugin.key)
- ) {
- customFormatting();
- }
- }
-};
-
-export const autoformatMarks: AutoformatRule[] = [
- {
- match: '***',
- mode: 'mark',
- type: [BoldPlugin.key, ItalicPlugin.key],
- },
- {
- match: '__*',
- mode: 'mark',
- type: [UnderlinePlugin.key, ItalicPlugin.key],
- },
- {
- match: '__**',
- mode: 'mark',
- type: [UnderlinePlugin.key, BoldPlugin.key],
- },
- {
- match: '___***',
- mode: 'mark',
- type: [UnderlinePlugin.key, BoldPlugin.key, ItalicPlugin.key],
- },
- {
- match: '**',
- mode: 'mark',
- type: BoldPlugin.key,
- },
- {
- match: '__',
- mode: 'mark',
- type: UnderlinePlugin.key,
- },
- {
- match: '*',
- mode: 'mark',
- type: ItalicPlugin.key,
- },
- {
- match: '_',
- mode: 'mark',
- type: ItalicPlugin.key,
- },
- {
- match: '~~',
- mode: 'mark',
- type: StrikethroughPlugin.key,
- },
- {
- match: '^',
- mode: 'mark',
- type: SuperscriptPlugin.key,
- },
- {
- match: '~',
- mode: 'mark',
- type: SubscriptPlugin.key,
- },
- {
- match: '==',
- mode: 'mark',
- type: HighlightPlugin.key,
- },
- {
- match: '≡',
- mode: 'mark',
- type: HighlightPlugin.key,
- },
- {
- match: '`',
- mode: 'mark',
- type: CodePlugin.key,
- },
-];
-
-export const autoformatBlocks: AutoformatRule[] = [
- {
- match: '# ',
- mode: 'block',
- type: HEADING_KEYS.h1,
- },
- {
- match: '## ',
- mode: 'block',
- type: HEADING_KEYS.h2,
- },
- {
- match: '### ',
- mode: 'block',
- type: HEADING_KEYS.h3,
- },
- {
- match: '#### ',
- mode: 'block',
- type: HEADING_KEYS.h4,
- },
- {
- match: '##### ',
- mode: 'block',
- type: HEADING_KEYS.h5,
- },
- {
- match: '###### ',
- mode: 'block',
- type: HEADING_KEYS.h6,
- },
- {
- match: '> ',
- mode: 'block',
- type: BlockquotePlugin.key,
- },
- {
- format: (editor) => {
- insertEmptyCodeBlock(editor, {
- defaultType: ParagraphPlugin.key,
- insertNodesOptions: { select: true },
- });
- },
- match: '```',
- mode: 'block',
- triggerAtBlockStart: false,
- type: CodeBlockPlugin.key,
- },
- {
- match: '+ ',
- mode: 'block',
- preFormat: openNextToggles,
- type: TogglePlugin.key,
- },
- {
- format: (editor) => {
- setNodes(editor, { type: HorizontalRulePlugin.key });
- insertNodes(editor, {
- children: [{ text: '' }],
- type: ParagraphPlugin.key,
- });
- },
- match: ['---', '—-', '___ '],
- mode: 'block',
- type: HorizontalRulePlugin.key,
- },
-];
-
-export const autoformatIndentLists: AutoformatRule[] = [
- {
- format: (editor) => {
- toggleIndentList(editor, {
- listStyleType: ListStyleType.Disc,
- });
- },
- match: ['* ', '- '],
- mode: 'block',
- type: 'list',
- },
- {
- format: (editor) =>
- toggleIndentList(editor, {
- listStyleType: ListStyleType.Decimal,
- }),
- match: [String.raw`^\d+\.$ `, String.raw`^\d+\)$ `],
- matchByRegex: true,
- mode: 'block',
- type: 'list',
- },
- {
- format: (editor) => {
- toggleIndentList(editor, {
- listStyleType: INDENT_LIST_KEYS.todo,
- });
- setNodes(editor, {
- checked: false,
- listStyleType: INDENT_LIST_KEYS.todo,
- });
- },
- match: ['[] '],
- mode: 'block',
- type: 'list',
- },
- {
- format: (editor) => {
- toggleIndentList(editor, {
- listStyleType: INDENT_LIST_KEYS.todo,
- });
- setNodes(editor, {
- checked: true,
- listStyleType: INDENT_LIST_KEYS.todo,
- });
- },
- match: ['[x] '],
- mode: 'block',
- type: 'list',
- },
-];
-
-export const autoformatPlugin = AutoformatPlugin.configure({
- options: {
- enableUndoOnDelete: true,
- rules: [
- ...autoformatBlocks,
- ...autoformatMarks,
- ...autoformatSmartQuotes,
- ...autoformatPunctuation,
- ...autoformatLegal,
- ...autoformatLegalHtml,
- ...autoformatArrow,
- ...autoformatMath,
- ...autoformatIndentLists,
- ],
- },
-});
diff --git a/apps/web/app/components/editor/plugins/basic-nodes-plugins.tsx b/apps/web/app/components/editor/plugins/basic-nodes-plugins.tsx
deleted file mode 100644
index e336e24f..00000000
--- a/apps/web/app/components/editor/plugins/basic-nodes-plugins.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-'use client';
-
-import { BasicMarksPlugin } from '@udecode/plate-basic-marks/react';
-import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
-import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
-import { HeadingPlugin } from '@udecode/plate-heading/react';
-import Prism from 'prismjs';
-
-export const basicNodesPlugins = [
- HeadingPlugin.configure({ options: { levels: 3 } }),
- BlockquotePlugin,
- CodeBlockPlugin.configure({ options: { prism: Prism } }),
- BasicMarksPlugin,
-] as const;
diff --git a/apps/web/app/components/editor/plugins/block-menu-plugins.ts b/apps/web/app/components/editor/plugins/block-menu-plugins.ts
deleted file mode 100644
index 7bf0320b..00000000
--- a/apps/web/app/components/editor/plugins/block-menu-plugins.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-'use client';
-
-import { BlockMenuPlugin } from '@udecode/plate-selection/react';
-
-import { BlockContextMenu } from '~/components/plate-ui/block-context-menu';
-
-import { blockSelectionPlugins } from './block-selection-plugins';
-
-export const blockMenuPlugins = [
- ...blockSelectionPlugins,
- BlockMenuPlugin.configure({
- render: { aboveEditable: BlockContextMenu },
- }),
-] as const;
diff --git a/apps/web/app/components/editor/plugins/block-selection-plugins.ts b/apps/web/app/components/editor/plugins/block-selection-plugins.ts
deleted file mode 100644
index ab35e38f..00000000
--- a/apps/web/app/components/editor/plugins/block-selection-plugins.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-'use client';
-
-import { BlockSelectionPlugin } from '@udecode/plate-selection/react';
-
-export const blockSelectionPlugins = [
- BlockSelectionPlugin.configure({
- inject: {
- excludeBelowPlugins: ['tr'],
- excludePlugins: ['table', 'code_line', 'column_group', 'column'],
- },
- options: {
- areaOptions: {
- behaviour: {
- scrolling: {
- speedDivider: 1.5,
- },
- startThreshold: 4,
- },
- },
- enableContextMenu: true,
- },
- }),
-] as const;
diff --git a/apps/web/app/components/editor/plugins/comments-plugin.tsx b/apps/web/app/components/editor/plugins/comments-plugin.tsx
deleted file mode 100644
index 6292eed9..00000000
--- a/apps/web/app/components/editor/plugins/comments-plugin.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-'use client';
-
-import { CommentsPlugin } from '@udecode/plate-comments/react';
-
-import { CommentsPopover } from '~/components/plate-ui/comments-popover';
-
-export const commentsPlugin = CommentsPlugin.configure({
- options: {
- myUserId: '1',
- users: {
- 1: {
- id: '1',
- avatarUrl: 'https://avatars.githubusercontent.com/u/19695832?s=96&v=4',
- name: 'zbeyens',
- },
- 2: {
- id: '2',
- avatarUrl: 'https://avatars.githubusercontent.com/u/4272090?v=4',
- name: '12joan',
- },
- },
- },
- render: { afterEditable: () => <CommentsPopover /> },
-});
diff --git a/apps/web/app/components/editor/plugins/copilot-plugins.ts b/apps/web/app/components/editor/plugins/copilot-plugins.ts
deleted file mode 100644
index 6fa41dcc..00000000
--- a/apps/web/app/components/editor/plugins/copilot-plugins.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-'use client';
-
-import type { TElement } from '@udecode/plate-common';
-
-import { faker } from '@faker-js/faker';
-import { CopilotPlugin } from '@udecode/plate-ai/react';
-import { getAncestorNode } from '@udecode/plate-common';
-import { serializeMdNodes, stripMarkdown } from '@udecode/plate-markdown';
-
-import { GhostText } from '~/components/plate-ui/ghost-text';
-
-export const copilotPlugins = [
- CopilotPlugin.configure(({ api }) => ({
- options: {
- completeOptions: {
- api: '/api/ai/copilot',
- body: {
- system: `You are an advanced AI writing assistant, similar to VSCode Copilot but for general text. Your task is to predict and generate the next part of the text based on the given context.
-
- Rules:
- - Continue the text naturally up to the next punctuation mark (., ,, ;, :, ?, or !).
- - Maintain style and tone. Don't repeat given text.
- - For unclear context, provide the most likely continuation.
- - Handle code snippets, lists, or structured text if needed.
- - Don't include """ in your response.
- - CRITICAL: Always end with a punctuation mark.
- - CRITICAL: Avoid starting a new block. Do not use block formatting like >, #, 1., 2., -, etc. The suggestion should continue in the same block as the context.
- - If no context is provided or you can't generate a continuation, return "0" without explanation.`,
- },
- onError: () => {
- // Mock the API response. Remove it when you implement the route /api/ai/copilot
- api.copilot.setBlockSuggestion({
- text: stripMarkdown(faker.lorem.sentence()),
- });
- },
- onFinish: (_, completion) => {
- if (completion === '0') return;
-
- api.copilot.setBlockSuggestion({
- text: stripMarkdown(completion),
- });
- },
- },
- debounceDelay: 500,
- getPrompt: ({ editor }) => {
- const contextEntry = getAncestorNode(editor);
-
- if (!contextEntry) return '';
-
- const prompt = serializeMdNodes([contextEntry[0] as TElement]);
-
- return `Continue the text up to the next punctuation mark:
- """
- ${prompt}
- """`;
- },
- renderGhostText: GhostText,
- },
- })),
-] as const;
diff --git a/apps/web/app/components/editor/plugins/delete-plugins.ts b/apps/web/app/components/editor/plugins/delete-plugins.ts
deleted file mode 100644
index 947137bc..00000000
--- a/apps/web/app/components/editor/plugins/delete-plugins.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-'use client';
-
-import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
-import { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react';
-import { DeletePlugin, SelectOnBackspacePlugin } from '@udecode/plate-select';
-
-export const deletePlugins = [
- SelectOnBackspacePlugin.configure({
- options: {
- query: {
- allow: [
- ImagePlugin.key,
- MediaEmbedPlugin.key,
- HorizontalRulePlugin.key,
- ],
- },
- },
- }),
- DeletePlugin,
-] as const;
diff --git a/apps/web/app/components/editor/plugins/dnd-plugins.tsx b/apps/web/app/components/editor/plugins/dnd-plugins.tsx
deleted file mode 100644
index 6b0db41b..00000000
--- a/apps/web/app/components/editor/plugins/dnd-plugins.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-'use client';
-
-import { DndPlugin } from '@udecode/plate-dnd';
-import { ImagePlugin } from '@udecode/plate-media/react';
-import { NodeIdPlugin } from '@udecode/plate-node-id';
-
-export const dndPlugins = [
- NodeIdPlugin,
- DndPlugin.configure({
- options: {
- enableScroller: true,
- onDropFiles: ({ dragItem, editor, target }) => {
- editor
- .getTransforms(ImagePlugin)
- .insert.imageFromFiles(dragItem.files, {
- at: target,
- nextBlock: false,
- });
- },
- },
- }),
-] as const;
diff --git a/apps/web/app/components/editor/plugins/editor-plugins.tsx b/apps/web/app/components/editor/plugins/editor-plugins.tsx
deleted file mode 100644
index fa24ba7a..00000000
--- a/apps/web/app/components/editor/plugins/editor-plugins.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-'use client';
-
-import { CalloutPlugin } from '@udecode/plate-callout/react';
-import { ParagraphPlugin } from '@udecode/plate-common/react';
-import { DatePlugin } from '@udecode/plate-date/react';
-import { DocxPlugin } from '@udecode/plate-docx';
-import {
- FontBackgroundColorPlugin,
- FontColorPlugin,
- FontSizePlugin,
-} from '@udecode/plate-font/react';
-import { HighlightPlugin } from '@udecode/plate-highlight/react';
-import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
-import { JuicePlugin } from '@udecode/plate-juice';
-import { KbdPlugin } from '@udecode/plate-kbd/react';
-import { ColumnPlugin } from '@udecode/plate-layout/react';
-import { MarkdownPlugin } from '@udecode/plate-markdown';
-import {
- EquationPlugin,
- InlineEquationPlugin,
-} from '@udecode/plate-math/react';
-import { CursorOverlayPlugin } from '@udecode/plate-selection/react';
-import { SlashPlugin } from '@udecode/plate-slash-command/react';
-import { TogglePlugin } from '@udecode/plate-toggle/react';
-import { TrailingBlockPlugin } from '@udecode/plate-trailing-block';
-
-import { CursorOverlay } from '~/components/plate-ui/cursor-overlay';
-
-import { aiPlugins } from './ai-plugins';
-import { alignPlugin } from './align-plugin';
-import { autoformatPlugin } from './autoformat-plugin';
-import { basicNodesPlugins } from './basic-nodes-plugins';
-import { blockMenuPlugins } from './block-menu-plugins';
-import { commentsPlugin } from './comments-plugin';
-import { deletePlugins } from './delete-plugins';
-import { dndPlugins } from './dnd-plugins';
-import { exitBreakPlugin } from './exit-break-plugin';
-import { indentListPlugins } from './indent-list-plugins';
-import { lineHeightPlugin } from './line-height-plugin';
-import { linkPlugin } from './link-plugin';
-import { mediaPlugins } from './media-plugins';
-import { mentionPlugin } from './mention-plugin';
-import { resetBlockTypePlugin } from './reset-block-type-plugin';
-import { softBreakPlugin } from './soft-break-plugin';
-import { tablePlugin } from './table-plugin';
-import { tocPlugin } from './toc-plugin';
-
-export const editorPlugins = [
- // AI
- ...aiPlugins,
-
- // Nodes
- ...basicNodesPlugins,
- HorizontalRulePlugin,
- linkPlugin,
- DatePlugin,
- mentionPlugin,
- SlashPlugin,
- tablePlugin,
- TogglePlugin,
- tocPlugin,
- ...mediaPlugins,
- InlineEquationPlugin,
- EquationPlugin,
- CalloutPlugin,
- ColumnPlugin,
-
- // Marks
- FontColorPlugin,
- FontBackgroundColorPlugin,
- FontSizePlugin,
- HighlightPlugin,
- KbdPlugin,
-
- // Block Style
- alignPlugin,
- ...indentListPlugins,
- lineHeightPlugin,
-
- // Functionality
- autoformatPlugin,
- CursorOverlayPlugin.configure({
- render: { afterEditable: () => <CursorOverlay /> },
- }),
- ...blockMenuPlugins,
- ...dndPlugins,
- exitBreakPlugin,
- resetBlockTypePlugin,
- ...deletePlugins,
- softBreakPlugin,
- TrailingBlockPlugin.configure({ options: { type: ParagraphPlugin.key } }),
-
- // Collaboration
- commentsPlugin,
-
- // Deserialization
- DocxPlugin,
- MarkdownPlugin.configure({ options: { indentList: true } }),
- JuicePlugin,
-];
diff --git a/apps/web/app/components/editor/plugins/exit-break-plugin.ts b/apps/web/app/components/editor/plugins/exit-break-plugin.ts
deleted file mode 100644
index 038a92b3..00000000
--- a/apps/web/app/components/editor/plugins/exit-break-plugin.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-'use client';
-
-import { ExitBreakPlugin } from '@udecode/plate-break/react';
-import { HEADING_LEVELS } from '@udecode/plate-heading';
-
-export const exitBreakPlugin = ExitBreakPlugin.configure({
- options: {
- rules: [
- {
- hotkey: 'mod+enter',
- },
- {
- before: true,
- hotkey: 'mod+shift+enter',
- },
- {
- hotkey: 'enter',
- level: 1,
- query: {
- allow: HEADING_LEVELS,
- end: true,
- start: true,
- },
- relative: true,
- },
- ],
- },
-});
diff --git a/apps/web/app/components/editor/plugins/fixed-toolbar-plugin.tsx b/apps/web/app/components/editor/plugins/fixed-toolbar-plugin.tsx
deleted file mode 100644
index b77d6c75..00000000
--- a/apps/web/app/components/editor/plugins/fixed-toolbar-plugin.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-'use client';
-
-import { createPlatePlugin } from '@udecode/plate-common/react';
-
-import { FixedToolbar } from '~/components/plate-ui/fixed-toolbar';
-import { FixedToolbarButtons } from '~/components/plate-ui/fixed-toolbar-buttons';
-
-export const FixedToolbarPlugin = createPlatePlugin({
- key: 'fixed-toolbar',
- render: {
- beforeEditable: () => (
- <FixedToolbar>
- <FixedToolbarButtons />
- </FixedToolbar>
- ),
- },
-});
diff --git a/apps/web/app/components/editor/plugins/floating-toolbar-plugin.tsx b/apps/web/app/components/editor/plugins/floating-toolbar-plugin.tsx
deleted file mode 100644
index aa200eaa..00000000
--- a/apps/web/app/components/editor/plugins/floating-toolbar-plugin.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-'use client';
-
-import { createPlatePlugin } from '@udecode/plate-common/react';
-
-import { FloatingToolbar } from '~/components/plate-ui/floating-toolbar';
-import { FloatingToolbarButtons } from '~/components/plate-ui/floating-toolbar-buttons';
-
-export const FloatingToolbarPlugin = createPlatePlugin({
- key: 'floating-toolbar',
- render: {
- afterEditable: () => (
- <FloatingToolbar>
- <FloatingToolbarButtons />
- </FloatingToolbar>
- ),
- },
-});
diff --git a/apps/web/app/components/editor/plugins/indent-list-plugins.ts b/apps/web/app/components/editor/plugins/indent-list-plugins.ts
deleted file mode 100644
index b4b5ebbc..00000000
--- a/apps/web/app/components/editor/plugins/indent-list-plugins.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-'use client';
-
-import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
-import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
-import { ParagraphPlugin } from '@udecode/plate-common/react';
-import { HEADING_LEVELS } from '@udecode/plate-heading';
-import { IndentPlugin } from '@udecode/plate-indent/react';
-import { IndentListPlugin } from '@udecode/plate-indent-list/react';
-import { TogglePlugin } from '@udecode/plate-toggle/react';
-
-import {
- FireLiComponent,
- FireMarker,
-} from '~/components/plate-ui/indent-fire-marker';
-import {
- TodoLi,
- TodoMarker,
-} from '~/components/plate-ui/indent-todo-marker';
-
-export const indentListPlugins = [
- IndentPlugin.extend({
- inject: {
- targetPlugins: [
- ParagraphPlugin.key,
- ...HEADING_LEVELS,
- BlockquotePlugin.key,
- CodeBlockPlugin.key,
- TogglePlugin.key,
- ],
- },
- }),
- IndentListPlugin.extend({
- inject: {
- targetPlugins: [
- ParagraphPlugin.key,
- ...HEADING_LEVELS,
- BlockquotePlugin.key,
- CodeBlockPlugin.key,
- TogglePlugin.key,
- ],
- },
- options: {
- listStyleTypes: {
- fire: {
- liComponent: FireLiComponent,
- markerComponent: FireMarker,
- type: 'fire',
- },
- todo: {
- liComponent: TodoLi,
- markerComponent: TodoMarker,
- type: 'todo',
- },
- },
- },
- }),
-];
diff --git a/apps/web/app/components/editor/plugins/line-height-plugin.ts b/apps/web/app/components/editor/plugins/line-height-plugin.ts
deleted file mode 100644
index 3639cabe..00000000
--- a/apps/web/app/components/editor/plugins/line-height-plugin.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-'use client';
-
-import { ParagraphPlugin } from '@udecode/plate-common/react';
-import { HEADING_LEVELS } from '@udecode/plate-heading';
-import { LineHeightPlugin } from '@udecode/plate-line-height/react';
-
-export const lineHeightPlugin = LineHeightPlugin.configure({
- inject: {
- nodeProps: {
- defaultNodeValue: 1.5,
- validNodeValues: [1, 1.2, 1.5, 2, 3],
- },
- targetPlugins: [ParagraphPlugin.key, ...HEADING_LEVELS],
- },
-});
diff --git a/apps/web/app/components/editor/plugins/link-plugin.tsx b/apps/web/app/components/editor/plugins/link-plugin.tsx
deleted file mode 100644
index 435b6e10..00000000
--- a/apps/web/app/components/editor/plugins/link-plugin.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-'use client';
-
-import { LinkPlugin } from '@udecode/plate-link/react';
-
-import { LinkFloatingToolbar } from '~/components/plate-ui/link-floating-toolbar';
-
-export const linkPlugin = LinkPlugin.extend({
- render: { afterEditable: () => <LinkFloatingToolbar /> },
-});
diff --git a/apps/web/app/components/editor/plugins/media-plugins.tsx b/apps/web/app/components/editor/plugins/media-plugins.tsx
deleted file mode 100644
index 03c1930e..00000000
--- a/apps/web/app/components/editor/plugins/media-plugins.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-'use client';
-
-import { CaptionPlugin } from '@udecode/plate-caption/react';
-import {
- AudioPlugin,
- FilePlugin,
- ImagePlugin,
- MediaEmbedPlugin,
- PlaceholderPlugin,
- VideoPlugin,
-} from '@udecode/plate-media/react';
-
-import { ImagePreview } from '~/components/plate-ui/image-preview';
-
-export const mediaPlugins = [
- PlaceholderPlugin,
- ImagePlugin.extend({
- options: {
- disableUploadInsert: true,
- },
- render: { afterEditable: ImagePreview },
- }),
- MediaEmbedPlugin,
- VideoPlugin,
- AudioPlugin,
- FilePlugin,
- CaptionPlugin.configure({
- options: { plugins: [ImagePlugin, MediaEmbedPlugin] },
- }),
-] as const;
diff --git a/apps/web/app/components/editor/plugins/mention-plugin.ts b/apps/web/app/components/editor/plugins/mention-plugin.ts
deleted file mode 100644
index 0b7421fd..00000000
--- a/apps/web/app/components/editor/plugins/mention-plugin.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-'use client';
-
-import { MentionPlugin } from '@udecode/plate-mention/react';
-
-export const mentionPlugin = MentionPlugin.configure({
- options: { triggerPreviousCharPattern: /^$|^[\s"']$/ },
-});
diff --git a/apps/web/app/components/editor/plugins/reset-block-type-plugin.ts b/apps/web/app/components/editor/plugins/reset-block-type-plugin.ts
deleted file mode 100644
index 61973e92..00000000
--- a/apps/web/app/components/editor/plugins/reset-block-type-plugin.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-'use client';
-
-import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
-import { CalloutPlugin } from '@udecode/plate-callout/react';
-import {
- isCodeBlockEmpty,
- isSelectionAtCodeBlockStart,
- unwrapCodeBlock,
-} from '@udecode/plate-code-block';
-import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
-import {
- isBlockAboveEmpty,
- isSelectionAtBlockStart,
-} from '@udecode/plate-common';
-import { ParagraphPlugin } from '@udecode/plate-common/react';
-import { HEADING_LEVELS } from '@udecode/plate-heading';
-import { INDENT_LIST_KEYS, ListStyleType } from '@udecode/plate-indent-list';
-import { ResetNodePlugin } from '@udecode/plate-reset-node/react';
-
-const resetBlockTypesCommonRule = {
- defaultType: ParagraphPlugin.key,
- types: [
- ...HEADING_LEVELS,
- BlockquotePlugin.key,
- INDENT_LIST_KEYS.todo,
- ListStyleType.Disc,
- ListStyleType.Decimal,
- CalloutPlugin.key,
- ],
-};
-
-const resetBlockTypesCodeBlockRule = {
- defaultType: ParagraphPlugin.key,
- types: [CodeBlockPlugin.key],
- onReset: unwrapCodeBlock,
-};
-
-export const resetBlockTypePlugin = ResetNodePlugin.configure({
- options: {
- rules: [
- {
- ...resetBlockTypesCommonRule,
- hotkey: 'Enter',
- predicate: isBlockAboveEmpty,
- },
- {
- ...resetBlockTypesCommonRule,
- hotkey: 'Backspace',
- predicate: isSelectionAtBlockStart,
- },
- {
- ...resetBlockTypesCodeBlockRule,
- hotkey: 'Enter',
- predicate: isCodeBlockEmpty,
- },
- {
- ...resetBlockTypesCodeBlockRule,
- hotkey: 'Backspace',
- predicate: isSelectionAtCodeBlockStart,
- },
- ],
- },
-});
diff --git a/apps/web/app/components/editor/plugins/soft-break-plugin.ts b/apps/web/app/components/editor/plugins/soft-break-plugin.ts
deleted file mode 100644
index a5ed0d45..00000000
--- a/apps/web/app/components/editor/plugins/soft-break-plugin.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-'use client';
-
-import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
-import { SoftBreakPlugin } from '@udecode/plate-break/react';
-import { CalloutPlugin } from '@udecode/plate-callout/react';
-import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
-import {
- TableCellHeaderPlugin,
- TableCellPlugin,
-} from '@udecode/plate-table/react';
-
-export const softBreakPlugin = SoftBreakPlugin.configure({
- options: {
- rules: [
- { hotkey: 'shift+enter' },
- {
- hotkey: 'enter',
- query: {
- allow: [
- CodeBlockPlugin.key,
- BlockquotePlugin.key,
- TableCellPlugin.key,
- TableCellHeaderPlugin.key,
- CalloutPlugin.key,
- ],
- },
- },
- ],
- },
-});
diff --git a/apps/web/app/components/editor/plugins/table-plugin.ts b/apps/web/app/components/editor/plugins/table-plugin.ts
deleted file mode 100644
index 82f65ffa..00000000
--- a/apps/web/app/components/editor/plugins/table-plugin.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-'use client';
-
-import { TablePlugin } from '@udecode/plate-table/react';
-
-export const tablePlugin = TablePlugin.configure({
- options: {
- enableMerging: true,
- },
-});
diff --git a/apps/web/app/components/editor/plugins/toc-plugin.ts b/apps/web/app/components/editor/plugins/toc-plugin.ts
deleted file mode 100644
index 0fd7d1f0..00000000
--- a/apps/web/app/components/editor/plugins/toc-plugin.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-'use client';
-
-import { TocPlugin } from '@udecode/plate-heading/react';
-
-export const tocPlugin = TocPlugin.configure({
- options: {
- // isScroll: true,
- topOffset: 80,
- },
-});
diff --git a/apps/web/app/components/editor/transforms.ts b/apps/web/app/components/editor/transforms.ts
deleted file mode 100644
index 7bd776c3..00000000
--- a/apps/web/app/components/editor/transforms.ts
+++ /dev/null
@@ -1,206 +0,0 @@
-'use client';
-
-import type { PlateEditor } from '@udecode/plate-common/react';
-
-import { insertCallout } from '@udecode/plate-callout';
-import { CalloutPlugin } from '@udecode/plate-callout/react';
-import { insertCodeBlock } from '@udecode/plate-code-block';
-import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
-import {
- type TElement,
- type TNodeEntry,
- getBlockAbove,
- getBlocks,
- getNodeEntry,
- insertNodes,
- removeEmptyPreviousBlock,
- setNodes,
- unsetNodes,
- withoutNormalizing,
-} from '@udecode/plate-common';
-import { insertDate } from '@udecode/plate-date';
-import { DatePlugin } from '@udecode/plate-date/react';
-import { insertToc } from '@udecode/plate-heading';
-import { TocPlugin } from '@udecode/plate-heading/react';
-import { INDENT_LIST_KEYS, ListStyleType } from '@udecode/plate-indent-list';
-import { IndentListPlugin } from '@udecode/plate-indent-list/react';
-import { insertColumnGroup, toggleColumnGroup } from '@udecode/plate-layout';
-import { LinkPlugin, triggerFloatingLink } from '@udecode/plate-link/react';
-import { insertEquation, insertInlineEquation } from '@udecode/plate-math';
-import {
- EquationPlugin,
- InlineEquationPlugin,
-} from '@udecode/plate-math/react';
-import {
- insertAudioPlaceholder,
- insertFilePlaceholder,
- insertMedia,
- insertVideoPlaceholder,
-} from '@udecode/plate-media';
-import {
- AudioPlugin,
- FilePlugin,
- ImagePlugin,
- MediaEmbedPlugin,
- VideoPlugin,
-} from '@udecode/plate-media/react';
-import { TablePlugin, insertTable } from '@udecode/plate-table/react';
-import { Path } from 'slate';
-
-const ACTION_THREE_COLUMNS = 'action_three_columns';
-
-const insertList = (editor: PlateEditor, type: string) => {
- insertNodes(
- editor,
- editor.api.create.block({
- indent: 1,
- listStyleType: type,
- }),
- { select: true }
- );
-};
-
-const insertBlockMap: Record<
- string,
- (editor: PlateEditor, type: string) => void
-> = {
- [ACTION_THREE_COLUMNS]: (editor) =>
- insertColumnGroup(editor, { layout: 3, select: true }),
- [AudioPlugin.key]: (editor) =>
- insertAudioPlaceholder(editor, { select: true }),
- [CalloutPlugin.key]: (editor) => insertCallout(editor, { select: true }),
- [CodeBlockPlugin.key]: (editor) => insertCodeBlock(editor, { select: true }),
- [EquationPlugin.key]: (editor) => insertEquation(editor, { select: true }),
- [FilePlugin.key]: (editor) => insertFilePlaceholder(editor, { select: true }),
- [INDENT_LIST_KEYS.todo]: insertList,
- [ImagePlugin.key]: (editor) =>
- insertMedia(editor, {
- select: true,
- type: ImagePlugin.key,
- }),
- [ListStyleType.Decimal]: insertList,
- [ListStyleType.Disc]: insertList,
- [MediaEmbedPlugin.key]: (editor) =>
- insertMedia(editor, {
- select: true,
- type: MediaEmbedPlugin.key,
- }),
- [TablePlugin.key]: (editor) => insertTable(editor, {}, { select: true }),
- [TocPlugin.key]: (editor) => insertToc(editor, { select: true }),
- [VideoPlugin.key]: (editor) =>
- insertVideoPlaceholder(editor, { select: true }),
-};
-
-const insertInlineMap: Record<
- string,
- (editor: PlateEditor, type: string) => void
-> = {
- [DatePlugin.key]: (editor) => insertDate(editor, { select: true }),
- [InlineEquationPlugin.key]: (editor) =>
- insertInlineEquation(editor, '', { select: true }),
- [LinkPlugin.key]: (editor) => triggerFloatingLink(editor, { focused: true }),
-};
-
-export const insertBlock = (editor: PlateEditor, type: string) => {
- withoutNormalizing(editor, () => {
- if (type in insertBlockMap) {
- insertBlockMap[type](editor, type);
- } else {
- const path = getBlockAbove(editor)?.[1];
-
- if (!path) return;
-
- const at = Path.next(path);
-
- insertNodes(editor, editor.api.create.block({ type }), {
- at,
- select: true,
- });
- }
-
- removeEmptyPreviousBlock(editor);
- });
-};
-
-export const insertInlineElement = (editor: PlateEditor, type: string) => {
- if (insertInlineMap[type]) {
- insertInlineMap[type](editor, type);
- }
-};
-
-const setList = (
- editor: PlateEditor,
- type: string,
- entry: TNodeEntry<TElement>
-) => {
- setNodes(
- editor,
- editor.api.create.block({
- indent: 1,
- listStyleType: type,
- }),
- {
- at: entry[1],
- }
- );
-};
-
-const setBlockMap: Record<
- string,
- (editor: PlateEditor, type: string, entry: TNodeEntry<TElement>) => void
-> = {
- [ACTION_THREE_COLUMNS]: (editor) => toggleColumnGroup(editor, { layout: 3 }),
- [INDENT_LIST_KEYS.todo]: setList,
- [ListStyleType.Decimal]: setList,
- [ListStyleType.Disc]: setList,
-};
-
-export const setBlockType = (
- editor: PlateEditor,
- type: string,
- { at }: { at?: Path } = {}
-) => {
- withoutNormalizing(editor, () => {
- const setEntry = (entry: TNodeEntry<TElement>) => {
- const [node, path] = entry;
-
- if (node[IndentListPlugin.key]) {
- unsetNodes(editor, [IndentListPlugin.key, 'indent'], { at: path });
- }
- if (type in setBlockMap) {
- return setBlockMap[type](editor, type, entry);
- }
- if (node.type !== type) {
- editor.setNodes<TElement>({ type }, { at: path });
- }
- };
-
- if (at) {
- const entry = getNodeEntry<TElement>(editor, at);
-
- if (entry) {
- setEntry(entry);
-
- return;
- }
- }
-
- const entries = getBlocks(editor);
-
- entries.forEach((entry) => setEntry(entry));
- });
-};
-
-export const getBlockType = (block: TElement) => {
- if (block[IndentListPlugin.key]) {
- if (block[IndentListPlugin.key] === ListStyleType.Decimal) {
- return ListStyleType.Decimal;
- } else if (block[IndentListPlugin.key] === INDENT_LIST_KEYS.todo) {
- return INDENT_LIST_KEYS.todo;
- } else {
- return ListStyleType.Disc;
- }
- }
-
- return block.type;
-};
diff --git a/apps/web/app/components/editor/use-chat.tsx b/apps/web/app/components/editor/use-chat.tsx
deleted file mode 100644
index 11b6ee6e..00000000
--- a/apps/web/app/components/editor/use-chat.tsx
+++ /dev/null
@@ -1,270 +0,0 @@
-"use client";
-
-import { type ReactNode, createContext, useContext, useState } from "react";
-
-import { faker } from "@faker-js/faker";
-import { cn } from "@udecode/cn";
-import { CopilotPlugin } from "@udecode/plate-ai/react";
-import { useEditorPlugin } from "@udecode/plate-common/react";
-import { useChat as useBaseChat } from "ai/react";
-import { ArrowUpRight, Check, ChevronsUpDown, Eye, EyeOff, Settings } from "lucide-react";
-import { Button } from "~/components/plate-ui/button";
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
-} from "~/components/plate-ui/command";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "~/components/plate-ui/dialog";
-import { Input } from "~/components/plate-ui/input";
-import { Popover, PopoverContent, PopoverTrigger } from "~/components/plate-ui/popover";
-
-export const useChat = (props?: Parameters<typeof useBaseChat>[0]) => {
- return useBaseChat({
- id: "editor",
- api: "/api/ai/command",
- ...props,
- });
-};
-
-// Used for testing. Remove it after implementing useChat api.
-const fakeStreamText = ({
- chunkCount = 10,
- streamProtocol = "data",
-}: {
- chunkCount?: number;
- streamProtocol?: "data" | "text";
-} = {}) => {
- const chunks = Array.from({ length: chunkCount }, () => ({
- delay: faker.number.int({ max: 150, min: 50 }),
- texts: faker.lorem.words({ max: 3, min: 1 }) + " ",
- }));
- const encoder = new TextEncoder();
-
- return new ReadableStream({
- async start(controller) {
- for (const chunk of chunks) {
- await new Promise((resolve) => setTimeout(resolve, chunk.delay));
-
- if (streamProtocol === "text") {
- controller.enqueue(encoder.encode(chunk.texts));
- } else {
- controller.enqueue(encoder.encode(`0:${JSON.stringify(chunk.texts)}\n`));
- }
- }
-
- if (streamProtocol === "data") {
- controller.enqueue(
- `d:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":${chunks.length}}}\n`,
- );
- }
-
- controller.close();
- },
- });
-};
-
-interface Model {
- label: string;
- value: string;
-}
-
-interface OpenAIContextType {
- apiKey: string;
- model: Model;
- setApiKey: (key: string) => void;
- setModel: (model: Model) => void;
-}
-
-export const models: Model[] = [
- { label: "gpt-4o-mini", value: "gpt-4o-mini" },
- { label: "gpt-4o", value: "gpt-4o" },
- { label: "gpt-4-turbo", value: "gpt-4-turbo" },
- { label: "gpt-4", value: "gpt-4" },
- { label: "gpt-3.5-turbo", value: "gpt-3.5-turbo" },
- { label: "gpt-3.5-turbo-instruct", value: "gpt-3.5-turbo-instruct" },
-];
-
-const OpenAIContext = createContext<OpenAIContextType | undefined>(undefined);
-
-export function OpenAIProvider({ children }: { children: ReactNode }) {
- const [apiKey, setApiKey] = useState("");
- const [model, setModel] = useState<Model>(models[0]);
-
- return (
- <OpenAIContext.Provider value={{ apiKey, model, setApiKey, setModel }}>
- {children}
- </OpenAIContext.Provider>
- );
-}
-
-export function useOpenAI() {
- const context = useContext(OpenAIContext);
-
- return (
- context ??
- ({
- apiKey: "",
- model: models[0],
- setApiKey: () => {},
- setModel: () => {},
- } as OpenAIContextType)
- );
-}
-
-export function SettingsDialog() {
- const { apiKey, model, setApiKey, setModel } = useOpenAI();
- const [tempKey, setTempKey] = useState(apiKey);
- const [showKey, setShowKey] = useState(false);
- const [open, setOpen] = useState(false);
- const [openModel, setOpenModel] = useState(false);
-
- const { getOptions, setOption } = useEditorPlugin(CopilotPlugin);
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- setApiKey(tempKey);
- setOpen(false);
-
- const completeOptions = getOptions().completeOptions ?? {};
-
- setOption("completeOptions", {
- ...completeOptions,
- body: {
- ...completeOptions.body,
- apiKey: tempKey,
- model: model.value,
- },
- });
- };
-
- return (
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogTrigger asChild>
- <Button
- size="icon"
- variant="default"
- className={cn(
- "group fixed bottom-4 right-4 z-50 size-10 overflow-hidden",
- "rounded-full shadow-md hover:shadow-lg",
- "transition-all duration-300 ease-in-out hover:w-[106px]",
- )}
- data-block-hide
- >
- <div className="flex size-full items-center justify-start gap-2">
- <Settings className="ml-1.5 size-4" />
- <span
- className={cn(
- "whitespace-nowrap opacity-0 transition-all duration-300 ease-in-out",
- "group-hover:translate-x-0 group-hover:opacity-100",
- "-translate-x-2",
- )}
- >
- Settings
- </span>
- </div>
- </Button>
- </DialogTrigger>
- <DialogContent>
- <DialogHeader className="space-y-4">
- <DialogTitle>AI Settings</DialogTitle>
- <DialogDescription>
- Enter your{" "}
- <a
- className="inline-flex items-center font-medium text-primary hover:underline"
- href="https://platform.openai.com/api-keys"
- rel="noreferrer"
- target="_blank"
- >
- OpenAI API key
- <ArrowUpRight className="size-[14px]" />
- </a>{" "}
- to use AI features.
- </DialogDescription>
- </DialogHeader>
- <form className="space-y-4" onSubmit={handleSubmit}>
- <div className="relative">
- <Input
- className="pr-10"
- value={tempKey}
- onChange={(e) => setTempKey(e.target.value)}
- placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- data-1p-ignore
- type={showKey ? "text" : "password"}
- />
- <Button
- size="icon"
- variant="ghost"
- className="absolute right-0 top-0 h-full"
- onClick={() => setShowKey(!showKey)}
- type="button"
- >
- {showKey ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
- <span className="sr-only">{showKey ? "Hide" : "Show"} API key</span>
- </Button>
- </div>
-
- <Popover open={openModel} onOpenChange={setOpenModel}>
- <PopoverTrigger asChild>
- <Button
- size="lg"
- variant="outline"
- className="w-full justify-between"
- aria-expanded={openModel}
- role="combobox"
- >
- <code>{model.label}</code>
- <ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-full p-0">
- <Command>
- <CommandInput placeholder="Search model..." />
- <CommandEmpty>No model found.</CommandEmpty>
-
- <CommandList>
- <CommandGroup>
- {models.map((m) => (
- <CommandItem
- key={m.value}
- value={m.value}
- onSelect={() => {
- setModel(m);
- setOpenModel(false);
- }}
- >
- <Check
- className={cn(
- "mr-2 size-4",
- model.value === m.value ? "opacity-100" : "opacity-0",
- )}
- />
- <code>{m.label}</code>
- </CommandItem>
- ))}
- </CommandGroup>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
-
- <Button size="lg" className="w-full" type="submit">
- Save
- </Button>
- </form>
- <p className="mt-4 text-sm text-muted-foreground">
- Not stored anywhere. Used only for current session requests.
- </p>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/apps/web/app/components/editor/use-create-editor.tsx b/apps/web/app/components/editor/use-create-editor.tsx
deleted file mode 100644
index bae45a74..00000000
--- a/apps/web/app/components/editor/use-create-editor.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import { withProps } from "@udecode/cn";
-import { AIPlugin } from "@udecode/plate-ai/react";
-import {
- BoldPlugin,
- CodePlugin,
- ItalicPlugin,
- StrikethroughPlugin,
- SubscriptPlugin,
- SuperscriptPlugin,
- UnderlinePlugin,
-} from "@udecode/plate-basic-marks/react";
-import { BlockquotePlugin } from "@udecode/plate-block-quote/react";
-import { CodeBlockPlugin, CodeLinePlugin, CodeSyntaxPlugin } from "@udecode/plate-code-block/react";
-import { CommentsPlugin } from "@udecode/plate-comments/react";
-import { Value } from "@udecode/plate-common";
-import { ParagraphPlugin, PlateLeaf, usePlateEditor } from "@udecode/plate-common/react";
-import { DatePlugin } from "@udecode/plate-date/react";
-import { ExcalidrawPlugin } from "@udecode/plate-excalidraw/react";
-import { HEADING_KEYS } from "@udecode/plate-heading";
-import { TocPlugin } from "@udecode/plate-heading/react";
-import { HighlightPlugin } from "@udecode/plate-highlight/react";
-import { HorizontalRulePlugin } from "@udecode/plate-horizontal-rule/react";
-import { KbdPlugin } from "@udecode/plate-kbd/react";
-import { ColumnItemPlugin, ColumnPlugin } from "@udecode/plate-layout/react";
-import { LinkPlugin } from "@udecode/plate-link/react";
-import { MarkdownPlugin } from "@udecode/plate-markdown";
-import { ImagePlugin, MediaEmbedPlugin } from "@udecode/plate-media/react";
-import { MentionInputPlugin, MentionPlugin } from "@udecode/plate-mention/react";
-import { SlashInputPlugin } from "@udecode/plate-slash-command/react";
-import {
- TableCellHeaderPlugin,
- TableCellPlugin,
- TablePlugin,
- TableRowPlugin,
-} from "@udecode/plate-table/react";
-import { TogglePlugin } from "@udecode/plate-toggle/react";
-import { copilotPlugins } from "~/components/editor/plugins/copilot-plugins";
-import { editorPlugins } from "~/components/editor/plugins/editor-plugins";
-import { FixedToolbarPlugin } from "~/components/editor/plugins/fixed-toolbar-plugin";
-import { FloatingToolbarPlugin } from "~/components/editor/plugins/floating-toolbar-plugin";
-import { AILeaf } from "~/components/plate-ui/ai-leaf";
-import { BlockquoteElement } from "~/components/plate-ui/blockquote-element";
-import { CodeBlockElement } from "~/components/plate-ui/code-block-element";
-import { CodeLeaf } from "~/components/plate-ui/code-leaf";
-import { CodeLineElement } from "~/components/plate-ui/code-line-element";
-import { CodeSyntaxLeaf } from "~/components/plate-ui/code-syntax-leaf";
-import { ColumnElement } from "~/components/plate-ui/column-element";
-import { ColumnGroupElement } from "~/components/plate-ui/column-group-element";
-import { CommentLeaf } from "~/components/plate-ui/comment-leaf";
-import { DateElement } from "~/components/plate-ui/date-element";
-import { ExcalidrawElement } from "~/components/plate-ui/excalidraw-element";
-import { HeadingElement } from "~/components/plate-ui/heading-element";
-import { HighlightLeaf } from "~/components/plate-ui/highlight-leaf";
-import { HrElement } from "~/components/plate-ui/hr-element";
-import { ImageElement } from "~/components/plate-ui/image-element";
-import { KbdLeaf } from "~/components/plate-ui/kbd-leaf";
-import { LinkElement } from "~/components/plate-ui/link-element";
-import { MediaEmbedElement } from "~/components/plate-ui/media-embed-element";
-import { MentionElement } from "~/components/plate-ui/mention-element";
-import { MentionInputElement } from "~/components/plate-ui/mention-input-element";
-import { ParagraphElement } from "~/components/plate-ui/paragraph-element";
-import { withPlaceholders } from "~/components/plate-ui/placeholder";
-import { SlashInputElement } from "~/components/plate-ui/slash-input-element";
-import { TableCellElement, TableCellHeaderElement } from "~/components/plate-ui/table-cell-element";
-import { TableElement } from "~/components/plate-ui/table-element";
-import { TableRowElement } from "~/components/plate-ui/table-row-element";
-import { TocElement } from "~/components/plate-ui/toc-element";
-import { ToggleElement } from "~/components/plate-ui/toggle-element";
-import { withDraggables } from "~/components/plate-ui/with-draggables";
-
-export const useCreateEditor = ({ initialValue }: { initialValue?: Value }) => {
- return usePlateEditor({
- override: {
- components: withDraggables(
- withPlaceholders({
- [AIPlugin.key]: AILeaf,
- [BlockquotePlugin.key]: BlockquoteElement,
- [BoldPlugin.key]: withProps(PlateLeaf, { as: "strong" }),
- [CodeBlockPlugin.key]: CodeBlockElement,
- [CodeLinePlugin.key]: CodeLineElement,
- [CodePlugin.key]: CodeLeaf,
- [CodeSyntaxPlugin.key]: CodeSyntaxLeaf,
- [ColumnItemPlugin.key]: ColumnElement,
- [ColumnPlugin.key]: ColumnGroupElement,
- [CommentsPlugin.key]: CommentLeaf,
- [DatePlugin.key]: DateElement,
- // [EmojiInputPlugin.key]: EmojiInputElement,
- [ExcalidrawPlugin.key]: ExcalidrawElement,
- [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: "h1" }),
- [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: "h2" }),
- [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: "h3" }),
- [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: "h4" }),
- [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: "h5" }),
- [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: "h6" }),
- [HighlightPlugin.key]: HighlightLeaf,
- [HorizontalRulePlugin.key]: HrElement,
- [ImagePlugin.key]: ImageElement,
- [ItalicPlugin.key]: withProps(PlateLeaf, { as: "em" }),
- [KbdPlugin.key]: KbdLeaf,
- [LinkPlugin.key]: LinkElement,
- [MediaEmbedPlugin.key]: MediaEmbedElement,
- [MentionInputPlugin.key]: MentionInputElement,
- [MentionPlugin.key]: MentionElement,
- [ParagraphPlugin.key]: ParagraphElement,
- [SlashInputPlugin.key]: SlashInputElement,
- [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: "s" }),
- [SubscriptPlugin.key]: withProps(PlateLeaf, { as: "sub" }),
- [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: "sup" }),
- [TableCellHeaderPlugin.key]: TableCellHeaderElement,
- [TableCellPlugin.key]: TableCellElement,
- [TablePlugin.key]: TableElement,
- [TableRowPlugin.key]: TableRowElement,
- [TocPlugin.key]: TocElement,
- [TogglePlugin.key]: ToggleElement,
- [UnderlinePlugin.key]: withProps(PlateLeaf, { as: "u" }),
- }),
- ),
- },
- plugins: [
- ...copilotPlugins,
- ...editorPlugins,
- FixedToolbarPlugin,
- FloatingToolbarPlugin,
- MarkdownPlugin,
- ],
- value: initialValue ?? [
- {
- children: [{ text: "Supermemory docs" }],
- type: "h1",
- },
- {
- children: [{ text: "Auto-updating AI-powered docs" }],
- type: ParagraphPlugin.key,
- },
- ],
- });
-};
diff --git a/apps/web/app/components/editor/writing-playground.tsx b/apps/web/app/components/editor/writing-playground.tsx
deleted file mode 100644
index b303ffc8..00000000
--- a/apps/web/app/components/editor/writing-playground.tsx
+++ /dev/null
@@ -1,242 +0,0 @@
-import { lazy, memo, useEffect, useRef, useState } from "react";
-import { DndProvider } from "react-dnd";
-import { HTML5Backend } from "react-dnd-html5-backend";
-
-import { useChat } from "@ai-sdk/react";
-import { Message } from "ai";
-import { OpenAIProvider } from "~/components/editor/use-chat";
-import { useCreateEditor } from "~/components/editor/use-create-editor";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
-import { useLiveTranscript } from "~/lib/hooks/use-live-transcript";
-import { Theme, useTheme } from "~/lib/theme-provider";
-
-const PlateEditorImport = lazy(() =>
- import("@udecode/plate-common/react").then((mod) => ({ default: mod.Plate })),
-);
-
-const EditorContainerImport = lazy(() =>
- import("~/components/plate-ui/editor").then((mod) => ({ default: mod.EditorContainer })),
-);
-
-const EditorImport = lazy(() =>
- import("~/components/plate-ui/editor").then((mod) => ({ default: mod.Editor })),
-);
-
-const Plate = memo(PlateEditorImport);
-const EditorContainer = memo(EditorContainerImport);
-const Editor = memo(EditorImport);
-
-export function WritingPlayground() {
- if (typeof window === "undefined") {
- return <div>Loading...</div>;
- }
- const localValue = localStorage.getItem("editorContent");
- const editor = useCreateEditor({ initialValue: localValue ? JSON.parse(localValue) : undefined });
-
- const [theme, setTheme] = useTheme();
-
- useEffect(() => {
- setTheme(Theme.LIGHT);
- }, [theme, setTheme]);
-
- const { toggleMicrophone, caption, status, isListening, isLoading } = useLiveTranscript();
-
- const { messages, input, handleInputChange, handleSubmit } = useChat({
- id: "editor",
- api: "/api/ai/command",
- initialMessages: [
- {
- id: "1",
- content: "Hi! I am here to help you quickly find what you're looking for.",
- role: "assistant",
- },
- {
- id: "2",
- content: "Just drop a question when you need me, ok?",
- role: "assistant",
- },
- ],
- keepLastMessageOnError: true,
- // @ts-expect-error
- experimental_prepareRequestBody: (request) => {
- // messages with the documentation content
- // @ts-expect-error
- const markdown = editor.api.markdown.serialize();
- console.log(JSON.stringify(editor.children));
- console.log(markdown);
- return {
- messages: [
- ...request.messages,
- {
- id: "3",
- content: `Here is the documentation for the company: ${markdown}`,
- role: "user",
- } satisfies Message,
- ],
- };
- },
- });
-
- const [lastProcessedLength, setLastProcessedLength] = useState(0);
- const [isProcessing, setIsProcessing] = useState(false);
- const updateTimeoutRef = useRef<NodeJS.Timeout>();
-
- useEffect(() => {
- if (!isListening) {
- return;
- }
- // Clear existing timeout
- if (updateTimeoutRef.current) {
- clearTimeout(updateTimeoutRef.current);
- }
-
- const currentSentences = caption.split(".").length;
-
- // Only process if we have new sentences and aren't currently processing
- if (currentSentences > lastProcessedLength && !isProcessing) {
- // Debounce the update for 2 seconds
- updateTimeoutRef.current = setTimeout(async () => {
- setIsProcessing(true);
- try {
- const result = await fetch("/api/ai/update", {
- method: "POST",
- body: JSON.stringify({ caption, document: editor.children }),
- });
- const data = (await result.json()) as {
- action: "edit" | "delete" | "append" | "ignore";
- blockId?: string;
- content?: string;
- reason: string;
- };
- if (data.action === "ignore") {
- return;
- }
- if (data.action === "edit") {
- // Make a copy of the editor children
- const newChildren = [...editor.children];
- // Find and update the block in the copy
- const blockIndex = newChildren.findIndex((block) => block.id === data.blockId);
- if (blockIndex !== -1) {
- newChildren[blockIndex] = {
- ...newChildren[blockIndex],
- children: [{ text: data.content ?? "" }],
- };
- }
- editor.tf.setValue(newChildren);
- } else if (data.action === "delete") {
- // Make a copy of the editor children and filter out the block
- const newChildren = editor.children.filter((block) => block.id !== data.blockId);
- editor.tf.setValue(newChildren);
- } else if (data.action === "append") {
- editor.tf.setValue([
- ...editor.children,
- {
- type: "paragraph",
- children: [{ text: data.content ?? "" }],
- },
- ]);
- }
- setLastProcessedLength(currentSentences);
- } catch (error) {
- console.error("Error updating editor:", error);
- } finally {
- setIsProcessing(false);
- }
- }, 2000);
- }
-
- return () => {
- if (updateTimeoutRef.current) {
- clearTimeout(updateTimeoutRef.current);
- }
- };
- }, [caption, editor, lastProcessedLength, isProcessing]);
-
- return (
- <div className="grid gap-4 grid-cols-2">
- <Tabs defaultValue="docs">
- <TabsList className="absolute top-12 left-12 z-50">
- <TabsTrigger value="docs">Docs</TabsTrigger>
- <TabsTrigger value="transcript">Transcript</TabsTrigger>
-
- <div className="flex items-center gap-2">
- <button
- onClick={toggleMicrophone}
- className={`flex items-center justify-center w-10 h-10 rounded-full transition-colors ${
- isListening
- ? "bg-red-500 hover:bg-red-600"
- : "bg-zinc-200 hover:bg-zinc-300 dark:bg-zinc-800 dark:hover:bg-zinc-700"
- }`}
- title={isListening ? "Stop recording" : "Start recording"}
- >
- <div className={`w-3 h-3 rounded-full ${isListening ? "bg-white" : "bg-red-500"}`} />
- </button>
- <span className="text-sm text-zinc-600 dark:text-zinc-400">{status}</span>
- </div>
- </TabsList>
- <div
- className="h-screen col-span-1 dark:caret-white relative overflow-auto"
- data-registry="plate"
- >
- <TabsContent value="docs">
- <OpenAIProvider>
- <DndProvider backend={HTML5Backend}>
- <Plate
- onChange={({ value }) => {
- // For performance, debounce your saving logic
- localStorage.setItem("editorContent", JSON.stringify(value));
- }}
- editor={editor}
- >
- <EditorContainer className="w-full border">
- <Editor variant="default" />
- </EditorContainer>
- </Plate>
- </DndProvider>
- </OpenAIProvider>
- </TabsContent>
- <TabsContent value="transcript">
- <div className="h-screen p-16 pt-24">{caption}</div>
- </TabsContent>
- </div>
- </Tabs>
- <div className="h-screen col-span-1 border flex flex-col">
- <div className="flex-1 overflow-y-auto p-4 space-y-4">
- {messages.map((message) => (
- <div
- key={message.id}
- className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}
- >
- <div
- className={`max-w-[80%] rounded-xl p-3 ${
- message.role === "user"
- ? "bg-zinc-900 text-white"
- : "bg-zinc-100 dark:bg-zinc-800"
- }`}
- >
- {message.content}
- </div>
- </div>
- ))}
- </div>
- <div className="border-t border-zinc-200 dark:border-zinc-800 p-4">
- <form onSubmit={handleSubmit} className="flex gap-2">
- <input
- name="prompt"
- value={input}
- onChange={handleInputChange}
- placeholder="Type a message..."
- className="flex-1 rounded-xl border border-zinc-200 dark:border-zinc-800 p-2 focus:outline-none focus:ring-1 bg-zinc-100 focus:ring-zinc-400 dark:bg-zinc-900 dark:text-white"
- />
- <button
- type="submit"
- className="rounded-xl bg-zinc-900 px-4 py-2 text-white hover:bg-zinc-800 focus:outline-none focus:ring-1 focus:ring-zinc-400 transition-colors"
- >
- Send
- </button>
- </form>
- </div>
- </div>
- </div>
- );
-} \ No newline at end of file
diff --git a/apps/web/app/components/gradients/gradient1.png b/apps/web/app/components/gradients/gradient1.png
deleted file mode 100644
index 123ebf32..00000000
--- a/apps/web/app/components/gradients/gradient1.png
+++ /dev/null
Binary files differ
diff --git a/apps/web/app/components/icons/IntegrationIcons.tsx b/apps/web/app/components/icons/IntegrationIcons.tsx
deleted file mode 100644
index 1a8998b5..00000000
--- a/apps/web/app/components/icons/IntegrationIcons.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import type { SVGProps } from "react";
-
-export const NotionIcon = (props: SVGProps<SVGSVGElement>) => (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="1em"
- height="1em"
- preserveAspectRatio="xMidYMid"
- viewBox="0 0 256 268"
- {...props}
- >
- <path
- fill="#FFF"
- d="M16.092 11.538 164.09.608c18.179-1.56 22.85-.508 34.28 7.801l47.243 33.282C253.406 47.414 256 48.975 256 55.207v182.527c0 11.439-4.155 18.205-18.696 19.24L65.44 267.378c-10.913.517-16.11-1.043-21.825-8.327L8.826 213.814C2.586 205.487 0 199.254 0 191.97V29.726c0-9.352 4.155-17.153 16.092-18.188Z"
- />
- <path d="M164.09.608 16.092 11.538C4.155 12.573 0 20.374 0 29.726v162.245c0 7.284 2.585 13.516 8.826 21.843l34.789 45.237c5.715 7.284 10.912 8.844 21.825 8.327l171.864-10.404c14.532-1.035 18.696-7.801 18.696-19.24V55.207c0-5.911-2.336-7.614-9.21-12.66l-1.185-.856L198.37 8.409C186.94.1 182.27-.952 164.09.608ZM69.327 52.22c-14.033.945-17.216 1.159-25.186-5.323L23.876 30.778c-2.06-2.086-1.026-4.69 4.163-5.207l142.274-10.395c11.947-1.043 18.17 3.12 22.842 6.758l24.401 17.68c1.043.525 3.638 3.637.517 3.637L71.146 52.095l-1.819.125Zm-16.36 183.954V81.222c0-6.767 2.077-9.887 8.3-10.413L230.02 60.93c5.724-.517 8.31 3.12 8.31 9.879v153.917c0 6.767-1.044 12.49-10.387 13.008l-161.487 9.361c-9.343.517-13.489-2.594-13.489-10.921ZM212.377 89.53c1.034 4.681 0 9.362-4.681 9.897l-7.783 1.542v114.404c-6.758 3.637-12.981 5.715-18.18 5.715-8.308 0-10.386-2.604-16.609-10.396l-50.898-80.079v77.476l16.1 3.646s0 9.362-12.989 9.362l-35.814 2.077c-1.043-2.086 0-7.284 3.63-8.318l9.351-2.595V109.823l-12.98-1.052c-1.044-4.68 1.55-11.439 8.826-11.965l38.426-2.585 52.958 81.113v-71.76l-13.498-1.552c-1.043-5.733 3.111-9.896 8.3-10.404l35.84-2.087Z" />
- </svg>
-);
-
-export const GoogleCalendarIcon = (props: SVGProps<SVGSVGElement>) => (
- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
- <path d="M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zm0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z" />
- </svg>
-);
-
-export const TwitterIcon = (props: SVGProps<SVGSVGElement>) => (
- <svg
- viewBox="0 0 256 209"
- width="1em"
- height="1em"
- xmlns="http://www.w3.org/2000/svg"
- preserveAspectRatio="xMidYMid"
- {...props}
- >
- <path
- d="M256 25.45c-9.42 4.177-19.542 7-30.166 8.27 10.845-6.5 19.172-16.793 23.093-29.057a105.183 105.183 0 0 1-33.351 12.745C205.995 7.201 192.346.822 177.239.822c-29.006 0-52.523 23.516-52.523 52.52 0 4.117.465 8.125 1.36 11.97-43.65-2.191-82.35-23.1-108.255-54.876-4.52 7.757-7.11 16.78-7.11 26.404 0 18.222 9.273 34.297 23.365 43.716a52.312 52.312 0 0 1-23.79-6.57c-.003.22-.003.44-.003.661 0 25.447 18.104 46.675 42.13 51.5a52.592 52.592 0 0 1-23.718.9c6.683 20.866 26.08 36.05 49.062 36.475-17.975 14.086-40.622 22.483-65.228 22.483-4.24 0-8.42-.249-12.529-.734 23.243 14.902 50.85 23.597 80.51 23.597 96.607 0 149.434-80.031 149.434-149.435 0-2.278-.05-4.543-.152-6.795A106.748 106.748 0 0 0 256 25.45"
- fill="#55acee"
- />
- </svg>
-);
-
-export const GithubIcon = (props: SVGProps<SVGSVGElement>) => (
- <svg
- viewBox="0 0 256 250"
- width="1em"
- height="1em"
- fill="#fff"
- xmlns="http://www.w3.org/2000/svg"
- preserveAspectRatio="xMidYMid"
- {...props}
- >
- <path d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0Zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931Zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66Zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08Zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27Zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622Zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868Zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403Z" />
- </svg>
-);
-
-export const DiscordIcon = (props: SVGProps<SVGSVGElement>) => (
- <svg
- viewBox="0 0 256 199"
- width="1em"
- height="1em"
- xmlns="http://www.w3.org/2000/svg"
- preserveAspectRatio="xMidYMid"
- {...props}
- >
- <path
- d="M216.856 16.597A208.502 208.502 0 0 0 164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0-1.832-4.4-4.55-9.933-6.846-14.046a207.809 207.809 0 0 0-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161.094 161.094 0 0 0 79.735 175.3a136.413 136.413 0 0 1-21.846-10.632 108.636 108.636 0 0 0 5.356-4.237c42.122 19.702 87.89 19.702 129.51 0a131.66 131.66 0 0 0 5.355 4.237 136.07 136.07 0 0 1-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848 21.142-6.58 42.646-16.637 64.815-33.213 5.316-56.288-9.08-105.09-38.056-148.36ZM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2c12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18Zm85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18Z"
- fill="#5865F2"
- />
- </svg>
-);
diff --git a/apps/web/app/components/icons/Logo.tsx b/apps/web/app/components/icons/Logo.tsx
deleted file mode 100644
index 8679cfea..00000000
--- a/apps/web/app/components/icons/Logo.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-export const Logo: React.FC<React.SVGAttributes<SVGElement>> = (props) => (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="42"
- height="42"
- viewBox="0 0 42 42"
- {...props}
- >
- <path
- className="fill-gray-700 dark:fill-gray-100"
- d="M19.0357 8C20.5531 8 21 9.27461 21 10.8438V16.3281H23.5536V14.2212C23.5536 13.1976 23.9468 12.216 24.6467 11.4922L25.0529 11.0721C24.9729 10.8772 24.9286 10.6627 24.9286 10.4375C24.9286 9.54004 25.6321 8.8125 26.5 8.8125C27.3679 8.8125 28.0714 9.54004 28.0714 10.4375C28.0714 11.335 27.3679 12.0625 26.5 12.0625C26.2822 12.0625 26.0748 12.0167 25.8863 11.9339L25.4801 12.354C25.0012 12.8492 24.7321 13.5209 24.7321 14.2212V16.3281H28.9714C29.2045 15.7326 29.7691 15.3125 30.4286 15.3125C31.2964 15.3125 32 16.04 32 16.9375C32 17.835 31.2964 18.5625 30.4286 18.5625C29.7691 18.5625 29.2045 18.1424 28.9714 17.5469H21V21.2031H25.0428C25.2759 20.6076 25.8405 20.1875 26.5 20.1875C27.3679 20.1875 28.0714 20.915 28.0714 21.8125C28.0714 22.71 27.3679 23.4375 26.5 23.4375C25.8405 23.4375 25.2759 23.0174 25.0428 22.4219H21V26.0781H24.4125C25.4023 26.0781 26.3516 26.4847 27.0515 27.2085L29.0292 29.2536C29.2177 29.1708 29.4251 29.125 29.6429 29.125C30.5107 29.125 31.2143 29.8525 31.2143 30.75C31.2143 31.6475 30.5107 32.375 29.6429 32.375C28.775 32.375 28.0714 31.6475 28.0714 30.75C28.0714 30.5248 28.1157 30.3103 28.1958 30.1154L26.2181 28.0703C25.7392 27.5751 25.0897 27.2969 24.4125 27.2969H21V31.1562C21 32.7254 20.5531 34 19.0357 34C17.6165 34 16.4478 32.8879 16.3004 31.4559C16.0451 31.527 15.775 31.5625 15.5 31.5625C13.7665 31.5625 12.3571 30.1051 12.3571 28.3125C12.3571 27.9367 12.421 27.5711 12.5339 27.2359C11.0509 26.657 10 25.1742 10 23.4375C10 21.8176 10.9183 20.416 12.2491 19.766C11.8219 19.2125 11.5714 18.5117 11.5714 17.75C11.5714 16.191 12.6321 14.891 14.0464 14.5711C13.9679 14.2918 13.9286 13.9922 13.9286 13.6875C13.9286 12.1691 14.9402 10.8895 16.3004 10.534C16.4478 9.11211 17.6165 8 19.0357 8Z"
- />
- </svg>
-);
diff --git a/apps/web/app/components/markdown/codeblock.tsx b/apps/web/app/components/markdown/codeblock.tsx
deleted file mode 100644
index 7f6756d7..00000000
--- a/apps/web/app/components/markdown/codeblock.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { CodeToHtmlOptions } from "@llm-ui/code";
-import { allLangs, allLangsAlias, loadHighlighter, useCodeBlockToHtml } from "@llm-ui/code";
-import { type LLMOutputComponent } from "@llm-ui/react";
-import parseHtml from "html-react-parser";
-import { getHighlighterCore } from "shiki/core";
-import { bundledLanguagesInfo } from "shiki/langs";
-// WARNING: Importing bundledThemes increases your bundle size
-// see: https://llm-ui.com/docs/blocks/code#bundle-size
-import { bundledThemes } from "shiki/themes";
-import getWasm from "shiki/wasm";
-
-const highlighter = loadHighlighter(
- getHighlighterCore({
- langs: allLangs(bundledLanguagesInfo),
- langAlias: allLangsAlias(bundledLanguagesInfo),
- themes: Object.values(bundledThemes),
- loadWasm: getWasm,
- }),
-);
-
-const codeToHtmlOptions: CodeToHtmlOptions = {
- theme: "github-dark",
-};
-
-export const CodeBlock: LLMOutputComponent = ({ blockMatch }) => {
- const { html, code } = useCodeBlockToHtml({
- markdownCodeBlock: blockMatch.output,
- highlighter,
- codeToHtmlOptions,
- });
- if (!html) {
- // fallback to <pre> if Shiki is not loaded yet
- return (
- <pre className="shiki">
- <code>{code}</code>
- </pre>
- );
- }
- return <>{parseHtml(html)}</>;
-};
diff --git a/apps/web/app/components/markdown/renderer.tsx b/apps/web/app/components/markdown/renderer.tsx
deleted file mode 100644
index 12e47331..00000000
--- a/apps/web/app/components/markdown/renderer.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import ReactMarkdown from "react-markdown";
-import remarkGfm from "remark-gfm";
-import { type LLMOutputComponent } from "@llm-ui/react";
-
-
-// Customize this component with your own styling
-export const MarkdownComponent: LLMOutputComponent = ({ blockMatch }) => {
- const markdown = blockMatch.output;
- return <ReactMarkdown remarkPlugins={[remarkGfm]}>{markdown}</ReactMarkdown>;
-}; \ No newline at end of file
diff --git a/apps/web/app/components/memories/AddMemory.tsx b/apps/web/app/components/memories/AddMemory.tsx
deleted file mode 100644
index e863d209..00000000
--- a/apps/web/app/components/memories/AddMemory.tsx
+++ /dev/null
@@ -1,583 +0,0 @@
-import React, { useEffect, useState } from "react";
-
-import { Logo } from "../icons/Logo";
-import { Button } from "../ui/button";
-import { Input } from "../ui/input";
-import { Switch } from "../ui/switch";
-import { Textarea } from "../ui/textarea";
-import Integrations from "./Integrations";
-import { FetchAndRenderContent, typeDecider } from "./SharedCard";
-import SpacesSelector from "./SpacesSelector";
-
-import { FileIcon, Link1Icon, PlusCircledIcon } from "@radix-ui/react-icons";
-import { SpaceIcon } from "@supermemory/shared/icons";
-import { AnimatePresence, motion } from "framer-motion";
-import { NotebookIcon, Plus, PuzzleIcon, Settings2Icon, Upload } from "lucide-react";
-import { toast } from "sonner";
-import {
- Credenza,
- CredenzaBody,
- CredenzaClose,
- CredenzaContent,
- CredenzaFooter,
- CredenzaHeader,
- CredenzaTitle,
- CredenzaTrigger,
-} from "~/components/ui/credenza";
-import { Label } from "~/components/ui/label";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
-import { useDebounce } from "~/lib/hooks/use-debounce";
-import { useMemories } from "~/lib/hooks/use-memories";
-import { useSpaces } from "~/lib/hooks/use-spaces";
-import { useUploadFile } from "~/lib/hooks/use-upload-file";
-import { cn } from "~/lib/utils";
-
-export function AddMemoryModal({
- children,
- spaceTab,
-}: {
- children: React.ReactNode;
- spaceTab?: boolean;
-}) {
- const [content, setContent] = useState<string | null>(null);
- const [activeTab, setActiveTab] = useState<
- "url" | "note" | "space" | "document" | "integrations"
- >(spaceTab ? "space" : "url");
- const [file, setFile] = useState<File | null>(null);
- const [isUploading, setIsUploading] = useState(false);
-
- const [isPublic, setIsPublic] = useState(false);
-
- const { uploadFile } = useUploadFile();
-
- useEffect(() => {
- if (!spaceTab) {
- const validTabs = ["url", "note", "space", "document", "integrations"];
- const savedTab = window.localStorage.getItem("lastUsedMemoryTab");
-
- if (savedTab && validTabs.includes(savedTab)) {
- setActiveTab(savedTab as typeof activeTab);
- }
- }
- }, [spaceTab]);
-
- // Get space ID from URL if we're in a space
- const [selectedSpaces, setSelectedSpaces] = useState<string[]>(() => {
- if (typeof window !== "undefined") {
- const match = window.location.pathname.match(/\/space\/([^\/]+)/);
- return match ? [match[1].split("---")[0]] : [];
- }
- return [];
- });
-
- const { createSpace, isCreating } = useSpaces();
- const { addMemory } = useMemories();
-
- // Debounce the content to avoid rapid type detection
- const debouncedContent = useDebounce(content, 500);
-
- // Reset content when switching tabs manually
- const handleTabChange = (value: string) => {
- setContent(null);
- setFile(null);
- setActiveTab(value as "url" | "note" | "space" | "document");
- window.localStorage.setItem("lastUsedMemoryTab", value);
- };
-
- const handleSubmit = async () => {
- if (activeTab === "document") {
- if (!file) return;
-
- setIsUploading(true);
- try {
- const { url } = await uploadFile(file);
- if (url) {
- addMemory({
- content: url,
- spaces: selectedSpaces,
- });
- }
- } catch (error) {
- console.error("Error uploading file:", error);
- toast.error("Failed to upload file");
- } finally {
- setIsUploading(false);
- }
- } else if (!content) {
- return;
- } else if (activeTab === "space") {
- try {
- createSpace({
- spaceName: content,
- isPublic,
- });
- } catch (error) {
- console.error("Error creating space:", error);
- }
- } else {
- try {
- console.log("Add memory run");
- addMemory({
- content,
- spaces: selectedSpaces,
- });
- } catch (error) {
- console.error("Error adding memory:", error);
- }
- }
- // click the close-memory-modal button
- const closeButton = document.getElementById("close-memory-modal");
- if (closeButton) {
- closeButton.click();
- }
- };
-
- const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- const selectedFile = e.target.files?.[0];
- if (selectedFile) {
- setFile(selectedFile);
- // Create object URL for preview
- const fileUrl = URL.createObjectURL(selectedFile);
- setContent(fileUrl);
- }
- };
-
- return (
- <Credenza>
- <CredenzaTrigger asChild>{children}</CredenzaTrigger>
- <CredenzaContent className="md:w-[90%] lg:w-[80%] xl:w-[70%] max-w-full h-[80vh] shadow-md sm:rounded-xl overflow-hidden">
- <CredenzaHeader className="h-min">
- <CredenzaTitle className="mb-4">
- <div className="flex flex-row gap-2 items-center">
- <PlusCircledIcon className="h-4 w-4 mr-2" />
- Add Memory
- </div>
- </CredenzaTitle>
- </CredenzaHeader>
- <CredenzaBody className="w-full h-full grid grid-cols-1 lg:grid-cols-3 gap-2 overflow-y-auto">
- <Tabs
- value={activeTab}
- onValueChange={handleTabChange}
- className={cn(
- "max-w-full min-h-[400px] gap-4 flex flex-col md:flex-row lg:col-span-2 w-full",
- activeTab === "integrations" && "lg:col-span-3",
- )}
- >
- <TabsList className="border md:flex-col md:h-full h-max md:justify-start md:space-y-2 bg-[#FAFBFC] dark:bg-zinc-800 md:col-span-1 p-2 flex flex-row justify-between overflow-x-auto md:overflow-y-scroll shrink-0">
- <TabsTrigger
- className="w-full justify-start text-left px-4 py-3 rounded-lg border border-transparent data-[state=active]:border-indigo-500/20 data-[state=active]:bg-indigo-500/10 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex flex-col items-start gap-2 transition-all duration-200 hover:shadow-sm"
- id="url-tab"
- value="url"
- >
- <Link1Icon className="h-4 w-4 mr-2 text-indigo-500" />
- Website
- <span className="text-xs text-zinc-500 dark:text-zinc-300 hidden md:block">
- Add a website or tweet URL
- </span>
- </TabsTrigger>
- <TabsTrigger
- className="w-full justify-start text-left px-4 py-3 rounded-lg border border-transparent data-[state=active]:border-emerald-500/20 data-[state=active]:bg-emerald-500/10 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex flex-col items-start gap-2 transition-all duration-200 hover:shadow-sm"
- id="note-tab"
- value="note"
- >
- <NotebookIcon className="h-4 w-4 mr-2 text-emerald-500" />
- Note
- <span className="text-xs text-zinc-500 dark:text-zinc-300 hidden md:block">
- Add a note or use the rich editor
- </span>
- </TabsTrigger>
- <TabsTrigger
- className="w-full justify-start text-left px-4 py-3 rounded-lg border border-transparent data-[state=active]:border-amber-500/20 data-[state=active]:bg-amber-500/10 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex flex-col items-start gap-2 transition-all duration-200 hover:shadow-sm"
- id="document-tab"
- value="document"
- >
- <FileIcon className="h-4 w-4 mr-2 text-amber-500" />
- Document
- <span className="text-xs text-zinc-500 dark:text-zinc-300 hidden md:block">
- Upload a PDF or other document
- </span>
- </TabsTrigger>
- <TabsTrigger
- className="w-full justify-start text-left px-4 py-3 rounded-lg border border-transparent data-[state=active]:border-violet-500/20 data-[state=active]:bg-violet-500/10 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex flex-col items-start gap-2 transition-all duration-200 hover:shadow-sm"
- id="integrations-tab"
- value="integrations"
- >
- <PuzzleIcon className="h-4 w-4 mr-2 text-violet-500" />
- Integrations
- <span className="text-xs text-zinc-500 dark:text-zinc-300 hidden md:block">
- Import memories from third-party services
- </span>
- </TabsTrigger>
- <TabsTrigger
- className="w-full justify-start text-left px-4 py-3 rounded-lg border border-transparent data-[state=active]:border-rose-500/20 data-[state=active]:bg-rose-500/10 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex flex-col items-start gap-2 transition-all duration-200 hover:shadow-sm"
- id="space-tab"
- value="space"
- >
- <SpaceIcon className="h-4 w-4 mr-2 text-rose-500" />
- Create Space
- <span className="text-xs text-zinc-500 dark:text-zinc-300 hidden md:block">
- A collection of memories
- </span>
- </TabsTrigger>
- </TabsList>
-
- <div className="w-full overflow-y-auto">
- <TabsContent value="url">
- <form
- onSubmit={(e) => {
- e.preventDefault();
- handleSubmit();
- }}
- >
- <div className="flex flex-col gap-4 p-6 bg-white dark:bg-zinc-800 rounded-lg shadow-lg border border-indigo-500/20 dark:border-indigo-500/20">
- <Label
- htmlFor="url"
- className="text-lg font-semibold text-zinc-900 dark:text-zinc-100"
- >
- Website or Tweet URL
- </Label>
- <Input
- value={content ?? ""}
- onChange={(e) => setContent(e.target.value)}
- className="text-lg p-4 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 focus:ring-indigo-500 focus:border-indigo-500"
- id="url"
- autoFocus
- placeholder="https://supermemory.ai"
- autoComplete="off"
- />
-
- {/* Additional note */}
- <div className="hidden md:flex text-sm text-zinc-600 dark:text-zinc-200 border border-zinc-200 dark:border-zinc-700 rounded-md p-3 bg-zinc-50 dark:bg-zinc-900 items-center justify-between shadow-sm">
- <span className="text-sm">
- Pro tip:{" "}
- <a
- className="underline text-sky-500 hover:text-sky-600 dark:text-sky-400 dark:hover:text-sky-300"
- href="https://supermemory.ai/extension"
- >
- Use our Chrome extension
- </a>{" "}
- to save websites and tweets instantly
- </span>
- </div>
-
- <SpacesSelector selectedSpaces={selectedSpaces} onChange={setSelectedSpaces} />
- </div>
- </form>
- </TabsContent>
- <TabsContent value="note">
- <form
- onSubmit={(e) => {
- e.preventDefault();
- handleSubmit();
- }}
- >
- <div className="flex flex-col gap-4 p-6 bg-white dark:bg-zinc-800 rounded-lg shadow-lg border border-emerald-500/20 dark:border-emerald-500/20">
- <Label
- htmlFor="note"
- className="text-lg font-semibold text-zinc-900 dark:text-zinc-100"
- >
- Note
- </Label>
- <Textarea
- autoFocus
- value={content ?? ""}
- onChange={(e) => setContent(e.target.value)}
- className="text-lg p-4 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 focus:ring-emerald-500 focus:border-emerald-500 [&:-webkit-autofill]:bg-white [&:-webkit-autofill]:dark:bg-zinc-900"
- id="note"
- placeholder="Add a note"
- />
- <SpacesSelector selectedSpaces={selectedSpaces} onChange={setSelectedSpaces} />
- {/* TODO: will show this later */}
- {/* <div className="hidden md:flex text-sm border border-blue-500/20 rounded-md p-4 bg-blue-500/10 dark:bg-blue-500/5 flex-row gap-4 justify-between items-center shadow-sm">
- <span className="font-medium text-zinc-900 dark:text-zinc-100">
- You can also use the rich editor to write your note.
- </span>
- <Button
- variant="link"
- className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-semibold"
- asChild
- >
- <a href="/editor">Use Editor</a>
- </Button>
- </div> */}
- </div>
- </form>
- </TabsContent>
- <TabsContent value="space">
- {/* Ask for space name */}
- <div className="flex flex-col gap-4 p-6 bg-white dark:bg-zinc-800 rounded-lg shadow-lg border border-rose-500/20 dark:border-rose-500/20">
- <Label
- htmlFor="spaceName"
- className="text-lg font-semibold text-zinc-900 dark:text-zinc-100"
- >
- Create a space
- </Label>
- <Input
- id="spaceName"
- placeholder="Enter a name for your space"
- className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 [&:-webkit-autofill]:bg-white [&:-webkit-autofill]:dark:bg-zinc-900"
- value={content ?? ""}
- onChange={(e) => setContent(e.target.value)}
- autoFocus
- />
-
- <div className="flex flex-col gap-2">
- <div className="flex items-center space-x-2">
- <Switch
- className="data-[state=checked]:bg-emerald-500 data-[state=unchecked]:bg-zinc-200 dark:data-[state=unchecked]:bg-zinc-700 dark:data-[state=checked]:bg-emerald-500 bg-zinc-200 dark:bg-zinc-700"
- id="isPublic"
- onCheckedChange={setIsPublic}
- />
- <Label htmlFor="isPublic">Public</Label>
- </div>
- {isPublic && (
- <div className="text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1.5">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="14"
- height="14"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- >
- <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
- <line x1="12" y1="9" x2="12" y2="13" />
- <line x1="12" y1="17" x2="12.01" y2="17" />
- </svg>
- When public, anyone with the link can view this space
- </div>
- )}
- </div>
- </div>
- </TabsContent>
- <TabsContent className="pb-12 md:pb-0" value="document">
- <div className="flex flex-col gap-4 p-6 bg-white dark:bg-zinc-800 rounded-lg shadow-lg border border-amber-500/20 dark:border-amber-500/20">
- <Label
- htmlFor="file"
- className="text-lg font-semibold text-zinc-900 dark:text-zinc-100"
- >
- Upload Document
- </Label>
-
- <div className="flex flex-col items-center justify-center w-full">
- <label
- htmlFor="file-upload"
- className="flex flex-col items-center justify-center w-full h-64 border-2 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:bg-zinc-900 border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-zinc-800"
- >
- <div className="flex flex-col items-center justify-center pt-5 pb-6">
- <Upload className="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400" />
- <p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
- <span className="font-semibold">Click to upload</span> or drag and drop
- </p>
- <p className="text-xs text-gray-500 dark:text-gray-400">
- PDF, DOC, DOCX (MAX. 10MB)
- </p>
- </div>
- <input
- id="file-upload"
- type="file"
- className="hidden"
- accept=".pdf,.doc,.docx"
- onChange={handleFileChange}
- />
- </label>
- </div>
-
- {file && (
- <div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-zinc-900 rounded border border-gray-200 dark:border-gray-700">
- <FileIcon className="h-4 w-4 text-amber-500" />
- <span className="text-sm text-gray-700 dark:text-gray-300">{file.name}</span>
- </div>
- )}
-
- <SpacesSelector selectedSpaces={selectedSpaces} onChange={setSelectedSpaces} />
- </div>
- </TabsContent>
- <TabsContent value="integrations">
- <Integrations />
- </TabsContent>
- </div>
- </Tabs>
- {activeTab !== "integrations" && (
- <div className="hidden lg:block lg:col-span-1">
- <div className="md:h-full w-full relative overflow-hidden">
- <div className="absolute inset-0">
- {activeTab === "space" ? (
- <div className="relative bg-white dark:bg-zinc-900 rounded-lg">
- <div className="flex flex-col gap-4 h-full w-full p-8 border border-dashed border-rose-300/50 dark:border-rose-500/30 rounded-lg">
- <div className="absolute inset-0 after:content-[''] after:absolute after:inset-0 after:bg-[linear-gradient(to_right,rgba(0,0,0,0.05)_1px,transparent_1px),linear-gradient(to_bottom,rgba(0,0,0,0.05)_1px,transparent_1px)] after:bg-[size:24px_24px] after:[mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,black_60%,transparent_100%)] dark:after:bg-[linear-gradient(to_right,rgba(255,255,255,0.05)_1px,transparent_1px),linear-gradient(to_bottom,rgba(255,255,255,0.05)_1px,transparent_1px)]" />
-
- <div className="relative text-2xl font-light text-zinc-900 dark:text-zinc-100 mt-16 flex items-center gap-2">
- <SpaceIcon className="w-8 h-8 text-rose-500 dark:text-rose-400" />
- Create a Space
- </div>
- <div className="relative text-base text-zinc-600 dark:text-zinc-300">
- Spaces help you organize your memories. Create a new space to start
- collecting related memories together.
- </div>
- </div>
- </div>
- ) : activeTab === "document" && file ? (
- <div className="block p-4 rounded-3xl border border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
- <div className="flex items-center gap-3 text-gray-600 dark:text-gray-300">
- <FileIcon className="h-5 w-5" />
- <div className="flex flex-col">
- <span className="text-sm font-medium text-gray-500 dark:text-gray-400">
- Document
- </span>
- <span className="text-base font-medium text-gray-900 dark:text-white mt-0.5">
- {file.name}
- </span>
- </div>
- </div>
- </div>
- ) : debouncedContent ? (
- <FetchAndRenderContent content={debouncedContent} />
- ) : (
- <div className="relative bg-white dark:bg-zinc-900 rounded-lg">
- <div className="flex flex-col gap-4 h-full w-full p-8 border border-dashed border-zinc-300/50 dark:border-zinc-500/30 rounded-lg">
- <div className="absolute inset-0 after:content-[''] after:absolute after:inset-0 after:bg-[linear-gradient(to_right,rgba(0,0,0,0.05)_1px,transparent_1px),linear-gradient(to_bottom,rgba(0,0,0,0.05)_1px,transparent_1px)] after:bg-[size:24px_24px] after:[mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,black_60%,transparent_100%)] dark:after:bg-[linear-gradient(to_right,rgba(255,255,255,0.05)_1px,transparent_1px),linear-gradient(to_bottom,rgba(255,255,255,0.05)_1px,transparent_1px)]" />
-
- <div className="relative text-2xl font-light text-zinc-900 dark:text-zinc-100 mt-16">
- Create a Memory
- </div>
- <div className="relative text-base text-zinc-600 dark:text-zinc-300">
- Add your first memory to start building your knowledge base. You will see
- a preview here.
- </div>
- </div>
- </div>
- )}
- </div>
- <div className="absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-white dark:from-[#2B2B2B] to-transparent pointer-events-none" />
- </div>
- </div>
- )}
- </CredenzaBody>
- <CredenzaFooter className="border-t">
- <div className="flex justify-between w-full pt-2">
- <CredenzaClose asChild>
- <Button id="close-memory-modal" variant="outline">
- Cancel
- </Button>
- </CredenzaClose>
- <Button onClick={handleSubmit} disabled={!content && !file} className="relative">
- {isUploading ? (
- <>
- <span className="opacity-0">
- Add {activeTab === "space" ? "Space" : "Memory"}
- </span>
- <div className="absolute inset-0 flex items-center justify-center">
- <div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
- </div>
- </>
- ) : (
- <>Add {activeTab === "space" ? "Space" : "Memory"}</>
- )}
- </Button>
- </div>
- </CredenzaFooter>
- </CredenzaContent>
- </Credenza>
- );
-}
-
-function AddMemory({ isSpace = false }: { isSpace?: boolean }) {
- const [isMobile, setIsMobile] = useState(false);
-
- useEffect(() => {
- const checkMobile = () => {
- setIsMobile(window.innerWidth <= 768);
- };
-
- checkMobile();
- window.addEventListener("resize", checkMobile);
-
- return () => window.removeEventListener("resize", checkMobile);
- }, []);
-
- return (
- <div className={`flex ${isMobile ? "flex-col gap-4" : ""} w-full`}>
- <motion.div
- className={`flex ${isMobile ? "h-[100px]" : "h-[220px]"} w-full flex-col rounded-2xl items-start justify-start border-2 border-dashed border-gray-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 p-4 hover:border-gray-400 dark:hover:border-neutral-600 hover:bg-gray-50 dark:hover:bg-neutral-700 group`}
- whileHover={
- !isMobile
- ? {
- scale: 1.05,
- opacity: 0.9,
- boxShadow: "0px 10px 20px rgba(0, 0, 0, 0.2)",
- }
- : {}
- }
- whileTap={isMobile ? { scale: 0.95 } : {}}
- transition={{
- type: "spring",
- stiffness: 260,
- damping: 20,
- }}
- >
- <div
- className={
- isMobile
- ? "flex flex-col items-center justify-center w-full h-full"
- : "group-hover:hidden"
- }
- >
- {isMobile ? (
- <AddMemoryModal>
- <div className="flex flex-col items-center">
- <Logo className="h-8 w-8 text-rgray-10 dark:text-white" />
- <span className="dark:text-white text-sm">Add Memory</span>
- </div>
- </AddMemoryModal>
- ) : (
- <>
- <Plus className="h-12 w-12 text-black dark:text-white" />
- <div className="text-xl font-medium text-black dark:text-white p-2">
- Add to your second brain.
- </div>
- <div className="text-md text-neutral-500 dark:text-neutral-400 p-2 pr-4">
- Add a link, a note, a document, tweet, etc.
- </div>
- </>
- )}
- </div>
-
- <div
- className={`${isMobile ? "hidden" : "hidden group-hover:flex"} items-center justify-between h-full p-2 w-full`}
- >
- <AddMemoryModal>
- <div className="md:h-full p-4 rounded-md hover:bg-white dark:hover:bg-neutral-900 hover:border hover:border-border min-w-[47%] flex flex-col items-center justify-center cursor-pointer">
- <Logo className="h-12 w-12 text-rgray-10 dark:text-white" />
- <span className="dark:text-white">Memory</span>
- </div>
- </AddMemoryModal>
- {!isSpace && (
- <AddMemoryModal spaceTab={true}>
- <div className="h-full p-4 rounded-md hover:bg-white dark:hover:bg-neutral-900 hover:border hover:border-border min-w-[47%] flex flex-col items-center justify-center">
- <SpaceIcon className="h-12 w-12" />
- <span className="dark:text-white">Space</span>
- </div>
- </AddMemoryModal>
- )}
- </div>
- </motion.div>
- {isMobile && !isSpace && (
- <AddMemoryModal spaceTab={true}>
- <motion.div
- className="flex h-[100px] w-full flex-col gap-2 rounded-2xl items-center justify-center border-2 border-dashed border-gray-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 p-4 hover:border-gray-400 dark:hover:border-neutral-600 hover:bg-gray-50 dark:hover:bg-neutral-700 cursor-pointer"
- whileTap={{ scale: 0.95 }}
- >
- <SpaceIcon className="h-8 w-8" />
- <span className="dark:text-white text-sm">Create Space</span>
- </motion.div>
- </AddMemoryModal>
- )}
- </div>
- );
-}
-
-export default AddMemory;
diff --git a/apps/web/app/components/memories/CSVUploadModal.tsx b/apps/web/app/components/memories/CSVUploadModal.tsx
deleted file mode 100644
index 66359269..00000000
--- a/apps/web/app/components/memories/CSVUploadModal.tsx
+++ /dev/null
@@ -1,267 +0,0 @@
-import { useEffect, useState } from "react";
-
-import { Button } from "../ui/button";
-import { Dialog, DialogContent, DialogDescription, DialogTitle } from "../ui/dialog";
-import SpacesSelector from "./SpacesSelector";
-
-import { motion } from "framer-motion";
-import { AlertCircle, CheckCircle, Upload } from "lucide-react";
-import Papa from "papaparse";
-import { toast } from "sonner";
-
-interface CSVUploadModalProps {
- isOpen: boolean;
- onClose: () => void;
-}
-
-export function CSVUploadModal({ isOpen, onClose }: CSVUploadModalProps) {
- const [file, setFile] = useState<File | null>(null);
- const [urls, setUrls] = useState<string[]>([]);
- const [isUploading, setIsUploading] = useState(false);
- const [progress, setProgress] = useState<{
- progress: number;
- processed: number;
- total: number;
- succeeded: number;
- failed: number;
- status: string;
- } | null>(null);
- const [selectedSpaces, setSelectedSpaces] = useState<string[]>([]);
-
- const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- const selectedFile = e.target.files?.[0];
- if (selectedFile) {
- if (!selectedFile.name.endsWith(".csv")) {
- toast.error("Please upload a CSV file");
- return;
- }
- setFile(selectedFile);
-
- // Parse CSV file
- Papa.parse(selectedFile, {
- complete: (results) => {
- // Find column containing URLs by checking first row
- const firstRow = results.data[0];
- let urlColumnIndex = -1;
-
- // Look for a column containing URLs in the header row
- // @ts-expect-error - firstRow is of type unknown
- firstRow.forEach((cell: string, index: number) => {
- if (
- cell?.toLowerCase().includes("url") ||
- (cell && typeof cell === "string" && cell.trim().startsWith("http"))
- ) {
- urlColumnIndex = index;
- }
- });
-
- // If no URL column found in header, check first data row
- if (urlColumnIndex === -1 && results.data[1]) {
- // @ts-expect-error - results.data[1] is of type unknown
- results.data[1].forEach((cell: string, index: number) => {
- if (cell && typeof cell === "string" && cell.trim().startsWith("http")) {
- urlColumnIndex = index;
- }
- });
- }
-
- if (urlColumnIndex === -1) {
- toast.error("Could not find a column containing URLs");
- setFile(null);
- return;
- }
-
- // Extract URLs from the identified column
- const validUrls = results.data
- .slice(1) // Skip header row
- .map((row: any) => row[urlColumnIndex])
- .filter((url: string) => url && url.trim() && url.startsWith("http"));
-
- if (validUrls.length === 0) {
- toast.error("No valid URLs found in the CSV file");
- setFile(null);
- return;
- }
-
- setUrls(validUrls);
- toast.success(`Found ${validUrls.length} valid URLs in column ${urlColumnIndex + 1}`);
- },
- error: (error) => {
- toast.error("Error parsing CSV file: " + error.message);
- setFile(null);
- },
- });
- }
- };
-
- const handleUpload = async () => {
- if (!file || urls.length === 0) return;
-
- setIsUploading(true);
- try {
- const response = await fetch("/backend/v1/batch-add", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- urls,
- spaces: selectedSpaces,
- }),
- });
-
- if (!response.ok) {
- throw new Error("Failed to start batch upload");
- }
-
- const reader = response.body?.getReader();
- if (!reader) throw new Error("No reader available");
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- const text = new TextDecoder().decode(value);
- const lines = text.split("\n");
-
- for (const line of lines) {
- if (line.startsWith("data: ")) {
- try {
- const data = JSON.parse(line.slice(5));
- setProgress(data);
-
- if (data.status === "complete") {
- toast.success(
- `Batch upload complete! ${data.succeeded} succeeded, ${data.failed} failed`,
- );
- setTimeout(() => {
- onClose();
- setFile(null);
- setUrls([]);
- setProgress(null);
- }, 2000);
- }
- } catch (e) {
- console.error("Error parsing SSE data:", e);
- }
- }
- }
- }
- } catch (error) {
- toast.error("Upload failed: " + (error instanceof Error ? error.message : "Unknown error"));
- } finally {
- setIsUploading(false);
- }
- };
-
- return (
- <Dialog open={isOpen} onOpenChange={() => !isUploading && onClose()}>
- <DialogContent className="sm:max-w-md">
- {isUploading ? (
- <div className="text-center">
- {progress ? (
- <>
- <div className="relative">
- <div className="absolute inset-0 flex items-center justify-center">
- <span className="text-lg font-semibold text-blue-500">
- {progress.progress}%
- </span>
- </div>
- <svg className="size-20 md:size-24 -rotate-90 transform">
- <circle
- className="text-neutral-200 dark:text-neutral-700"
- strokeWidth="6"
- stroke="currentColor"
- fill="transparent"
- r="45"
- cx="48"
- cy="48"
- />
- <circle
- className="text-blue-500 transition-all duration-300"
- strokeWidth="6"
- strokeDasharray={283}
- strokeDashoffset={283 - (283 * progress.progress) / 100}
- strokeLinecap="round"
- stroke="currentColor"
- fill="transparent"
- r="45"
- cx="48"
- cy="48"
- />
- </svg>
- </div>
- <DialogTitle className="mt-4">
- Processing URLs ({progress.processed}/{progress.total})
- </DialogTitle>
- <DialogDescription className="mt-2">
- {progress.succeeded} succeeded, {progress.failed} failed
- </DialogDescription>
- </>
- ) : (
- <motion.div
- animate={{ rotate: 360 }}
- transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
- className="w-12 h-12 mx-auto"
- >
- <Upload className="h-full w-full text-neutral-900 dark:text-white" />
- </motion.div>
- )}
- </div>
- ) : (
- <div className="space-y-4">
- <DialogTitle>Upload CSV File</DialogTitle>
- <DialogDescription>
- Upload a CSV file containing URLs to add to your memories. The URLs should be in the
- first column.
- </DialogDescription>
-
- <div className="flex flex-col items-center justify-center w-full">
- <label
- htmlFor="csv-upload"
- className="flex flex-col items-center justify-center w-full h-64 border-2 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:bg-zinc-900 border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-zinc-800"
- >
- <div className="flex flex-col items-center justify-center pt-5 pb-6">
- <Upload className="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400" />
- <p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
- <span className="font-semibold">Click to upload</span> or drag and drop
- </p>
- <p className="text-xs text-gray-500 dark:text-gray-400">CSV files only</p>
- </div>
- <input
- id="csv-upload"
- type="file"
- className="hidden"
- accept=".csv"
- onChange={handleFileChange}
- />
- </label>
- </div>
-
- {file && (
- <>
- <div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-zinc-900 rounded border border-gray-200 dark:border-gray-700">
- <CheckCircle className="h-4 w-4 text-green-500" />
- <span className="text-sm text-gray-700 dark:text-gray-300">
- {file.name} ({urls.length} URLs found)
- </span>
- </div>
-
- <SpacesSelector selectedSpaces={selectedSpaces} onChange={setSelectedSpaces} />
-
- <div className="flex justify-end gap-2">
- <Button variant="outline" onClick={onClose}>
- Cancel
- </Button>
- <Button onClick={handleUpload} disabled={urls.length === 0}>
- Upload
- </Button>
- </div>
- </>
- )}
- </div>
- )}
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/apps/web/app/components/memories/Integrations.tsx b/apps/web/app/components/memories/Integrations.tsx
deleted file mode 100644
index c5bd2996..00000000
--- a/apps/web/app/components/memories/Integrations.tsx
+++ /dev/null
@@ -1,697 +0,0 @@
-import { useEffect, useState } from "react";
-
-import { useNavigate, useRouteLoaderData } from "@remix-run/react";
-
-import { Button } from "../ui/button";
-import { Card } from "../ui/card";
-import { Dialog, DialogContent, DialogDescription, DialogTitle } from "../ui/dialog";
-import { CSVUploadModal } from "./CSVUploadModal";
-import { MarkdownUploadModal } from "./MarkdownUploadModal";
-
-import { motion } from "framer-motion";
-import {
- AlertCircle,
- BookIcon,
- CheckCircle,
- Clipboard,
- ClipboardCheckIcon,
- FileUpIcon,
- X,
-} from "lucide-react";
-import { toast } from "sonner";
-import { type IntegrationConfig, getIntegrations } from "~/config/integrations";
-import { getChromeExtensionId } from "~/config/util";
-import { cn } from "~/lib/utils";
-import { loader } from "~/root";
-
-function IntegrationButton({
- integration,
- onClick,
-}: {
- integration: IntegrationConfig;
- onClick: () => void;
-}) {
- return (
- <Card
- className="group relative overflow-hidden transition-all hover:shadow-lg flex-1 basis-[calc(33.333%-1rem)]"
- onClick={onClick}
- >
- <div
- className={cn(
- "absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100",
- integration.buttonClassName,
- )}
- />
-
- <div className="relative z-10 flex flex-col items-center gap-4 p-6">
- {integration.icon && (
- <div className="rounded-full bg-white/10 p-3">
- <integration.icon
- className={cn(
- "transition-transform group-hover:scale-110",
- integration.iconClassName,
- )}
- />
- </div>
- )}
-
- <div className="text-center">
- <h3 className="font-semibold text-neutral-900 dark:text-white">{integration.name}</h3>
- <p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400 hidden md:block">
- {integration.description}
- </p>
- </div>
- </div>
- </Card>
- );
-}
-
-function LoadingIntegration({
- integration,
- onCancel,
-}: {
- integration: IntegrationConfig;
- onCancel: () => void;
-}) {
- const [extensionPresent, setExtensionPresent] = useState(false);
- const [importComplete, setImportComplete] = useState(false);
-
- useEffect(() => {
- if (integration.id === "xBookmarks") {
- try {
- // Listen for import complete message
- const handleMessage = (event: MessageEvent) => {
- if (event.data.type === "TWITTER_IMPORT_COMPLETE") {
- setImportComplete(true);
- // Auto close after 2 seconds
- setTimeout(() => {
- onCancel();
- }, 2000);
- }
- };
-
- window.addEventListener("message", handleMessage);
-
- // Check if extension is present
- chrome?.runtime.sendMessage(getChromeExtensionId(), { action: "ping" }, (response: any) => {
- setExtensionPresent(!chrome?.runtime.lastError);
- });
-
- return () => {
- window.removeEventListener("message", handleMessage);
- };
- } catch (e) {
- setExtensionPresent(false);
- }
- }
- }, [integration, onCancel]);
-
- return (
- <Dialog open onOpenChange={() => onCancel()}>
- <DialogContent className="sm:max-w-md">
- {integration.id === "xBookmarks" && !extensionPresent ? (
- <div className="text-center">
- <AlertCircle className="mx-auto h-12 w-12 text-yellow-500" />
- <DialogTitle className="mt-4">Extension Required</DialogTitle>
- <DialogDescription>
- Please install the SuperMemory extension to connect with X
- </DialogDescription>
- <Button variant="outline" onClick={onCancel} className="mt-4">
- Cancel
- </Button>
- </div>
- ) : importComplete ? (
- <div className="text-center">
- <CheckCircle className="mx-auto h-12 w-12 text-green-500" />
- <DialogTitle className="mt-4">Import Complete!</DialogTitle>
- <DialogDescription>Your X bookmarks have been successfully imported.</DialogDescription>
- </div>
- ) : (
- <div className="text-center">
- {integration.icon && (
- <motion.div
- animate={{ rotate: 360 }}
- transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
- className="w-12 h-12 mx-auto"
- >
- <integration.icon className="h-full w-full text-neutral-900 dark:text-white" />
- </motion.div>
- )}
- <DialogTitle className="mt-4">Importing from {integration.name}...</DialogTitle>
- <DialogDescription className="mt-4">
- The import has started. It will keep going in the background. You can close this modal
- and enjoy supermemory! (You might need to reload to see the changes!)
- </DialogDescription>
- <Button variant="outline" onClick={onCancel} className="mt-4">
- Cancel
- </Button>
- </div>
- )}
- </DialogContent>
- </Dialog>
- );
-}
-
-function Integrations() {
- const navigate = useNavigate();
- const data = useRouteLoaderData<typeof loader>("root");
- const env = data?.ENV || {};
- const integrations = getIntegrations(env);
- const [loadingIntegration, setLoadingIntegration] = useState<IntegrationConfig | null>(null);
- const [apiKey, setApiKey] = useState<string | null>(null);
- const [copied, setCopied] = useState(false);
- const [isCSVModalOpen, setIsCSVModalOpen] = useState(false);
- const [isMarkdownModalOpen, setIsMarkdownModalOpen] = useState(false);
-
- const handleIntegrationClick = (integration: IntegrationConfig) => {
- setLoadingIntegration(integration);
-
- if (integration.handleConnection) {
- integration.handleConnection(env, navigate);
- } else if (integration.getAuthUrl) {
- const authUrl = integration.getAuthUrl(env);
-
- // Small delay to show the loading state
- setTimeout(() => {
- if (authUrl.startsWith(window.location.origin)) {
- navigate(new URL(authUrl).pathname);
- } else {
- window.location.href = authUrl;
- }
- }, 500);
- }
- };
-
- const getApiKey = async () => {
- const response = await fetch(`/backend/v1/user/key`, {
- credentials: "include",
- });
- if (response.ok) {
- const data = (await response.json()) as { key: string };
- setApiKey(data.key);
- } else {
- toast.error("Failed to fetch API key");
- console.error("Failed to fetch API key", response);
- return null;
- }
- };
-
- useEffect(() => {
- getApiKey();
- }, []);
-
- useEffect(() => {
- if (copied) {
- const timeout = setTimeout(() => {
- setCopied(false);
- }, 2000);
- return () => clearTimeout(timeout);
- }
- }, [copied]);
-
- return (
- <div className="container mx-auto px-4 py-8 md:py-16 max-w-full overflow-hidden">
- <div className="mb-8 md:mb-12 text-center">
- <h2 className="text-3xl md:text-4xl font-bold tracking-tight text-neutral-900 dark:text-white">
- Import your content
- </h2>
- <p className="mt-3 text-base md:text-lg text-neutral-600 dark:text-neutral-400">
- Connect your favorite platforms to import your content into Supermemory
- </p>
- </div>
-
- <div className="max-w-full bg-neutral-50 dark:bg-neutral-900 p-4 rounded-lg border border-neutral-200 dark:border-neutral-800 mb-8">
- <div className="flex items-center gap-2">
- <span className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
- Your API Key
- </span>
- {apiKey ? (
- <button
- onClick={() => {
- navigator.clipboard.writeText(apiKey);
- setCopied(true);
- toast.success("API key copied to clipboard!");
- }}
- className="flex-1 flex items-center gap-2 font-mono text-sm bg-white dark:bg-neutral-800 px-3 py-1.5 rounded group hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-all overflow-hidden"
- >
- <span className="flex-shrink-0 text-neutral-400 dark:text-neutral-500 group-hover:text-neutral-600 dark:group-hover:text-neutral-300 transition-all">
- {copied ? (
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M5 13l4 4L19 7"
- />
- </svg>
- ) : (
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
- />
- </svg>
- )}
- </span>
- <span className="blur-sm group-hover:blur-none transition-all truncate max-w-[500px]">
- {apiKey}
- </span>
- </button>
- ) : (
- <div className="flex-1 text-sm text-neutral-600 dark:text-neutral-400">Loading...</div>
- )}
- </div>
- </div>
-
- <div className="flex flex-wrap gap-4 overflow-x-auto">
- <Card
- className="group relative overflow-hidden transition-all hover:shadow-lg flex-1 basis-[calc(33.333%-1rem)]"
- onClick={() => setIsMarkdownModalOpen(true)}
- >
- <div className="absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100 bg-gradient-to-r from-purple-500/10 to-purple-600/10" />
- <div className="relative z-10 flex flex-col items-center gap-4 p-6">
- <div className="rounded-full bg-white/10 p-3">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- className="w-6 h-6"
- preserveAspectRatio="xMidYMid"
- viewBox="0 0 256 332"
- >
- <defs>
- <radialGradient
- id="a"
- cx="72.819%"
- cy="96.934%"
- r="163.793%"
- fx="72.819%"
- fy="96.934%"
- gradientTransform="rotate(-104 11141.322 0)"
- >
- <stop offset="0%" stop-color="#FFF" stop-opacity=".4" />
- <stop offset="100%" stop-opacity=".1" />
- </radialGradient>
- <radialGradient
- id="b"
- cx="52.917%"
- cy="90.632%"
- r="190.361%"
- fx="52.917%"
- fy="90.632%"
- gradientTransform="rotate(-82 10746.75 0)"
- >
- <stop offset="0%" stop-color="#FFF" stop-opacity=".6" />
- <stop offset="100%" stop-color="#FFF" stop-opacity=".1" />
- </radialGradient>
- <radialGradient
- id="c"
- cx="31.174%"
- cy="97.138%"
- r="178.714%"
- fx="31.174%"
- fy="97.138%"
- gradientTransform="rotate(-77 10724.606 0)"
- >
- <stop offset="0%" stop-color="#FFF" stop-opacity=".8" />
- <stop offset="100%" stop-color="#FFF" stop-opacity=".4" />
- </radialGradient>
- <radialGradient
- id="d"
- cx="71.813%"
- cy="99.994%"
- r="92.086%"
- fx="71.813%"
- fy="99.994%"
- gradientTransform="translate(0 22251839.658) skewY(-90)"
- >
- <stop offset="0%" stop-color="#FFF" stop-opacity=".3" />
- <stop offset="100%" stop-opacity=".3" />
- </radialGradient>
- <radialGradient
- id="e"
- cx="117.013%"
- cy="34.769%"
- r="328.729%"
- fx="117.013%"
- fy="34.769%"
- gradientTransform="rotate(102 -1004.443 0)"
- >
- <stop offset="0%" stop-color="#FFF" stop-opacity="0" />
- <stop offset="100%" stop-color="#FFF" stop-opacity=".2" />
- </radialGradient>
- <radialGradient
- id="f"
- cx="-9.431%"
- cy="8.712%"
- r="153.492%"
- fx="-9.431%"
- fy="8.712%"
- gradientTransform="rotate(45 1674.397 0)"
- >
- <stop offset="0%" stop-color="#FFF" stop-opacity=".2" />
- <stop offset="100%" stop-color="#FFF" stop-opacity=".4" />
- </radialGradient>
- <radialGradient
- id="g"
- cx="103.902%"
- cy="-22.172%"
- r="394.771%"
- fx="103.902%"
- fy="-22.172%"
- gradientTransform="rotate(80 3757.522 0)"
- >
- <stop offset="0%" stop-color="#FFF" stop-opacity=".1" />
- <stop offset="100%" stop-color="#FFF" stop-opacity=".3" />
- </radialGradient>
- <radialGradient
- id="h"
- cx="99.348%"
- cy="89.193%"
- r="203.824%"
- fx="99.348%"
- fy="89.193%"
- gradientTransform="translate(0 -38783246.548) skewY(-90)"
- >
- <stop offset="0%" stop-color="#FFF" stop-opacity=".2" />
- <stop offset="50%" stop-color="#FFF" stop-opacity=".2" />
- <stop offset="100%" stop-color="#FFF" stop-opacity=".3" />
- </radialGradient>
- </defs>
- <path
- fill-opacity=".3"
- d="M209.056 308.305c-2.043 14.93-16.738 26.638-31.432 22.552-20.823-5.658-44.946-14.616-66.634-16.266l-33.317-2.515a22.002 22.002 0 0 1-14.144-6.522L6.167 246.778a21.766 21.766 0 0 1-4.244-24.124s35.36-77.478 36.775-81.485c1.257-4.008 6.13-39.211 8.958-58.07a22.002 22.002 0 0 1 7.072-12.965L122.462 9.47a22.002 22.002 0 0 1 31.903 2.672l57.048 71.978a23.18 23.18 0 0 1 4.872 14.38c0 13.594 1.179 41.646 8.8 59.72a236.756 236.756 0 0 0 27.974 45.732 11.001 11.001 0 0 1 .786 12.258c-4.95 8.408-14.851 24.595-28.76 45.26a111.738 111.738 0 0 0-16.108 46.834h.079Z"
- />
- <path
- fill="#6C31E3"
- d="M209.606 305.79c-2.043 15.009-16.737 26.717-31.432 22.71-20.744-5.737-44.79-14.695-66.555-16.345L78.38 309.64a21.923 21.923 0 0 1-14.144-6.6L6.874 244.106a21.923 21.923 0 0 1-4.243-24.36s35.438-77.792 36.774-81.878c1.336-4.007 6.13-39.289 8.958-58.305a22.002 22.002 0 0 1 7.072-13.044L123.17 5.621a22.002 22.002 0 0 1 31.902 2.75l56.97 72.292a23.338 23.338 0 0 1 4.871 14.38c0 13.673 1.18 41.804 8.723 59.955a238.092 238.092 0 0 0 27.974 45.969 11.001 11.001 0 0 1 .864 12.336c-5.03 8.487-14.851 24.674-28.838 45.497a112.603 112.603 0 0 0-16.03 46.99Z"
- />
- <path
- fill="url(#a)"
- d="M70.365 307.44c26.638-53.983 25.93-92.722 14.537-120.225-10.372-25.459-29.781-41.489-45.025-51.468a19.233 19.233 0 0 1-1.415 4.243L2.631 219.747a21.923 21.923 0 0 0 4.321 24.36l57.284 58.933a23.762 23.762 0 0 0 6.129 4.4Z"
- />
- <path
- fill="url(#b)"
- d="M142.814 197.902a86.025 86.025 0 0 1 21.06 4.793c21.844 8.172 41.724 26.56 58.147 61.999 1.179-2.043 2.357-4.008 3.615-5.894a960.226 960.226 0 0 0 28.838-45.497 11.001 11.001 0 0 0-.786-12.336 238.092 238.092 0 0 1-28.052-45.969c-7.544-18.073-8.644-46.282-8.723-59.955 0-5.186-1.65-10.294-4.871-14.38l-56.97-72.292-.943-1.178c4.165 13.75 3.93 24.752 1.336 34.731-2.357 9.272-6.757 17.68-11.394 26.56-1.571 2.986-3.143 6.05-4.636 9.193a110.01 110.01 0 0 0-12.415 45.576c-.786 19.016 3.064 42.825 15.716 74.65h.078Z"
- />
- <path
- fill="url(#c)"
- d="M142.736 197.902c-12.652-31.824-16.502-55.633-15.716-74.65.786-18.858 6.286-33.002 12.415-45.575l4.715-9.193c4.558-8.88 8.88-17.288 11.315-26.56a61.684 61.684 0 0 0-1.336-34.731c-8.136-8.94-21.96-9.642-30.96-1.572L55.436 66.519a22.002 22.002 0 0 0-7.072 13.044l-8.25 54.69c0 .55-.158 1.022-.236 1.572 15.244 9.901 34.574 25.931 45.025 51.312 2.043 5.029 3.772 10.294 5.029 16.03a157.157 157.157 0 0 1 52.805-5.343v.078Z"
- />
- <path
- fill="url(#d)"
- d="M178.253 328.5c14.616 4.007 29.31-7.701 31.353-22.789a120.225 120.225 0 0 1 12.494-41.017c-16.502-35.44-36.382-53.827-58.148-61.999-23.18-8.643-48.404-5.736-74.021.472 5.736 26.01 2.357 60.034-19.487 104.273 2.436 1.257 5.186 1.965 7.936 2.2l34.496 2.593c18.701 1.336 46.597 11.001 65.377 16.266Z"
- />
- <path
- fill="url(#e)"
- d="M127.177 122.074c-.864 18.859 1.493 40.39 14.144 72.135l-3.929-.393c-11.394-33.081-13.908-50.054-13.044-69.149.786-19.094 6.994-33.789 13.123-46.361 1.571-3.143 5.186-9.037 6.758-12.023 4.557-8.879 7.622-13.515 10.215-21.609 3.772-11.315 2.986-16.658 2.514-22.001 2.908 19.251-8.172 35.988-16.501 53.04a113.939 113.939 0 0 0-13.358 46.361h.078Z"
- />
- <path
- fill="url(#f)"
- d="M88.674 188.551c1.571 3.458 2.907 6.287 3.85 10.608l-3.379.786c-1.336-5.029-2.357-8.643-4.322-12.965-11.472-26.953-29.86-40.861-44.79-51.076 18.074 9.744 36.697 25.066 48.64 52.647Z"
- />
- <path
- fill="url(#g)"
- d="M92.681 202.617c6.286 29.467-.786 66.948-21.609 103.409 17.445-36.146 25.931-70.8 18.859-102.938l2.75-.55v.079Z"
- />
- <path
- fill="url(#h)"
- d="M164.659 199.867c34.181 12.808 47.383 40.86 57.205 64.355-12.18-24.516-29.074-51.626-58.462-61.684-22.317-7.7-41.175-6.758-73.471.55l-.707-3.143c34.26-7.858 52.176-8.8 75.435 0v-.078Z"
- />
- </svg>
- </div>
- <div className="text-center">
- <h3 className="font-semibold text-neutral-900 dark:text-white">Obsidian</h3>
- <p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400 hidden md:block">
- Import notes from your Obsidian vault
- </p>
- </div>
- </div>
- </Card>
-
- <Card
- className="group relative overflow-hidden transition-all hover:shadow-lg flex-1 basis-[calc(33.333%-1rem)]"
- onClick={() => setIsCSVModalOpen(true)}
- >
- <div className="absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100 bg-gradient-to-r from-blue-500/10 to-blue-600/10" />
- <div className="relative z-10 flex flex-col items-center gap-4 p-6">
- <div className="rounded-full bg-white/10 p-3">
- <FileUpIcon className="transition-transform group-hover:scale-110 text-blue-500" />
- </div>
- <div className="text-center">
- <h3 className="font-semibold text-neutral-900 dark:text-white">CSV Upload</h3>
- <p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400 hidden md:block">
- Bulk import URLs from a CSV file
- </p>
- </div>
- </div>
- </Card>
-
- {Object.entries(integrations).map(([key, integration]) => (
- <IntegrationButton
- key={key}
- integration={integration}
- onClick={() => handleIntegrationClick(integration)}
- />
- ))}
- </div>
-
- {loadingIntegration && (
- <LoadingIntegration
- integration={loadingIntegration}
- onCancel={() => setLoadingIntegration(null)}
- />
- )}
-
- <CSVUploadModal isOpen={isCSVModalOpen} onClose={() => setIsCSVModalOpen(false)} />
- <MarkdownUploadModal
- isOpen={isMarkdownModalOpen}
- onClose={() => setIsMarkdownModalOpen(false)}
- />
-
- <div className="mt-8 md:mt-12 text-center">
- <p className="text-sm text-neutral-600 dark:text-neutral-400">
- More integrations coming soon.{" "}
- <a
- href="mailto:[email protected]"
- className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
- >
- Have a suggestion?
- </a>
- </p>
- </div>
- </div>
- );
-}
-
-type ImportStatus = "idle" | "loading" | "success" | "error";
-
-export function IntegrationModals({ integrationId }: { integrationId: string }) {
- const [status, setStatus] = useState<ImportStatus>("idle");
- const [progress, setProgress] = useState<number>(0);
- const [isModalOpen, setIsModalOpen] = useState(true);
- const router = useNavigate();
-
- useEffect(() => {
- if (integrationId === "notion") {
- handleNotionImport();
- }
- }, [integrationId]);
-
- const handleNotionImport = async () => {
- setStatus("loading");
- let toastId: string | number | undefined;
-
- try {
- const eventSource = new EventSource(`/backend/v1/integrations/notion/import`, {
- withCredentials: true,
- });
-
- eventSource.onmessage = (event) => {
- try {
- const data = JSON.parse(event.data);
- if (data.progress) {
- setProgress(data.progress);
- if (data.page) {
- toastId = toast.loading(`Importing ${data.page}... ${data.progress}%`, {
- id: toastId,
- });
- } else {
- toastId = toast.loading(`Importing from Notion: ${data.progress}%`, { id: toastId });
- }
- }
- if (data.warning) {
- toast.warning(data.message);
- }
- if (data.error) {
- throw new Error(data.error);
- }
- if (data.progress === 100) {
- eventSource.close();
- setStatus("success");
- if (toastId) {
- toast.success("Successfully imported from Notion!", { id: toastId });
- } else {
- toast.success("Successfully imported from Notion!");
- }
- setTimeout(() => {
- router("/");
- }, 1500);
- }
- } catch (e) {
- console.error("Error parsing SSE data:", e);
- eventSource.close();
- setStatus("error");
- if (toastId) {
- toast.error(e instanceof Error ? e.message : "Failed to import from Notion", {
- id: toastId,
- });
- } else {
- toast.error(e instanceof Error ? e.message : "Failed to import from Notion");
- }
- }
- };
-
- eventSource.onerror = (error) => {
- console.error("EventSource error:", error);
- eventSource.close();
- setStatus("error");
- if (toastId) {
- toast.error("Failed to connect to import stream", { id: toastId });
- } else {
- toast.error("Failed to connect to import stream");
- }
- };
- } catch (error) {
- console.error("Import error:", error);
- setStatus("error");
- if (toastId) {
- toast.error(error instanceof Error ? error.message : "Failed to import from Notion", {
- id: toastId,
- });
- } else {
- toast.error(error instanceof Error ? error.message : "Failed to import from Notion");
- }
- }
- };
-
- const handleClose = () => {
- setIsModalOpen(false);
- };
-
- if (!integrationId || !isModalOpen) return null;
-
- return (
- <motion.div
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- exit={{ opacity: 0 }}
- className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50"
- onClick={handleClose}
- >
- <motion.div
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- className="relative w-full max-w-sm md:max-w-md rounded-xl bg-white p-6 md:p-8 shadow-xl dark:bg-neutral-800"
- onClick={(e) => e.stopPropagation()}
- >
- <button
- onClick={handleClose}
- className="absolute right-3 top-3 md:right-4 md:top-4 text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors"
- >
- <X className="size-5" />
- </button>
-
- <div className="flex flex-col items-center gap-4 md:gap-6">
- {status === "loading" && (
- <>
- <div className="relative">
- <div className="absolute inset-0 flex items-center justify-center">
- <span className="text-lg font-semibold text-blue-500">{progress}%</span>
- </div>
- <svg className="size-20 md:size-24 -rotate-90 transform">
- <circle
- className="text-neutral-200 dark:text-neutral-700"
- strokeWidth="6"
- stroke="currentColor"
- fill="transparent"
- r="45"
- cx="48"
- cy="48"
- />
- <circle
- className="text-blue-500 transition-all duration-300"
- strokeWidth="6"
- strokeDasharray={283}
- strokeDashoffset={283 - (283 * progress) / 100}
- strokeLinecap="round"
- stroke="currentColor"
- fill="transparent"
- r="45"
- cx="48"
- cy="48"
- />
- </svg>
- </div>
-
- <div className="text-center">
- <h3 className="text-lg md:text-xl font-semibold text-neutral-900 dark:text-white">
- Importing from Notion
- </h3>
- <p className="mt-2 text-sm text-neutral-500 dark:text-neutral-400">
- This may take a few minutes. You can close this modal and let it run in the
- background.
- </p>
- </div>
- </>
- )}
-
- {status === "success" && (
- <>
- <motion.div
- initial={{ scale: 0 }}
- animate={{ scale: 1 }}
- className="rounded-full bg-green-100 p-3 md:p-4 dark:bg-green-900/30"
- >
- <CheckCircle className="size-10 md:size-12 text-green-600 dark:text-green-400" />
- </motion.div>
- <div className="text-center">
- <h3 className="text-lg md:text-xl font-semibold text-neutral-900 dark:text-white">
- Import Complete!
- </h3>
- <p className="mt-2 text-sm text-neutral-500 dark:text-neutral-400">
- Redirecting you to your dashboard...
- </p>
- </div>
- </>
- )}
-
- {status === "error" && (
- <>
- <motion.div
- initial={{ scale: 0 }}
- animate={{ scale: 1 }}
- className="rounded-full bg-red-100 p-3 md:p-4 dark:bg-red-900/30"
- >
- <AlertCircle className="size-10 md:size-12 text-red-600 dark:text-red-400" />
- </motion.div>
- <div className="text-center">
- <h3 className="text-lg md:text-xl font-semibold text-neutral-900 dark:text-white">
- Import Failed
- </h3>
- <p className="mt-2 text-sm text-neutral-500 dark:text-neutral-400">
- Please try again or contact support if the issue persists
- </p>
- <Button onClick={handleNotionImport} variant="outline" className="mt-4">
- Retry Import
- </Button>
- </div>
- </>
- )}
- </div>
- </motion.div>
- </motion.div>
- );
-}
-
-export default Integrations;
diff --git a/apps/web/app/components/memories/MarkdownUploadModal.tsx b/apps/web/app/components/memories/MarkdownUploadModal.tsx
deleted file mode 100644
index 23e6f33b..00000000
--- a/apps/web/app/components/memories/MarkdownUploadModal.tsx
+++ /dev/null
@@ -1,294 +0,0 @@
-import { useEffect, useState } from "react";
-
-import { Button } from "../ui/button";
-import { Dialog, DialogContent, DialogDescription, DialogTitle } from "../ui/dialog";
-import SpacesSelector from "./SpacesSelector";
-
-import { motion } from "framer-motion";
-import { AlertCircle, CheckCircle, Upload } from "lucide-react";
-import { toast } from "sonner";
-
-interface MarkdownUploadModalProps {
- isOpen: boolean;
- onClose: () => void;
-}
-
-export function MarkdownUploadModal({ isOpen, onClose }: MarkdownUploadModalProps) {
- const [files, setFiles] = useState<File[]>([]);
- const [isUploading, setIsUploading] = useState(false);
- const [progress, setProgress] = useState<{
- progress: number;
- processed: number;
- total: number;
- succeeded: number;
- failed: number;
- status: string;
- } | null>(null);
- const [selectedSpaces, setSelectedSpaces] = useState<string[]>([]);
-
- const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
- const selectedFiles = e.target.files;
- if (!selectedFiles) return;
-
- const mdFiles: File[] = [];
- const processFile = async (file: File) => {
- if (file.name.endsWith(".md")) {
- mdFiles.push(file);
- }
- };
-
- // Handle both individual files and directory
- const files = Array.from(selectedFiles);
- await Promise.all(files.map(processFile));
-
- if (mdFiles.length === 0) {
- toast.error("No markdown files found");
- return;
- }
-
- setFiles(mdFiles);
- toast.success(`Found ${mdFiles.length} markdown files`);
- };
-
- const handleUpload = async () => {
- if (files.length === 0) return;
-
- setIsUploading(true);
- const progressToastId = toast.loading("Starting markdown import...");
- let lastToastTime = Date.now();
-
- try {
- // Convert markdown files to content
- const contents = await Promise.all(
- files.map(async (file) => {
- const content = await file.text();
- return {
- content,
- title: file.name.replace(".md", ""),
- type: "note",
- };
- }),
- );
-
- // Send to batch endpoint
- const response = await fetch("/backend/v1/batch-add", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- contents,
- spaces: selectedSpaces,
- }),
- });
-
- if (!response.ok) {
- throw new Error("Failed to start batch upload");
- }
-
- const reader = response.body?.getReader();
- if (!reader) throw new Error("No reader available");
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- const text = new TextDecoder().decode(value);
- const lines = text.split("\n");
-
- for (const line of lines) {
- if (line.startsWith("data: ")) {
- try {
- const data = JSON.parse(line.slice(5));
- setProgress(data);
-
- // Update the main progress toast every 500ms
- const now = Date.now();
- if (now - lastToastTime > 500) {
- toast.loading(
- `Processing files: ${data.processed}/${data.total} (${data.progress}%)`,
- { id: progressToastId },
- );
- lastToastTime = now;
- }
-
- // Show individual file status toasts with a limit
- if (data.status === "success" && data.processed % 5 === 0) {
- toast.success(`Successfully imported ${data.processed} files so far`);
- } else if (data.status === "error") {
- toast.error(`Failed to import: ${data.title} - ${data.error}`);
- }
-
- if (data.status === "complete") {
- toast.success(
- `Import complete! ${data.succeeded} succeeded, ${data.failed} failed`,
- { id: progressToastId, duration: 5000 },
- );
-
- // Wait for 2 seconds before closing
- await new Promise((resolve) => setTimeout(resolve, 2000));
- onClose();
- setFiles([]);
- setProgress(null);
- break;
- }
- } catch (e) {
- console.error("Error parsing SSE data:", e);
- }
- }
- }
- }
- } catch (error) {
- toast.error("Upload failed: " + (error instanceof Error ? error.message : "Unknown error"));
- } finally {
- setIsUploading(false);
- }
- };
-
- return (
- <Dialog open={isOpen} onOpenChange={() => !isUploading && onClose()}>
- <DialogContent className="sm:max-w-md">
- {isUploading ? (
- <div className="text-center">
- {progress ? (
- <>
- <div className="relative">
- <div className="absolute inset-0 flex items-center justify-center">
- <span className="text-lg font-semibold text-blue-500">
- {progress.progress}%
- </span>
- </div>
- <svg className="size-20 md:size-24 -rotate-90 transform">
- <circle
- className="text-neutral-200 dark:text-neutral-700"
- strokeWidth="6"
- stroke="currentColor"
- fill="transparent"
- r="45"
- cx="48"
- cy="48"
- />
- <circle
- className="text-blue-500 transition-all duration-300"
- strokeWidth="6"
- strokeDasharray={283}
- strokeDashoffset={283 - (283 * progress.progress) / 100}
- strokeLinecap="round"
- stroke="currentColor"
- fill="transparent"
- r="45"
- cx="48"
- cy="48"
- />
- </svg>
- </div>
- <DialogTitle className="mt-4">
- Processing Files ({progress.processed}/{progress.total})
- </DialogTitle>
- <DialogDescription className="mt-2">
- {progress.succeeded} succeeded, {progress.failed} failed
- </DialogDescription>
- </>
- ) : (
- <motion.div
- animate={{ rotate: 360 }}
- transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
- className="w-12 h-12 mx-auto"
- >
- <Upload className="h-full w-full text-neutral-900 dark:text-white" />
- </motion.div>
- )}
- </div>
- ) : (
- <div className="space-y-4">
- <DialogTitle>Import from Obsidian</DialogTitle>
- <DialogDescription>
- Upload markdown files from your Obsidian vault. You can select multiple files or drop
- a folder.
- </DialogDescription>
-
- <div className="flex flex-col gap-4">
- {/* Individual Files Selection */}
- <div className="flex flex-col items-center justify-center w-full">
- <label
- htmlFor="markdown-files-upload"
- className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:bg-zinc-900 border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-zinc-800"
- >
- <div className="flex flex-col items-center justify-center pt-5 pb-6">
- <Upload className="w-6 h-6 mb-2 text-gray-500 dark:text-gray-400" />
- <p className="text-sm text-gray-500 dark:text-gray-400">
- <span className="font-semibold">Select Files</span>
- </p>
- <p className="text-xs text-gray-500 dark:text-gray-400">
- Choose individual markdown files
- </p>
- </div>
- <input
- id="markdown-files-upload"
- type="file"
- className="hidden"
- accept=".md"
- multiple
- onChange={handleFileChange}
- />
- </label>
- </div>
-
- <div className="text-center text-sm text-gray-500 dark:text-gray-400">OR</div>
-
- {/* Folder Selection */}
- <div className="flex flex-col items-center justify-center w-full">
- <label
- htmlFor="markdown-folder-upload"
- className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:bg-zinc-900 border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-zinc-800"
- >
- <div className="flex flex-col items-center justify-center pt-5 pb-6">
- <Upload className="w-6 h-6 mb-2 text-gray-500 dark:text-gray-400" />
- <p className="text-sm text-gray-500 dark:text-gray-400">
- <span className="font-semibold">Select Folder</span>
- </p>
- <p className="text-xs text-gray-500 dark:text-gray-400">
- Choose an entire folder
- </p>
- </div>
- <input
- id="markdown-folder-upload"
- type="file"
- className="hidden"
- accept=".md"
- multiple
- // @ts-ignore - webkitdirectory is a non-standard attribute
- webkitdirectory=""
- // @ts-ignore - directory is a non-standard attribute
- directory=""
- onChange={handleFileChange}
- />
- </label>
- </div>
- </div>
-
- {files.length > 0 && (
- <>
- <div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-zinc-900 rounded border border-gray-200 dark:border-gray-700">
- <CheckCircle className="h-4 w-4 text-green-500" />
- <span className="text-sm text-gray-700 dark:text-gray-300">
- {files.length} markdown files selected
- </span>
- </div>
-
- <SpacesSelector selectedSpaces={selectedSpaces} onChange={setSelectedSpaces} />
-
- <div className="flex justify-end gap-2">
- <Button variant="outline" onClick={onClose}>
- Cancel
- </Button>
- <Button onClick={handleUpload}>Import</Button>
- </div>
- </>
- )}
- </div>
- )}
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/apps/web/app/components/memories/MemoriesPage.tsx b/apps/web/app/components/memories/MemoriesPage.tsx
deleted file mode 100644
index da12427e..00000000
--- a/apps/web/app/components/memories/MemoriesPage.tsx
+++ /dev/null
@@ -1,423 +0,0 @@
-import { createContext, memo, useCallback, useContext, useMemo, useState } from "react";
-import { useParams } from "react-router-dom";
-
-import { Button } from "../ui/button";
-import AddMemory from "./AddMemory";
-import SharedCard from "./SharedCard";
-
-import { Trash2 } from "lucide-react";
-import { Masonry, useInfiniteLoader } from "masonic";
-import { useHydrated } from "remix-utils/use-hydrated";
-import { toast } from "sonner";
-import { useMemories } from "~/lib/hooks/use-memories";
-import { useSpaces } from "~/lib/hooks/use-spaces";
-import { Memory } from "~/lib/types/memory";
-import { cn } from "~/lib/utils";
-
-const variants = ["All Memories", "Web pages", "Tweets", "Documents", "Spaces", "Notes"] as const;
-type Variant = (typeof variants)[number];
-
-interface MemoriesPageProps {
- showAddButtons?: boolean;
- isSpace?: boolean;
-}
-
-interface SelectionContextType {
- isSelectionMode: boolean;
- selectedItems: Set<string>;
- toggleSelection: (uuid: string) => void;
-}
-
-const SelectionContext = createContext<SelectionContextType | null>(null);
-
-function MemoriesPage({ showAddButtons = true, isSpace = false }: MemoriesPageProps) {
- const isHydrated = useHydrated();
- const { spaceId } = useParams();
- const [selectedVariant, setSelectedVariant] = useState<Variant>("All Memories");
- const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
- const [isSelectionMode, setIsSelectionMode] = useState(false);
- const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
- const [isDeleting, setIsDeleting] = useState(false);
-
- const { memories, isLoading, loadMore, hasMore, mutate } = useMemories(0, 20, spaceId);
-
- const { spaces } = useSpaces();
-
- const handleToggleSelection = useCallback((uuid: string) => {
- setSelectedItems((prev) => {
- const newSet = new Set(prev);
- if (newSet.has(uuid)) {
- newSet.delete(uuid);
- } else {
- newSet.add(uuid);
- }
- return newSet;
- });
- }, []);
-
- const handleBatchDelete = useCallback(async () => {
- if (selectedItems.size === 0) return;
-
- const confirmed = window.confirm(
- `Are you sure you want to delete ${selectedItems.size} item${selectedItems.size > 1 ? "s" : ""}?`,
- );
-
- if (!confirmed) return;
-
- setIsDeleting(true);
- try {
- const response = await fetch("/backend/v1/memories/batch-delete", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ ids: Array.from(selectedItems) }),
- credentials: "include",
- });
-
- if (!response.ok) {
- throw new Error("Failed to delete items");
- }
-
- const data = (await response.json()) as { success: boolean; deletedCount: number };
- toast.success(
- `Successfully deleted ${data.deletedCount} item${data.deletedCount > 1 ? "s" : ""}`,
- );
-
- // Reset selection mode and clear selected items
- setIsSelectionMode(false);
- setSelectedItems(new Set());
-
- // Refresh the memories list
- mutate();
- } catch (error) {
- toast.error(error instanceof Error ? error.message : "Failed to delete items");
- } finally {
- setIsDeleting(false);
- }
- }, [selectedItems, mutate]);
-
- const loadMoreItems = useCallback(
- (startIndex: number, stopIndex: number) => {
- if (!hasMore || isLoading) {
- return Promise.resolve();
- }
- return loadMore();
- },
- [hasMore, isLoading, loadMore],
- );
-
- const maybeLoadMore = useInfiniteLoader(loadMoreItems, {
- isItemLoaded: (index) => !hasMore || index < (memories?.length || 0),
- minimumBatchSize: 20,
- threshold: 6,
- });
-
- // Memoize space items transformation
- const spaceItems = useMemo(() => {
- if (spaceId) return [];
- return spaces
- .filter((space) => space.uuid !== "<HOME>")
- .map((space) => ({
- id: space.uuid,
- type: "space",
- content: space.name,
- createdAt: new Date(space.createdAt),
- description: null,
- ogImage: null,
- title: space.name,
- url: `/space/${space.uuid}`,
- uuid: space.uuid,
- updatedAt: null,
- raw: null,
- userId: space.ownerId,
- isSuccessfullyProcessed: true,
- }));
- }, [spaces, spaceId]);
-
- // Memoize filtered memories
- const filteredMemories = useMemo(() => {
- const baseMemories = memories || [];
- switch (selectedVariant) {
- case "Web pages":
- return baseMemories.filter((m) => m.type === "page");
- case "Tweets":
- return baseMemories.filter((m) => m.type === "tweet");
- case "Documents":
- return baseMemories.filter((m) => m.type === "document");
- case "Spaces":
- return spaceId ? baseMemories : [];
- case "Notes":
- return baseMemories.filter((m) => m.type === "note");
- default:
- return baseMemories;
- }
- }, [memories, selectedVariant, spaceId]);
-
- // Memoize add button item
- const addButtonItem = useMemo(() => {
- if (!showAddButtons) return [];
- return [
- {
- id: "add-button",
- type: "note",
- content: null,
- createdAt: new Date(),
- description: null,
- ogImage: null,
- title: null,
- url: null,
- uuid: "add-button",
- updatedAt: null,
- raw: null,
- userId: "0",
- isSuccessfullyProcessed: true,
- },
- ];
- }, [showAddButtons]);
-
- // Combine items and generate key
- const { items, key } = useMemo(() => {
- const shouldShowSpaces =
- !isSpace && (selectedVariant === "All Memories" || selectedVariant === "Spaces");
- const allItems = [
- ...addButtonItem,
- ...(shouldShowSpaces ? spaceItems : []),
- ...filteredMemories,
- ];
-
- return {
- items: allItems,
- key: `${selectedVariant || "default"}-${spaceId || "no-space"}-${allItems.length}`,
- };
- }, [addButtonItem, spaceItems, filteredMemories, selectedVariant, spaceId, isSpace]);
-
- const selectionContextValue = useMemo(
- () => ({
- isSelectionMode,
- selectedItems,
- toggleSelection: handleToggleSelection,
- }),
- [isSelectionMode, selectedItems, handleToggleSelection],
- );
-
- const MemoizedSharedCard = memo(
- ({
- data,
- index,
- showAddButtons,
- isSpace,
- }: {
- data: Memory;
- index: number;
- showAddButtons: boolean;
- isSpace: boolean;
- }) => {
- const selection = useContext(SelectionContext);
-
- if (index === 0 && showAddButtons) {
- return <AddMemory isSpace={isSpace} />;
- }
- if (data.type === "space") {
- return <SharedCard data={data} />;
- }
- return (
- <SharedCard
- data={data}
- isSelectionMode={selection?.isSelectionMode ?? false}
- isSelected={selection?.selectedItems.has(data.uuid) ?? false}
- onToggleSelect={() => selection?.toggleSelection(data.uuid)}
- />
- );
- },
- (prevProps, nextProps) => {
- // Custom comparison function for memo
- return prevProps.data.uuid === nextProps.data.uuid;
- },
- );
-
- MemoizedSharedCard.displayName = "MemoizedSharedCard";
-
- const renderCard = useCallback(
- ({ data, index }: { data: Memory; index: number }) => (
- <MemoizedSharedCard
- data={data}
- index={index}
- showAddButtons={showAddButtons}
- isSpace={isSpace}
- />
- ),
- [showAddButtons, isSpace],
- );
-
- const handleVariantClick = useCallback((variant: Variant) => {
- setSelectedVariant(variant);
- setIsMobileMenuOpen(false);
- if (variant === "Spaces") {
- setIsSelectionMode(false);
- setSelectedItems(new Set());
- }
- }, []);
-
- const SelectionControls = useMemo(
- () => (
- <div className="flex items-center gap-2 mb-4">
- {selectedVariant !== "Spaces" && (
- <>
- <Button
- variant={isSelectionMode ? "secondary" : "outline"}
- onClick={() => {
- setIsSelectionMode(!isSelectionMode);
- if (!isSelectionMode) {
- setSelectedItems(new Set());
- }
- }}
- >
- {isSelectionMode ? "Cancel Selection" : "Select Items"}
- </Button>
- {isSelectionMode && (
- <>
- <Button
- variant="outline"
- onClick={() => {
- // Get all non-space items' UUIDs
- const allUuids = filteredMemories
- .filter((item) => item.type !== "space")
- .map((item) => item.uuid);
-
- // If all items are selected, clear selection
- if (allUuids.every((uuid) => selectedItems.has(uuid))) {
- setSelectedItems(new Set());
- } else {
- // Otherwise, select all items
- setSelectedItems(new Set(allUuids));
- }
- }}
- >
- {filteredMemories.every(
- (item) => item.type === "space" || selectedItems.has(item.uuid),
- )
- ? "Deselect All"
- : "Select All"}
- </Button>
- <Button
- variant="destructive"
- onClick={handleBatchDelete}
- disabled={selectedItems.size === 0 || isDeleting}
- >
- <Trash2 className="w-4 h-4 mr-2" />
- {isDeleting ? "Deleting..." : `Delete Selected (${selectedItems.size})`}
- </Button>
- </>
- )}
- </>
- )}
- </div>
- ),
- [
- isSelectionMode,
- selectedItems.size,
- isDeleting,
- handleBatchDelete,
- selectedVariant,
- filteredMemories,
- selectedItems,
- ],
- );
-
- const MobileVariantButton = useMemo(
- () => (
- <Button
- className="md:hidden w-full mb-2"
- onClick={() => setIsMobileMenuOpen((prev) => !prev)}
- >
- {selectedVariant} ▼
- </Button>
- ),
- [selectedVariant],
- );
-
- const MobileVariantMenu = useMemo(
- () => (
- <div
- className={cn(
- "md:hidden overflow-hidden transition-all duration-300 rounded-lg bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60",
- isMobileMenuOpen ? "max-h-[500px] shadow-lg" : "max-h-0",
- )}
- >
- <div className="flex flex-col gap-2 p-2">
- {variants
- .filter((variant) => !(isSpace && variant === "Spaces"))
- .map((variant) => (
- <Button
- key={variant}
- variant="ghost"
- onClick={() => handleVariantClick(variant)}
- className={cn(
- "text-muted-foreground hover:bg-accent hover:text-foreground text-base w-full py-4 rounded-lg transition-colors",
- selectedVariant === variant
- ? "bg-accent text-foreground font-medium shadow-sm"
- : "hover:shadow-sm",
- )}
- >
- {variant}
- </Button>
- ))}
- </div>
- </div>
- ),
- [selectedVariant, isMobileMenuOpen, handleVariantClick, isSpace],
- );
-
- const DesktopVariantMenu = useMemo(
- () => (
- <div className="hidden md:flex">
- {variants
- .filter((variant) => !(isSpace && variant === "Spaces"))
- .map((variant) => (
- <Button
- key={variant}
- variant="ghost"
- onClick={() => setSelectedVariant(variant)}
- className={cn(
- "text-muted-foreground hover:bg-accent hover:text-foreground text-base",
- selectedVariant === variant ? "bg-accent text-foreground" : "",
- )}
- >
- {variant}
- </Button>
- ))}
- </div>
- ),
- [selectedVariant, isSpace],
- );
-
- if (!isHydrated) return null;
-
- return (
- <SelectionContext.Provider value={selectionContextValue}>
- <div className="min-h-screen p-2 md:p-4">
- <div className="mb-4">
- {MobileVariantButton}
- {MobileVariantMenu}
- {DesktopVariantMenu}
- </div>
-
- {SelectionControls}
-
- <Masonry
- key={key}
- id="memories-masonry"
- items={items}
- // @ts-ignore
- render={renderCard}
- columnGutter={16}
- columnWidth={Math.min(270, window.innerWidth - 32)}
- onRender={maybeLoadMore}
- />
-
- {isLoading && <div className="py-4 text-center text-muted-foreground">Loading more...</div>}
- </div>
- </SelectionContext.Provider>
- );
-}
-
-export default MemoriesPage;
diff --git a/apps/web/app/components/memories/SharedCard.tsx b/apps/web/app/components/memories/SharedCard.tsx
deleted file mode 100644
index 04ba769b..00000000
--- a/apps/web/app/components/memories/SharedCard.tsx
+++ /dev/null
@@ -1,892 +0,0 @@
-import * as ReactTweet from "react-tweet";
-import { memo, useCallback, useEffect, useMemo, useState } from "react";
-import { useInView } from "react-intersection-observer";
-import { TweetSkeleton } from "react-tweet";
-
-import { useNavigate, useParams } from "@remix-run/react";
-
-import { NotionIcon } from "../icons/IntegrationIcons";
-import { CustomTwitterComp } from "../twitter/render-tweet";
-import Loader from "../ui/Loader";
-import { Button, ButtonProps } from "../ui/button";
-import { Card } from "../ui/card";
-import { Checkbox } from "../ui/checkbox";
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
-} from "../ui/command";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuPortal,
- DropdownMenuSeparator,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuTrigger,
-} from "../ui/dropdown-menu";
-
-import { FileIcon } from "@radix-ui/react-icons";
-import { SpaceIcon } from "@supermemory/shared/icons";
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { FastAverageColor } from "fast-average-color";
-import { MenuIcon, TrashIcon } from "lucide-react";
-import { FolderIcon, MoreVertical, Trash } from "lucide-react";
-import { toast } from "sonner";
-import { pastelColors } from "~/lib/constants/pastelColors";
-import { typeIcons } from "~/lib/constants/typeIcons";
-import { ExtraSpaceMetaData, fetchSpaces } from "~/lib/hooks/use-spaces";
-import { useTextOverflow } from "~/lib/hooks/use-text-overflow";
-import { Memory, WebsiteMetadata } from "~/lib/types/memory";
-import { cn } from "~/lib/utils";
-
-const { useTweet } = ReactTweet;
-
-export const typeDecider = (content: string) => {
- try {
- // If it's a tweet URL
- if (content.match(/https?:\/\/(x\.com|twitter\.com)\/[\w]+\/[\w]+\/[\d]+/)) {
- return "tweet";
- }
- // If it's a document
- if (content.match(/\.(pdf|doc|docx|txt|rtf|odt|md)/i)) {
- return "document";
- }
- // If it's a Notion URL
- if (content.match(/https?:\/\/(www\.)?notion\.so/)) {
- return "notion";
- }
- // If it's any other URL
- if (content.match(/^(https?:\/\/)?(www\.)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(\/.*)?$/i)) {
- return "page";
- }
- // Otherwise it's a note
- return "note";
- } catch (e) {
- console.error("[Decide Type Error]", e);
- return "note";
- }
-};
-
-// Create a cache to store fetched metadata
-const metadataCache: Record<string, WebsiteMetadata> = {};
-
-const renderContent = {
- tweet: ({ data: ourData }: { data: Memory }) => {
- // If content starts with <raw>, parse the JSON inside
- if (ourData.content?.startsWith("<raw>")) {
- const rawContent = ourData.content.replace(/<\/?raw>/g, "");
- try {
- const parsedData = JSON.parse(rawContent);
- return <CustomTwitterComp tweet={parsedData} />;
- } catch (error) {
- console.error("Error parsing raw tweet data:", error);
- return (
- <div className="p-4 text-muted-foreground">Error parsing tweet data. {rawContent}</div>
- );
- }
- }
-
- // Otherwise use normal tweet ID flow
- const tweetId = ourData.url?.match(/\/status\/(\d+)/)?.[1] ?? ourData.url;
- const { data, error } = useTweet(tweetId ?? undefined);
-
- if (error) {
- console.error("Error parsing tweet:", error);
- console.log("Tweet ID:", tweetId);
- return (
- <div className="p-4 text-muted-foreground">
- Error parsing tweet. {tweetId} {JSON.stringify(error)}
- </div>
- );
- }
-
- if (!data || typeof data == "undefined") {
- return <TweetSkeleton />;
- }
-
- return <CustomTwitterComp tweet={data} />;
- },
-
- page: ({ data }: { data: Memory }) => (
- <WebsiteCard
- id={data.uuid}
- url={data.url ?? ""}
- title={data.title}
- description={data.description}
- image={data.ogImage}
- />
- ),
-
- note: ({ data }: { data: Memory }) => {
- const { contentRef, showFade } = useTextOverflow(data.content ?? "", 5);
-
- // Use a constant color for id 0, otherwise select based on content length
- const color = useMemo(
- () =>
- data.id === 0
- ? pastelColors[0]
- : pastelColors[(data.content?.length ?? 0) % pastelColors.length],
- [data.content, data.id],
- );
-
- const formattedDate = new Date(data.createdAt).toLocaleDateString("en-US", {
- month: "short",
- day: "numeric",
- year: "numeric",
- });
-
- return (
- <div
- style={{ backgroundColor: color }}
- className="text-lg rounded-2xl relative p-8 font-semibold"
- >
- <p ref={contentRef} className="text-card-foreground line-clamp-4">
- {data.content}
- </p>
- {showFade && <div className="fade-overlay"></div>}
-
- <div className="flex justify-between items-center mt-8 ">
- <div className="text-slate-500 font-normal text-base">{formattedDate}</div>
-
- <a className="rounded-full bg-black text-white p-2" href="#">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 20 20"
- fill="currentColor"
- className="size-3"
- >
- <path d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z" />
- </svg>
- </a>
- </div>
- </div>
- );
- },
-
- notion: ({ data }: { data: Memory }) => {
- const { contentRef, showFade } = useTextOverflow(data.content ?? "", 5);
- const formattedDate = new Date(data.createdAt).toLocaleDateString("en-US", {
- month: "short",
- day: "numeric",
- year: "numeric",
- });
-
- return (
- <div className="text-lg rounded-2xl relative p-8 font-semibold bg-[#F7F6F3] dark:bg-neutral-900">
- <div className="flex items-center gap-2 mb-4">
- <NotionIcon className="w-5 h-5" />
- <span className="text-sm font-medium text-gray-800 dark:text-gray-200">Notion Page</span>
- </div>
-
- <p
- ref={contentRef}
- className="text-card-foreground line-clamp-4 text-gray-900 dark:text-gray-100"
- >
- {data.content}
- </p>
- {showFade && <div className="fade-overlay"></div>}
-
- <div className="flex justify-between items-center mt-8">
- <div className="text-gray-500 dark:text-gray-400 font-normal text-base">
- {formattedDate}
- </div>
-
- <a
- href={data.url ?? "#"}
- target="_blank"
- rel="noopener noreferrer"
- className="rounded-full bg-black dark:bg-white text-white dark:text-black p-2 hover:opacity-80 transition-opacity"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 20 20"
- fill="currentColor"
- className="size-3"
- >
- <path
- fillRule="evenodd"
- d="M5.22 14.78a.75.75 0 001.06 0l7.22-7.22v5.69a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75h-7.5a.75.75 0 000 1.5h5.69l-7.22 7.22a.75.75 0 000 1.06z"
- clipRule="evenodd"
- />
- </svg>
- </a>
- </div>
- </div>
- );
- },
-
- space: ({ data }: { data: Memory & Partial<ExtraSpaceMetaData> }) => {
- return (
- <a
- href={data.url ?? ""}
- className="flex flex-col gap-2 p-6 bg-white dark:bg-neutral-800 border border-gray-200 dark:border-gray-800 rounded-3xl"
- >
- <div className="flex items-center gap-2 text-gray-600 dark:text-gray-300">
- <SpaceIcon className="h-4 w-4" />
- <span className="text-sm font-medium">Space</span>
- </div>
-
- <h3 className="text-lg font-semibold text-gray-900 dark:text-white mt-2">{data.content}</h3>
-
- <div className="flex flex-col gap-3 mt-4">
- <div className="flex items-center gap-2">
- {data.permissions?.isPublic ? (
- <div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 font-medium">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 20 20"
- fill="currentColor"
- className="w-4 h-4"
- >
- <path
- fillRule="evenodd"
- d="M4.5 2A1.5 1.5 0 0 0 3 3.5v13A1.5 1.5 0 0 0 4.5 18h11a1.5 1.5 0 0 0 1.5-1.5V7.621a1.5 1.5 0 0 0-.44-1.06l-4.12-4.122A1.5 1.5 0 0 0 11.378 2H4.5Zm7.75 9.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
- clipRule="evenodd"
- />
- </svg>
- Public Space
- </div>
- ) : (
- <div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 font-medium">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 20 20"
- fill="currentColor"
- className="w-4 h-4"
- >
- <path
- fillRule="evenodd"
- d="M10 1a4.5 4.5 0 0 0-4.5 4.5V9H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-.5V5.5A4.5 4.5 0 0 0 10 1Zm3 8V5.5a3 3 0 1 0-6 0V9h6Z"
- clipRule="evenodd"
- />
- </svg>
- Private Space
- </div>
- )}
-
- {data.favorited && (
- <div className="flex items-center gap-1 px-3 py-1.5 rounded-full bg-yellow-50 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400 font-medium">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 20 20"
- fill="currentColor"
- className="w-4 h-4"
- >
- <path
- fillRule="evenodd"
- d="M10.868 2.884c-.321-.772-1.415-.772-1.736 0l-1.83 4.401-4.753.381c-.833.067-1.171 1.107-.536 1.651l3.62 3.102-1.106 4.637c-.194.813.691 1.456 1.405 1.02L10 15.591l4.069 2.485c.713.436 1.598-.207 1.404-1.02l-1.106-4.637 3.62-3.102c.635-.544.297-1.584-.536-1.65l-4.752-.382-1.831-4.401z"
- clipRule="evenodd"
- />
- </svg>
- Favorited
- </div>
- )}
- </div>
-
- {data.owner ? (
- <>
- <div className="flex flex-wrap gap-2">
- {data.permissions?.canRead && !data.permissions.canEdit && (
- <div className="text-xs px-2 py-1 rounded bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400">
- Can Read
- </div>
- )}
- {data.permissions?.canEdit && (
- <div className="text-xs px-2 py-1 rounded bg-purple-50 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400">
- Can Edit
- </div>
- )}
- </div>
-
- <div className="flex items-center gap-2 mt-1">
- {data.owner.profileImage ? (
- <img
- src={data.owner.profileImage}
- alt={data.owner.name}
- className="w-6 h-6 rounded-full"
- />
- ) : (
- <div className="w-6 h-6 rounded-full bg-gray-200 dark:bg-gray-700" />
- )}
- <span className="text-sm text-gray-600 dark:text-gray-300">{data.owner.name}</span>
- </div>
- </>
- ) : null}
- </div>
- </a>
- );
- },
-
- document: ({ data }: { data: Memory }) => {
- // just render the document like normally
- // and get the name from the url
-
- // TODO: This can be improved
- return (
- <a
- href={`/content/${data.id}`}
- className="block p-4 rounded-3xl border border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
- >
- <div className="flex items-center gap-3 text-gray-600 dark:text-gray-300">
- <FileIcon className="h-5 w-5 flex-shrink-0" />
- <div className="flex flex-col min-w-0">
- <span className="text-sm font-medium text-gray-500 dark:text-gray-400">Document</span>
- <span className="text-base font-medium text-gray-900 dark:text-white mt-0.5">
- {decodeURIComponent(data.url?.split("/").pop() ?? "")}
- </span>
- </div>
- </div>
- </a>
- );
- },
-};
-
-async function fetchWebsiteMetadata(url: string): Promise<WebsiteMetadata> {
- // Check if metadata is already in cache
- if (metadataCache[url]) {
- return metadataCache[url];
- }
-
- // Add http:// if not present
- if (!url.startsWith("http://") && !url.startsWith("https://")) {
- url = "http://" + url;
- }
-
- try {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
-
- const response = await fetch(`/api/metadata?url=${encodeURIComponent(url)}`, {
- signal: controller.signal,
- });
-
- clearTimeout(timeoutId);
-
- if (!response.ok) {
- throw new Error("Failed to fetch metadata");
- }
-
- const metadata = (await response.json()) as WebsiteMetadata;
-
- if (metadata.image) {
- try {
- const fac = new FastAverageColor();
- const color = await fac.getColorAsync(metadata.image, {
- algorithm: "dominant",
- crossOrigin: "anonymous",
- mode: "speed",
- });
- metadata.dominantColor = color.hex;
- metadata.isDark = color.isDark;
- } catch (error) {
- console.error("Error getting dominant color:", error);
- // Fallback to a default color if there's an error
- metadata.dominantColor = "#CFCFCF";
- metadata.isDark = false;
- }
- } else {
- metadata.dominantColor = "#CFCFCF";
- metadata.isDark = false;
- }
-
- // Store the fetched metadata in cache
- metadataCache[url] = metadata;
- return metadata;
- } catch (error) {
- console.error("Error fetching metadata:", error);
- return {
- title: "Unknown",
- description: "No description available",
- image: "",
- dominantColor: "#CFCFCF",
- isDark: false,
- };
- }
-}
-
-const WebsiteCard = memo(
- ({
- url,
- title,
- description,
- image,
- id,
- }: {
- url: string;
- title?: string | null;
- description?: string | null;
- image?: string | null;
- id: string;
- }) => {
- // Memoize domain extraction to avoid recalculation
- const domain = useMemo(() => {
- try {
- let formattedUrl = url;
- if (!formattedUrl.startsWith("http")) {
- formattedUrl = "http://" + formattedUrl;
- }
- return new URL(formattedUrl).hostname.replace(/^www\./, "");
- } catch {
- return url;
- }
- }, [url]);
-
- // Memoize initial color based on URL
- const initialColor = useMemo(() => {
- if (!image) {
- const hash = url.split("").reduce((acc, char) => {
- return char.charCodeAt(0) + ((acc << 5) - acc);
- }, 0);
- return `hsl(${hash % 360}, 70%, 85%)`; // Slightly lighter base color
- }
- return "#f0f0f0"; // Lighter default color
- }, [url, image]);
-
- const [dominantColor, setDominantColor] = useState(initialColor);
- const [isDark, setIsDark] = useState(false);
- // Only calculate dominant color when component is in view
- useEffect(() => {
- if (image) {
- const fac = new FastAverageColor();
- fac
- .getColorAsync(image, {
- algorithm: "dominant",
- crossOrigin: "anonymous",
- mode: "speed",
- })
- .then((color) => {
- setDominantColor(color.hex);
- setIsDark(color.isDark);
- })
- .catch((error) => {
- console.error("Error getting dominant color:", error);
- });
- }
- }, [image]);
-
- const displayTitle = title || domain;
- const displayDescription = description || `Saved from ${domain}`;
-
- return (
- <div className="overflow-hidden rounded-xl shadow-sm hover:shadow-md transition-shadow duration-200">
- <div className="relative">
- {image && (
- <div className="relative h-40">
- <img
- src={image}
- alt={displayTitle}
- className="absolute inset-0 w-full h-full object-cover"
- loading="lazy"
- style={{ backgroundColor: dominantColor }}
- />
- <div
- className="absolute inset-0"
- style={{
- background: `linear-gradient(to bottom, transparent 40%, ${dominantColor} 100%)`,
- }}
- />
- </div>
- )}
- <div
- className={cn(
- "p-5 relative",
- !image && "rounded-lg",
- isDark ? "text-white/90" : "text-black/90",
- )}
- style={{
- backgroundColor: dominantColor,
- marginTop: image ? "-2.5rem" : 0,
- }}
- >
- <h3 className="text-lg font-semibold tracking-tight">{displayTitle}</h3>
- <p className="mt-2 line-clamp-2 text-sm opacity-80">{displayDescription}</p>
- <a
- href={`/content/${id}`}
- className="mt-3 inline-flex items-center gap-1 text-sm hover:underline opacity-70 hover:opacity-100 transition-opacity"
- style={{
- color: isDark ? "white" : "black",
- }}
- >
- <span>{domain}</span>
- <svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor">
- <path
- d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- />
- </svg>
- </a>
- </div>
- </div>
- </div>
- );
- },
-);
-
-export function FetchAndRenderContent({ content }: { content: string }) {
- const [memory, setMemory] = useState<Memory | null>(null);
- const [isLoading, setIsLoading] = useState(false);
-
- useEffect(() => {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
-
- (async () => {
- const type = typeDecider(content);
-
- if (type !== "page") {
- const item: Memory = {
- type: type,
- content: content,
- createdAt: new Date(),
- description: null,
- id: 0,
- ogImage: null,
- title: null,
- url: null,
- uuid: "",
- updatedAt: null,
- raw: null,
- userId: 0,
- isSuccessfullyProcessed: true,
- errorMessage: null,
- contentHash: null,
- metadata: null,
- };
- setMemory(item);
- return;
- }
-
- try {
- setIsLoading(true);
- const metadata = await fetchWebsiteMetadata(content);
-
- const item: Memory = {
- type: type,
- content: content,
- createdAt: new Date(),
- description: metadata?.description ?? null,
- id: 0,
- ogImage: metadata?.image ?? null,
- title: metadata?.title ?? null,
- url: content,
- uuid: "",
- updatedAt: null,
- raw: null,
- userId: 0,
- isSuccessfullyProcessed: true,
- errorMessage: null,
- contentHash: null,
- metadata: null,
- };
- setMemory(item);
- } catch (error) {
- // If metadata fetch fails, create memory with basic URL info
- const item: Memory = {
- type: type,
- content: content,
- createdAt: new Date(),
- description: null,
- id: 0,
- ogImage: null,
- title: content,
- url: content,
- uuid: "",
- updatedAt: null,
- raw: null,
- userId: 0,
- isSuccessfullyProcessed: false,
- errorMessage: null,
- contentHash: null,
- metadata: null,
- };
- setMemory(item);
- } finally {
- setIsLoading(false);
- clearTimeout(timeoutId);
- }
- })();
-
- return () => {
- clearTimeout(timeoutId);
- controller.abort();
- };
- }, [content]);
-
- if (isLoading) {
- return <div className="p-4 text-muted-foreground">Loading website metadata...</div>;
- }
-
- return memory ? <SharedCard data={memory} /> : null;
-}
-
-interface SharedCardProps {
- data: Memory;
- isSelectionMode?: boolean;
- isSelected?: boolean;
- onToggleSelect?: () => void;
-}
-
-export default function SharedCard({
- data,
- isSelectionMode,
- isSelected,
- onToggleSelect,
-}: SharedCardProps) {
- const queryClient = useQueryClient();
- const navigate = useNavigate();
-
- // Flatten the data if it's a nested array and get the first item
- if (Array.isArray(data)) {
- console.log("weird data here, will try flattening.", data);
- data = data.flat(Infinity)[0];
- }
-
- const ContentRenderer =
- renderContent[data.type as keyof typeof renderContent] ||
- (() => {
- console.log("SharedCard data", data);
- return (
- <div>
- Unsupported content type: {typeof data.type === "undefined" ? "undefined" : data.type}
- </div>
- );
- });
-
- // Delete mutation
- const deleteMutation = useMutation({
- mutationFn: async (id: number) => {
- const response = await fetch(`/backend/v1/memories/${id}`, {
- method: "DELETE",
- credentials: "include",
- });
- if (!response.ok) {
- throw new Error("Failed to delete memory");
- }
- return response.json();
- },
- onMutate: async (id) => {
- // Cancel outgoing refetches
- await queryClient.cancelQueries({ queryKey: ["memories"] });
-
- // Snapshot the previous value
- const previousMemories = queryClient.getQueryData(["memories"]);
-
- // Optimistically remove the memory
- queryClient.setQueryData(["memories"], (old: any) => {
- return old?.filter((memory: Memory) => memory.id !== id);
- });
-
- return { previousMemories };
- },
- onError: (err, variables, context) => {
- // Revert the optimistic update
- queryClient.setQueryData(["memories"], context?.previousMemories);
- toast.error("Failed to delete memory");
- },
- onSuccess: () => {
- toast.success("Memory deleted successfully");
- queryClient.invalidateQueries({ queryKey: ["memories"] });
- queryClient.invalidateQueries({ queryKey: ["spaces"] });
- },
- });
-
- // Move to space mutation
- const moveToSpaceMutation = useMutation({
- mutationFn: async ({ spaceId, documentId }: { spaceId: string; documentId: string }) => {
- const response = await fetch("/backend/v1/spaces/moveContent", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- credentials: "include",
- body: JSON.stringify({ spaceId, documentId }),
- });
- if (!response.ok) {
- throw new Error("Failed to move memory");
- }
- return response.json() as Promise<{ spaceId: string }>;
- },
- onError: (err) => {
- toast.error("Failed to move memory to space");
- },
- onSuccess: ({ spaceId }: { spaceId: string }) => {
- toast.success("Memory moved successfully");
- queryClient.invalidateQueries({ queryKey: ["memories"] });
- queryClient.invalidateQueries({ queryKey: ["spaces"] });
- if (spaceId === "<HOME>") {
- return navigate("/");
- }
- return navigate(`/space/${spaceId}`);
- },
- });
-
- const handleDelete = (e?: Event) => {
- if (e) {
- e.preventDefault();
- e.stopPropagation();
- }
- if (window.confirm("Are you sure you want to delete this memory?")) {
- deleteMutation.mutate(data.id);
- }
- };
-
- const handleMoveToSpace = (spaceId: string) => {
- moveToSpaceMutation.mutate({
- spaceId,
- documentId: data.uuid,
- });
- };
-
- const handleClick = (e: React.MouseEvent) => {
- if (isSelectionMode && onToggleSelect) {
- e.preventDefault();
- onToggleSelect();
- return;
- }
- };
-
- return (
- <Card
- className={cn(
- "group relative overflow-hidden rounded-3xl border",
- (() => {
- switch (data.type) {
- case "note":
- return "border-primary/30";
- case "document":
- return "border-secondary/30";
- default:
- return "border-accent/30";
- }
- })(),
- isSelectionMode && "hover:ring-2 hover:ring-primary",
- isSelected && "ring-2 ring-primary bg-primary/5",
- )}
- onClick={handleClick}
- >
- {isSelectionMode ? (
- <div className="absolute right-2 top-2 z-50">
- <Checkbox checked={isSelected} onCheckedChange={onToggleSelect} />
- </div>
- ) : (
- data.type !== "space" && (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <div className="absolute right-3 top-3 z-10 flex items-center gap-1.5 rounded-full bg-white/70 dark:bg-gray-900/70 px-2 py-1 text-xs font-medium text-gray-600 dark:text-gray-300 shadow-sm/50 backdrop-blur-[2px] border border-gray-200/30 dark:border-gray-700/30 opacity-0 group-hover:opacity-100 transition-opacity">
- <MenuIcon className="h-4 w-4" />
- </div>
- </DropdownMenuTrigger>
- <DropdownMenuContent>
- <DropdownMenuSub>
- <DropdownMenuSubTrigger>Move to ...</DropdownMenuSubTrigger>
- <DropdownMenuPortal>
- <MemoizedSpaceSelector contentId={data.id} onSelect={handleMoveToSpace} />
- </DropdownMenuPortal>
- </DropdownMenuSub>
- <DropdownMenuItem onSelect={(e) => handleDelete(e)} asChild>
- <Button className="w-full gap-2 flex" variant={"destructive"}>
- <TrashIcon className="h-4 w-4" /> Delete
- </Button>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- )
- )}
- <ContentRenderer data={data} />
- </Card>
- );
-}
-
-export const SpaceSelector = function SpaceSelector({
- contentId,
- onSelect,
-}: {
- contentId: number;
- onSelect: (spaceId: string) => void;
-}) {
- const [search, setSearch] = useState("");
- const { spaceId } = useParams();
-
- console.log(spaceId);
-
- const {
- data: spacesData,
- isLoading,
- error,
- } = useQuery({
- queryKey: ["spaces"],
- queryFn: fetchSpaces,
- staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
- });
-
- const filteredSpaces = useMemo(() => {
- if (!spacesData?.spaces) return [];
- return spacesData.spaces.filter(
- (space) =>
- space.name.toLowerCase().includes(search.toLowerCase()) &&
- space.uuid !== (spaceId ? spaceId.split("---")[0] : "<HOME>"),
- );
- }, [spacesData?.spaces, search]);
-
- if (isLoading) {
- return (
- <DropdownMenuSubContent>
- <DropdownMenuItem disabled>
- <div className="flex items-center gap-2">
- <Loader />
- Loading spaces...
- </div>
- </DropdownMenuItem>
- </DropdownMenuSubContent>
- );
- }
-
- if (error) {
- return (
- <DropdownMenuSubContent>
- <DropdownMenuItem disabled>
- <div className="text-destructive">
- Error: {error instanceof Error ? error.message : "Failed to load spaces"}
- </div>
- </DropdownMenuItem>
- </DropdownMenuSubContent>
- );
- }
-
- return (
- <DropdownMenuSubContent className="p-0">
- <Command>
- <CommandInput placeholder="Search spaces..." value={search} onValueChange={setSearch} />
- <CommandList className="max-h-[200px] overflow-y-auto">
- <CommandGroup>
- {filteredSpaces.map((space) => (
- <CommandItem
- key={space.uuid}
- value={space.name}
- onSelect={() => onSelect(space.uuid)}
- >
- <div className="flex items-center gap-2">
- <SpaceIcon className="h-4 w-4" />
- {space.name}
- </div>
- </CommandItem>
- ))}
- {filteredSpaces.length === 0 && <CommandEmpty>No spaces found.</CommandEmpty>}
- </CommandGroup>
- </CommandList>
- </Command>
- </DropdownMenuSubContent>
- );
-};
-
-const MemoizedSpaceSelector = memo(SpaceSelector);
diff --git a/apps/web/app/components/memories/SpacesSelector.tsx b/apps/web/app/components/memories/SpacesSelector.tsx
deleted file mode 100644
index f8581946..00000000
--- a/apps/web/app/components/memories/SpacesSelector.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import React from "react";
-
-import { SpaceIcon } from "@supermemory/shared/icons";
-
-import { Check, ChevronsUpDown, X } from "lucide-react";
-import { Badge } from "~/components/ui/badge";
-import { Button } from "~/components/ui/button";
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
-} from "~/components/ui/command";
-import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
-import { useSpaces } from "~/lib/hooks/use-spaces";
-import { cn } from "~/lib/utils";
-
-function SpacesSelector({
- selectedSpaces = [],
- onChange,
-}: {
- selectedSpaces?: string[];
- onChange?: (spaces: string[]) => void;
-}) {
- const { spaces, isLoading } = useSpaces();
- const [open, setOpen] = React.useState(false);
-
- const handleSelect = (currentValue: string) => {
- if (!onChange || !spaces) return;
-
- const selectedSpace = spaces.find(
- (space) => space.name.toLowerCase() === currentValue.toLowerCase(),
- );
- if (!selectedSpace) return;
-
- const spaceId = selectedSpace.uuid;
- if (selectedSpaces.includes(spaceId)) {
- onChange(selectedSpaces.filter((id) => id !== spaceId));
- } else {
- onChange([...selectedSpaces, spaceId]);
- }
- setOpen(false);
- };
-
- const handleRemove = (e: React.MouseEvent, spaceId: string) => {
- e.stopPropagation();
- if (!onChange) return;
- onChange(selectedSpaces.filter((id) => id !== spaceId));
- };
-
- return (
- <Popover open={open} onOpenChange={setOpen}>
- <PopoverTrigger asChild>
- <Button
- variant="outline"
- role="combobox"
- aria-expanded={open}
- className="min-w-[40px] h-10 px-2 md:px-3 flex-1 md:flex-none flex items-center gap-1 md:gap-2 text-secondary-foreground hover:bg-gray-100 dark:hover:bg-neutral-600"
- >
- <SpaceIcon className="h-5 w-5 min-h-[20px] min-w-[20px]" />
- <div className="hidden md:flex flex-wrap gap-1 flex-grow max-w-[200px] overflow-hidden">
- {isLoading ? (
- <span className="animate-pulse">Loading...</span>
- ) : selectedSpaces.length === 0 ? (
- <span>Space</span>
- ) : (
- <div className="flex flex-nowrap overflow-hidden">
- {selectedSpaces.slice(0, 2).map((spaceId) => {
- const space = spaces?.find((s) => s.uuid === spaceId);
- if (!space) return null;
- return (
- <Badge key={spaceId} variant="secondary" className="flex items-center gap-1 text-xs whitespace-nowrap mr-1">
- {space.name}
- <X
- className="h-3 w-3 cursor-pointer hover:text-destructive"
- onClick={(e) => handleRemove(e, spaceId)}
- />
- </Badge>
- );
- })}
- {selectedSpaces.length > 2 && (
- <Badge variant="secondary" className="text-xs whitespace-nowrap">
- +{selectedSpaces.length - 2} more
- </Badge>
- )}
- </div>
- )}
- </div>
- {/* Mobile badge count */}
- {selectedSpaces.length > 0 && (
- <span className="md:hidden text-xs bg-blue-100 dark:bg-blue-900 text-blue-900 dark:text-blue-100 px-1.5 py-0.5 rounded-full">
- {selectedSpaces.length}
- </span>
- )}
- <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-[280px] md:w-[320px] p-0">
- <Command>
- <CommandInput placeholder="Search spaces..." className="h-9" />
- <CommandEmpty>No spaces found.</CommandEmpty>
- <CommandGroup className="max-h-[200px] overflow-y-auto">
- {spaces?.map((space) => (
- <CommandItem
- key={space.uuid}
- value={space.name}
- onSelect={handleSelect}
- className="cursor-pointer py-2 px-2"
- >
- <Check
- className={cn(
- "mr-2 h-4 w-4",
- selectedSpaces.includes(space.uuid) ? "opacity-100" : "opacity-0",
- )}
- />
- <div className="flex items-center justify-between w-full">
- <span className="text-sm truncate">{space.name}</span>
- {space.isPublic && (
- <Badge variant="secondary" className="ml-2 text-xs">
- Public
- </Badge>
- )}
- </div>
- </CommandItem>
- ))}
- </CommandGroup>
- </Command>
- </PopoverContent>
- </Popover>
- );
-}
-
-export default SpacesSelector;
diff --git a/apps/web/app/components/plate-ui/ai-chat-editor.tsx b/apps/web/app/components/plate-ui/ai-chat-editor.tsx
deleted file mode 100644
index a34755a0..00000000
--- a/apps/web/app/components/plate-ui/ai-chat-editor.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-'use client';
-
-import React, { memo } from 'react';
-
-import { AIChatPlugin, useLastAssistantMessage } from '@udecode/plate-ai/react';
-import {
- type PlateEditor,
- Plate,
- useEditorPlugin,
-} from '@udecode/plate-common/react';
-import { deserializeMd } from '@udecode/plate-markdown';
-
-import { Editor } from './editor';
-
-export const AIChatEditor = memo(
- ({
- aiEditorRef,
- }: {
- aiEditorRef: React.MutableRefObject<PlateEditor | null>;
- }) => {
- const { getOptions } = useEditorPlugin(AIChatPlugin);
- const lastAssistantMessage = useLastAssistantMessage();
- const content = lastAssistantMessage?.content ?? '';
-
- const aiEditor = React.useMemo(() => {
- const editor = getOptions().createAIEditor();
-
- const fragment = deserializeMd(editor, content);
- editor.children =
- fragment.length > 0 ? fragment : editor.api.create.value();
-
- return editor;
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- React.useEffect(() => {
- if (aiEditor && content) {
- aiEditorRef.current = aiEditor;
-
- setTimeout(() => {
- aiEditor.tf.setValue(deserializeMd(aiEditor, content));
- }, 0);
- }
- }, [aiEditor, aiEditorRef, content]);
-
- if (!content) return null;
-
- return (
- <Plate editor={aiEditor}>
- <Editor variant="aiChat" readOnly />
- </Plate>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/ai-leaf.tsx b/apps/web/app/components/plate-ui/ai-leaf.tsx
deleted file mode 100644
index 0080ac45..00000000
--- a/apps/web/app/components/plate-ui/ai-leaf.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn, withRef } from '@udecode/cn';
-import { PlateLeaf } from '@udecode/plate-common/react';
-
-export const AILeaf = withRef<typeof PlateLeaf>(
- ({ children, className, ...props }, ref) => {
- return (
- <PlateLeaf
- ref={ref}
- className={cn(
- 'border-b-2 border-b-purple-100 bg-purple-50 text-purple-800',
- 'transition-all duration-200 ease-in-out',
- className
- )}
- {...props}
- >
- {children}
- </PlateLeaf>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/ai-menu-items.tsx b/apps/web/app/components/plate-ui/ai-menu-items.tsx
deleted file mode 100644
index a00f8f54..00000000
--- a/apps/web/app/components/plate-ui/ai-menu-items.tsx
+++ /dev/null
@@ -1,301 +0,0 @@
-'use client';
-
-import { useEffect, useMemo } from 'react';
-
-import { AIChatPlugin, AIPlugin } from '@udecode/plate-ai/react';
-import {
- getAncestorNode,
- getEndPoint,
- getNodeString,
-} from '@udecode/plate-common';
-import {
- type PlateEditor,
- focusEditor,
- useEditorPlugin,
-} from '@udecode/plate-common/react';
-import { useIsSelecting } from '@udecode/plate-selection/react';
-import {
- Album,
- BadgeHelp,
- Check,
- CornerUpLeft,
- FeatherIcon,
- ListEnd,
- ListMinus,
- ListPlus,
- PenLine,
- Wand,
- X,
-} from 'lucide-react';
-
-import { CommandGroup, CommandItem } from './command';
-
-export type EditorChatState =
- | 'cursorCommand'
- | 'cursorSuggestion'
- | 'selectionCommand'
- | 'selectionSuggestion';
-
-export const aiChatItems = {
- accept: {
- icon: <Check />,
- label: 'Accept',
- value: 'accept',
- onSelect: ({ editor }) => {
- editor.getTransforms(AIChatPlugin).aiChat.accept();
- focusEditor(editor, getEndPoint(editor, editor.selection!));
- },
- },
- continueWrite: {
- icon: <PenLine />,
- label: 'Continue writing',
- value: 'continueWrite',
- onSelect: ({ editor }) => {
- const ancestorNode = getAncestorNode(editor);
-
- if (!ancestorNode) return;
-
- const isEmpty = getNodeString(ancestorNode[0]).trim().length === 0;
-
- void editor.getApi(AIChatPlugin).aiChat.submit({
- mode: 'insert',
- prompt: isEmpty
- ? `<Document>
-{editor}
-</Document>
-Start writing a new paragraph AFTER <Document> ONLY ONE SENTENCE`
- : 'Continue writing AFTER <Block> ONLY ONE SENTENCE. DONT REPEAT THE TEXT.',
- });
- },
- },
- discard: {
- icon: <X />,
- label: 'Discard',
- shortcut: 'Escape',
- value: 'discard',
- onSelect: ({ editor }) => {
- editor.getTransforms(AIPlugin).ai.undo();
- editor.getApi(AIChatPlugin).aiChat.hide();
- },
- },
- explain: {
- icon: <BadgeHelp />,
- label: 'Explain',
- value: 'explain',
- onSelect: ({ editor }) => {
- void editor.getApi(AIChatPlugin).aiChat.submit({
- prompt: {
- default: 'Explain {editor}',
- selecting: 'Explain',
- },
- });
- },
- },
- fixSpelling: {
- icon: <Check />,
- label: 'Fix spelling & grammar',
- value: 'fixSpelling',
- onSelect: ({ editor }) => {
- void editor.getApi(AIChatPlugin).aiChat.submit({
- prompt: 'Fix spelling and grammar',
- });
- },
- },
- improveWriting: {
- icon: <Wand />,
- label: 'Improve writing',
- value: 'improveWriting',
- onSelect: ({ editor }) => {
- void editor.getApi(AIChatPlugin).aiChat.submit({
- prompt: 'Improve the writing',
- });
- },
- },
- insertBelow: {
- icon: <ListEnd />,
- label: 'Insert below',
- value: 'insertBelow',
- onSelect: ({ aiEditor, editor }) => {
- void editor.getTransforms(AIChatPlugin).aiChat.insertBelow(aiEditor);
- },
- },
- makeLonger: {
- icon: <ListPlus />,
- label: 'Make longer',
- value: 'makeLonger',
- onSelect: ({ editor }) => {
- void editor.getApi(AIChatPlugin).aiChat.submit({
- prompt: 'Make longer',
- });
- },
- },
- makeShorter: {
- icon: <ListMinus />,
- label: 'Make shorter',
- value: 'makeShorter',
- onSelect: ({ editor }) => {
- void editor.getApi(AIChatPlugin).aiChat.submit({
- prompt: 'Make shorter',
- });
- },
- },
- replace: {
- icon: <Check />,
- label: 'Replace selection',
- value: 'replace',
- onSelect: ({ aiEditor, editor }) => {
- void editor.getTransforms(AIChatPlugin).aiChat.replaceSelection(aiEditor);
- },
- },
- simplifyLanguage: {
- icon: <FeatherIcon />,
- label: 'Simplify language',
- value: 'simplifyLanguage',
- onSelect: ({ editor }) => {
- void editor.getApi(AIChatPlugin).aiChat.submit({
- prompt: 'Simplify the language',
- });
- },
- },
- summarize: {
- icon: <Album />,
- label: 'Add a summary',
- value: 'summarize',
- onSelect: ({ editor }) => {
- void editor.getApi(AIChatPlugin).aiChat.submit({
- mode: 'insert',
- prompt: {
- default: 'Summarize {editor}',
- selecting: 'Summarize',
- },
- });
- },
- },
- tryAgain: {
- icon: <CornerUpLeft />,
- label: 'Try again',
- value: 'tryAgain',
- onSelect: ({ editor }) => {
- void editor.getApi(AIChatPlugin).aiChat.reload();
- },
- },
-} satisfies Record<
- string,
- {
- icon: React.ReactNode;
- label: string;
- value: string;
- component?: React.ComponentType<{ menuState: EditorChatState }>;
- filterItems?: boolean;
- items?: { label: string; value: string }[];
- shortcut?: string;
- onSelect?: ({
- aiEditor,
- editor,
- }: {
- aiEditor: PlateEditor;
- editor: PlateEditor;
- }) => void;
- }
->;
-
-const menuStateItems: Record<
- EditorChatState,
- {
- items: (typeof aiChatItems)[keyof typeof aiChatItems][];
- heading?: string;
- }[]
-> = {
- cursorCommand: [
- {
- items: [
- aiChatItems.continueWrite,
- aiChatItems.summarize,
- aiChatItems.explain,
- ],
- },
- ],
- cursorSuggestion: [
- {
- items: [aiChatItems.accept, aiChatItems.discard, aiChatItems.tryAgain],
- },
- ],
- selectionCommand: [
- {
- items: [
- aiChatItems.improveWriting,
- aiChatItems.makeLonger,
- aiChatItems.makeShorter,
- aiChatItems.fixSpelling,
- aiChatItems.simplifyLanguage,
- ],
- },
- ],
- selectionSuggestion: [
- {
- items: [
- aiChatItems.replace,
- aiChatItems.insertBelow,
- aiChatItems.discard,
- aiChatItems.tryAgain,
- ],
- },
- ],
-};
-
-export const AIMenuItems = ({
- aiEditorRef,
- setValue,
-}: {
- aiEditorRef: React.MutableRefObject<PlateEditor | null>;
- setValue: (value: string) => void;
-}) => {
- const { editor, useOption } = useEditorPlugin(AIChatPlugin);
- const { messages } = useOption('chat');
- const isSelecting = useIsSelecting();
-
- const menuState = useMemo(() => {
- if (messages && messages.length > 0) {
- return isSelecting ? 'selectionSuggestion' : 'cursorSuggestion';
- }
-
- return isSelecting ? 'selectionCommand' : 'cursorCommand';
- }, [isSelecting, messages]);
-
- const menuGroups = useMemo(() => {
- const items = menuStateItems[menuState];
-
- return items;
- }, [menuState]);
-
- useEffect(() => {
- if (menuGroups.length > 0 && menuGroups[0].items.length > 0) {
- setValue(menuGroups[0].items[0].value);
- }
- }, [menuGroups, setValue]);
-
- return (
- <>
- {menuGroups.map((group, index) => (
- <CommandGroup key={index} heading={group.heading}>
- {group.items.map((menuItem) => (
- <CommandItem
- key={menuItem.value}
- className="[&_svg]:text-muted-foreground"
- value={menuItem.value}
- onSelect={() => {
- menuItem.onSelect?.({
- aiEditor: aiEditorRef.current!,
- editor: editor,
- });
- }}
- >
- {menuItem.icon}
- <span>{menuItem.label}</span>
- </CommandItem>
- ))}
- </CommandGroup>
- ))}
- </>
- );
-};
diff --git a/apps/web/app/components/plate-ui/ai-menu.tsx b/apps/web/app/components/plate-ui/ai-menu.tsx
deleted file mode 100644
index 37ed2d75..00000000
--- a/apps/web/app/components/plate-ui/ai-menu.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-'use client';
-
-import * as React from 'react';
-
-import { AIChatPlugin, useEditorChat } from '@udecode/plate-ai/react';
-import {
- type TElement,
- type TNodeEntry,
- getAncestorNode,
- getBlocks,
- isElementEmpty,
- isHotkey,
- isSelectionAtBlockEnd,
-} from '@udecode/plate-common';
-import {
- type PlateEditor,
- toDOMNode,
- useEditorPlugin,
- useHotkeys,
-} from '@udecode/plate-common/react';
-import {
- BlockSelectionPlugin,
- useIsSelecting,
-} from '@udecode/plate-selection/react';
-import { Loader2Icon } from 'lucide-react';
-
-import { useChat } from '~/components/editor/use-chat';
-
-import { AIChatEditor } from './ai-chat-editor';
-import { AIMenuItems } from './ai-menu-items';
-import { Command, CommandList, InputCommand } from './command';
-import { Popover, PopoverAnchor, PopoverContent } from './popover';
-
-export function AIMenu() {
- const { api, editor, useOption } = useEditorPlugin(AIChatPlugin);
- const open = useOption('open');
- const mode = useOption('mode');
- const isSelecting = useIsSelecting();
-
- const aiEditorRef = React.useRef<PlateEditor | null>(null);
- const [value, setValue] = React.useState('');
-
- const chat = useChat();
-
- const { input, isLoading, messages, setInput } = chat;
- const [anchorElement, setAnchorElement] = React.useState<HTMLElement | null>(
- null
- );
-
- const setOpen = (open: boolean) => {
- if (open) {
- api.aiChat.show();
- } else {
- api.aiChat.hide();
- }
- };
-
- const show = (anchorElement: HTMLElement) => {
- setAnchorElement(anchorElement);
- setOpen(true);
- };
-
- useEditorChat({
- chat,
- onOpenBlockSelection: (blocks: TNodeEntry[]) => {
- show(toDOMNode(editor, blocks.at(-1)![0])!);
- },
- onOpenChange: (open) => {
- if (!open) {
- setAnchorElement(null);
- setInput('');
- }
- },
- onOpenCursor: () => {
- const ancestor = getAncestorNode(editor)?.[0] as TElement;
-
- if (!isSelectionAtBlockEnd(editor) && !isElementEmpty(editor, ancestor)) {
- editor
- .getApi(BlockSelectionPlugin)
- .blockSelection.addSelectedRow(ancestor.id as string);
- }
-
- show(toDOMNode(editor, ancestor)!);
- },
- onOpenSelection: () => {
- show(toDOMNode(editor, getBlocks(editor).at(-1)![0])!);
- },
- });
-
- useHotkeys(
- 'meta+j',
- () => {
- api.aiChat.show();
- },
- { enableOnContentEditable: true, enableOnFormTags: true }
- );
-
- return (
- <Popover open={open} onOpenChange={setOpen} modal={false}>
- <PopoverAnchor virtualRef={{ current: anchorElement }} />
-
- <PopoverContent
- className="border-none bg-transparent p-0 shadow-none"
- style={{
- width: anchorElement?.offsetWidth,
- }}
- onEscapeKeyDown={(e) => {
- e.preventDefault();
-
- if (isLoading) {
- api.aiChat.stop();
- } else {
- api.aiChat.hide();
- }
- }}
- align="center"
- avoidCollisions={false}
- side="bottom"
- >
- <Command
- className="w-full rounded-lg border shadow-md"
- value={value}
- onValueChange={setValue}
- >
- {mode === 'chat' && isSelecting && messages.length > 0 && (
- <AIChatEditor aiEditorRef={aiEditorRef} />
- )}
-
- {isLoading ? (
- <div className="flex grow select-none items-center gap-2 p-2 text-sm text-muted-foreground">
- <Loader2Icon className="size-4 animate-spin" />
- {messages.length > 1 ? 'Editing...' : 'Thinking...'}
- </div>
- ) : (
- <InputCommand
- variant="ghost"
- className="rounded-none border-b border-solid border-border [&_svg]:hidden"
- value={input}
- onKeyDown={(e) => {
- if (isHotkey('backspace')(e) && input.length === 0) {
- e.preventDefault();
- api.aiChat.hide();
- }
- if (isHotkey('enter')(e) && !e.shiftKey && !value) {
- e.preventDefault();
- void api.aiChat.submit();
- }
- }}
- onValueChange={setInput}
- placeholder="Ask AI anything..."
- data-plate-focus
- autoFocus
- />
- )}
-
- {!isLoading && (
- <CommandList>
- <AIMenuItems aiEditorRef={aiEditorRef} setValue={setValue} />
- </CommandList>
- )}
- </Command>
- </PopoverContent>
- </Popover>
- );
-}
diff --git a/apps/web/app/components/plate-ui/ai-toolbar-button.tsx b/apps/web/app/components/plate-ui/ai-toolbar-button.tsx
deleted file mode 100644
index 566d86b2..00000000
--- a/apps/web/app/components/plate-ui/ai-toolbar-button.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { withRef } from '@udecode/cn';
-import { AIChatPlugin } from '@udecode/plate-ai/react';
-import { useEditorPlugin } from '@udecode/plate-common/react';
-
-import { ToolbarButton } from './toolbar';
-
-export const AIToolbarButton = withRef<typeof ToolbarButton>(
- ({ children, ...rest }, ref) => {
- const { api } = useEditorPlugin(AIChatPlugin);
-
- return (
- <ToolbarButton
- ref={ref}
- {...rest}
- onClick={() => {
- api.aiChat.show();
- }}
- onMouseDown={(e) => {
- e.preventDefault();
- }}
- >
- {children}
- </ToolbarButton>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/align-dropdown-menu.tsx b/apps/web/app/components/plate-ui/align-dropdown-menu.tsx
deleted file mode 100644
index e0764690..00000000
--- a/apps/web/app/components/plate-ui/align-dropdown-menu.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
-
-import {
- useAlignDropdownMenu,
- useAlignDropdownMenuState,
-} from '@udecode/plate-alignment/react';
-import {
- AlignCenterIcon,
- AlignJustifyIcon,
- AlignLeftIcon,
- AlignRightIcon,
-} from 'lucide-react';
-
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuTrigger,
- useOpenState,
-} from './dropdown-menu';
-import { ToolbarButton } from './toolbar';
-
-const items = [
- {
- icon: AlignLeftIcon,
- value: 'left',
- },
- {
- icon: AlignCenterIcon,
- value: 'center',
- },
- {
- icon: AlignRightIcon,
- value: 'right',
- },
- {
- icon: AlignJustifyIcon,
- value: 'justify',
- },
-];
-
-export function AlignDropdownMenu({ children, ...props }: DropdownMenuProps) {
- const state = useAlignDropdownMenuState();
- const { radioGroupProps } = useAlignDropdownMenu(state);
-
- const openState = useOpenState();
- const IconValue =
- items.find((item) => item.value === radioGroupProps.value)?.icon ??
- AlignLeftIcon;
-
- return (
- <DropdownMenu modal={false} {...openState} {...props}>
- <DropdownMenuTrigger asChild>
- <ToolbarButton pressed={openState.open} tooltip="Align" isDropdown>
- <IconValue />
- </ToolbarButton>
- </DropdownMenuTrigger>
-
- <DropdownMenuContent className="min-w-0" align="start">
- <DropdownMenuRadioGroup {...radioGroupProps}>
- {items.map(({ icon: Icon, value: itemValue }) => (
- <DropdownMenuRadioItem key={itemValue} value={itemValue} hideIcon>
- <Icon />
- </DropdownMenuRadioItem>
- ))}
- </DropdownMenuRadioGroup>
- </DropdownMenuContent>
- </DropdownMenu>
- );
-}
diff --git a/apps/web/app/components/plate-ui/avatar.tsx b/apps/web/app/components/plate-ui/avatar.tsx
deleted file mode 100644
index 3aad74b5..00000000
--- a/apps/web/app/components/plate-ui/avatar.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-'use client';
-
-import * as AvatarPrimitive from '@radix-ui/react-avatar';
-import { withCn } from '@udecode/cn';
-
-export const Avatar = withCn(
- AvatarPrimitive.Root,
- 'relative flex size-10 shrink-0 overflow-hidden rounded-full'
-);
-
-export const AvatarImage = withCn(
- AvatarPrimitive.Image,
- 'aspect-square size-full'
-);
-
-export const AvatarFallback = withCn(
- AvatarPrimitive.Fallback,
- 'flex size-full items-center justify-center rounded-full bg-muted'
-);
diff --git a/apps/web/app/components/plate-ui/block-context-menu.tsx b/apps/web/app/components/plate-ui/block-context-menu.tsx
deleted file mode 100644
index bec50b02..00000000
--- a/apps/web/app/components/plate-ui/block-context-menu.tsx
+++ /dev/null
@@ -1,206 +0,0 @@
-'use client';
-
-import { useCallback, useState } from 'react';
-
-import { AIChatPlugin } from '@udecode/plate-ai/react';
-import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
-import { unsetNodes } from '@udecode/plate-common';
-import {
- ParagraphPlugin,
- focusEditor,
- useEditorPlugin,
-} from '@udecode/plate-common/react';
-import { HEADING_KEYS } from '@udecode/plate-heading';
-import { IndentListPlugin } from '@udecode/plate-indent-list/react';
-import {
- BLOCK_CONTEXT_MENU_ID,
- BlockMenuPlugin,
- BlockSelectionPlugin,
-} from '@udecode/plate-selection/react';
-
-import { useIsTouchDevice } from '~/lib/hooks/use-is-touch-device';
-
-import {
- ContextMenu,
- ContextMenuContent,
- ContextMenuGroup,
- ContextMenuItem,
- ContextMenuSub,
- ContextMenuSubContent,
- ContextMenuSubTrigger,
- ContextMenuTrigger,
-} from './context-menu';
-
-type Value = 'askAI' | null;
-
-export function BlockContextMenu({ children }: { children: React.ReactNode }) {
- const { api, editor } = useEditorPlugin(BlockMenuPlugin);
- const [value, setValue] = useState<Value>(null);
- const isTouch = useIsTouchDevice();
-
- const handleTurnInto = useCallback(
- (type: string) => {
- editor
- .getApi(BlockSelectionPlugin)
- .blockSelection.getNodes()
- .forEach(([node, path]) => {
- if (node[IndentListPlugin.key]) {
- unsetNodes(editor, [IndentListPlugin.key, 'indent'], { at: path });
- }
-
- editor.tf.toggle.block({ type }, { at: path });
- });
- },
- [editor]
- );
-
- const handleAlign = useCallback(
- (align: 'center' | 'left' | 'right') => {
- editor
- .getTransforms(BlockSelectionPlugin)
- .blockSelection.setNodes({ align });
- },
- [editor]
- );
-
- if (isTouch) {
- return children;
- }
-
- return (
- <ContextMenu
- onOpenChange={(open) => {
- if (!open) {
- // prevent unselect the block selection
- setTimeout(() => {
- api.blockMenu.hide();
- }, 0);
- }
- }}
- modal={false}
- >
- <ContextMenuTrigger
- asChild
- onContextMenu={(event) => {
- const dataset = (event.target as HTMLElement).dataset;
-
- const disabled = dataset?.slateEditor === 'true';
-
- if (disabled) return event.preventDefault();
-
- api.blockMenu.show(BLOCK_CONTEXT_MENU_ID, {
- x: event.clientX,
- y: event.clientY,
- });
- }}
- >
- <div className="w-full">{children}</div>
- </ContextMenuTrigger>
- <ContextMenuContent
- className="w-64"
- onCloseAutoFocus={(e) => {
- e.preventDefault();
- editor.getApi(BlockSelectionPlugin).blockSelection.focus();
-
- if (value === 'askAI') {
- editor.getApi(AIChatPlugin).aiChat.show();
- }
-
- setValue(null);
- }}
- >
- <ContextMenuGroup>
- <ContextMenuItem
- onClick={() => {
- setValue('askAI');
- }}
- >
- Ask AI
- </ContextMenuItem>
- <ContextMenuItem
- onClick={() => {
- editor
- .getTransforms(BlockSelectionPlugin)
- .blockSelection.removeNodes();
- focusEditor(editor);
- }}
- >
- Delete
- </ContextMenuItem>
- <ContextMenuItem
- onClick={() => {
- editor
- .getTransforms(BlockSelectionPlugin)
- .blockSelection.duplicate(
- editor.getApi(BlockSelectionPlugin).blockSelection.getNodes()
- );
- }}
- >
- Duplicate
- {/* <ContextMenuShortcut>⌘ + D</ContextMenuShortcut> */}
- </ContextMenuItem>
- <ContextMenuSub>
- <ContextMenuSubTrigger>Turn into</ContextMenuSubTrigger>
- <ContextMenuSubContent className="w-48">
- <ContextMenuItem
- onClick={() => handleTurnInto(ParagraphPlugin.key)}
- >
- Paragraph
- </ContextMenuItem>
-
- <ContextMenuItem onClick={() => handleTurnInto(HEADING_KEYS.h1)}>
- Heading 1
- </ContextMenuItem>
- <ContextMenuItem onClick={() => handleTurnInto(HEADING_KEYS.h2)}>
- Heading 2
- </ContextMenuItem>
- <ContextMenuItem onClick={() => handleTurnInto(HEADING_KEYS.h3)}>
- Heading 3
- </ContextMenuItem>
- <ContextMenuItem
- onClick={() => handleTurnInto(BlockquotePlugin.key)}
- >
- Blockquote
- </ContextMenuItem>
- </ContextMenuSubContent>
- </ContextMenuSub>
- </ContextMenuGroup>
-
- <ContextMenuGroup>
- <ContextMenuItem
- onClick={() =>
- editor
- .getTransforms(BlockSelectionPlugin)
- .blockSelection.setIndent(1)
- }
- >
- Indent
- </ContextMenuItem>
- <ContextMenuItem
- onClick={() =>
- editor
- .getTransforms(BlockSelectionPlugin)
- .blockSelection.setIndent(-1)
- }
- >
- Outdent
- </ContextMenuItem>
- <ContextMenuSub>
- <ContextMenuSubTrigger>Align</ContextMenuSubTrigger>
- <ContextMenuSubContent className="w-48">
- <ContextMenuItem onClick={() => handleAlign('left')}>
- Left
- </ContextMenuItem>
- <ContextMenuItem onClick={() => handleAlign('center')}>
- Center
- </ContextMenuItem>
- <ContextMenuItem onClick={() => handleAlign('right')}>
- Right
- </ContextMenuItem>
- </ContextMenuSubContent>
- </ContextMenuSub>
- </ContextMenuGroup>
- </ContextMenuContent>
- </ContextMenu>
- );
-}
diff --git a/apps/web/app/components/plate-ui/block-selection.tsx b/apps/web/app/components/plate-ui/block-selection.tsx
deleted file mode 100644
index b0562fe7..00000000
--- a/apps/web/app/components/plate-ui/block-selection.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-'use client';
-
-import { cn } from '@udecode/cn';
-import { useBlockSelected } from '@udecode/plate-selection/react';
-import { type VariantProps, cva } from 'class-variance-authority';
-
-export const blockSelectionVariants = cva(
- 'pointer-events-none absolute inset-0 z-[1] bg-brand/[.13] transition-opacity',
- {
- defaultVariants: {
- active: true,
- },
- variants: {
- active: {
- false: 'opacity-0',
- true: 'opacity-100',
- },
- },
- }
-);
-
-export function BlockSelection({
- className,
- ...props
-}: React.HTMLAttributes<HTMLDivElement> &
- VariantProps<typeof blockSelectionVariants>) {
- const isBlockSelected = useBlockSelected();
-
- if (!isBlockSelected) return null;
-
- return (
- <div
- className={cn(
- blockSelectionVariants({
- active: isBlockSelected,
- }),
- className
- )}
- {...props}
- />
- );
-}
diff --git a/apps/web/app/components/plate-ui/blockquote-element.tsx b/apps/web/app/components/plate-ui/blockquote-element.tsx
deleted file mode 100644
index f6fa8a20..00000000
--- a/apps/web/app/components/plate-ui/blockquote-element.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn, withRef } from '@udecode/cn';
-
-import { PlateElement } from './plate-element';
-
-export const BlockquoteElement = withRef<typeof PlateElement>(
- ({ children, className, ...props }, ref) => {
- return (
- <PlateElement
- ref={ref}
- as="blockquote"
- className={cn('my-1 border-l-2 pl-6 italic', className)}
- {...props}
- >
- {children}
- </PlateElement>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/button.tsx b/apps/web/app/components/plate-ui/button.tsx
deleted file mode 100644
index d017db41..00000000
--- a/apps/web/app/components/plate-ui/button.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import * as React from 'react';
-
-import { Slot } from '@radix-ui/react-slot';
-import { cn, withRef } from '@udecode/cn';
-import { type VariantProps, cva } from 'class-variance-authority';
-
-export const buttonVariants = cva(
- 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
- {
- defaultVariants: {
- size: 'sm',
- variant: 'default',
- },
- variants: {
- isMenu: {
- true: 'w-full cursor-pointer justify-start',
- },
- size: {
- icon: 'size-[28px] rounded-md px-1.5',
- lg: 'h-10 rounded-md px-4',
- md: 'h-8 px-3 text-sm',
- none: '',
- sm: 'h-[28px] rounded-md px-2.5',
- xs: 'h-8 rounded-md px-3 text-xs',
- },
- variant: {
- default: 'bg-primary text-primary-foreground hover:bg-primary/90',
- destructive:
- 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
- ghost: 'hover:bg-accent hover:text-accent-foreground',
- inlineLink: 'text-base text-primary underline underline-offset-4',
- link: 'text-primary underline-offset-4 hover:underline',
- outline:
- 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
- secondary:
- 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
- },
- },
- }
-);
-
-export const Button = withRef<
- 'button',
- {
- asChild?: boolean;
- } & VariantProps<typeof buttonVariants>
->(({ asChild = false, className, isMenu, size, variant, ...props }, ref) => {
- const Comp = asChild ? Slot : 'button';
-
- return (
- <Comp
- ref={ref}
- className={cn(buttonVariants({ className, isMenu, size, variant }))}
- {...props}
- />
- );
-});
diff --git a/apps/web/app/components/plate-ui/calendar.tsx b/apps/web/app/components/plate-ui/calendar.tsx
deleted file mode 100644
index 2205ef1a..00000000
--- a/apps/web/app/components/plate-ui/calendar.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import { DayPicker } from 'react-day-picker';
-
-import { cn } from '@udecode/cn';
-import { ChevronLeft, ChevronRight } from 'lucide-react';
-
-import { buttonVariants } from './button';
-
-export type CalendarProps = React.ComponentProps<typeof DayPicker>;
-
-function Calendar({
- className,
- classNames,
- showOutsideDays = true,
- ...props
-}: CalendarProps) {
- return (
- <DayPicker
- className={cn('p-3', className)}
- classNames={{
- caption: 'flex justify-center pt-1 relative items-center',
- caption_label: 'text-sm font-medium',
- cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
- day: cn(
- buttonVariants({ variant: 'ghost' }),
- 'size-9 p-0 font-normal aria-selected:opacity-100'
- ),
- day_disabled: 'text-muted-foreground opacity-50',
- day_hidden: 'invisible',
- day_outside:
- 'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
- day_range_end: 'day-range-end',
- day_range_middle:
- 'aria-selected:bg-accent aria-selected:text-accent-foreground',
- day_selected:
- 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
- day_today: 'bg-accent text-accent-foreground',
- head_cell:
- 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
- head_row: 'flex',
- month: 'space-y-4',
- months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
- nav: 'space-x-1 flex items-center',
- nav_button: cn(
- buttonVariants({ variant: 'outline' }),
- 'size-7 bg-transparent p-0 opacity-50 hover:opacity-100'
- ),
- nav_button_next: 'absolute right-1',
- nav_button_previous: 'absolute left-1',
- row: 'flex w-full mt-2',
- table: 'w-full border-collapse space-y-1',
- ...classNames,
- }}
- components={{
- IconLeft: () => <ChevronLeft className="size-4" />,
- IconRight: () => <ChevronRight className="size-4" />,
- }}
- showOutsideDays={showOutsideDays}
- {...props}
- />
- );
-}
-
-Calendar.displayName = 'Calendar';
-
-export { Calendar };
diff --git a/apps/web/app/components/plate-ui/caption.tsx b/apps/web/app/components/plate-ui/caption.tsx
deleted file mode 100644
index 9ab81642..00000000
--- a/apps/web/app/components/plate-ui/caption.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-'use client';
-
-import {
- cn,
- createPrimitiveComponent,
- withCn,
- withVariants,
-} from '@udecode/cn';
-import {
- Caption as CaptionPrimitive,
- CaptionTextarea as CaptionTextareaPrimitive,
- useCaptionButton,
- useCaptionButtonState,
-} from '@udecode/plate-caption/react';
-import { cva } from 'class-variance-authority';
-
-import { Button } from './button';
-
-const captionVariants = cva('max-w-full', {
- defaultVariants: {
- align: 'center',
- },
- variants: {
- align: {
- center: 'mx-auto',
- left: 'mr-auto',
- right: 'ml-auto',
- },
- },
-});
-
-export const Caption = withVariants(CaptionPrimitive, captionVariants, [
- 'align',
-]);
-
-export const CaptionTextarea = withCn(
- CaptionTextareaPrimitive,
- cn(
- 'mt-2 w-full resize-none border-none bg-inherit p-0 font-[inherit] text-inherit',
- 'focus:outline-none focus:[&::placeholder]:opacity-0',
- 'text-center print:placeholder:text-transparent'
- )
-);
-
-export const CaptionButton = createPrimitiveComponent(Button)({
- propsHook: useCaptionButton,
- stateHook: useCaptionButtonState,
-});
diff --git a/apps/web/app/components/plate-ui/checkbox.tsx b/apps/web/app/components/plate-ui/checkbox.tsx
deleted file mode 100644
index d369c923..00000000
--- a/apps/web/app/components/plate-ui/checkbox.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-'use client';
-
-import * as React from 'react';
-
-import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
-import { cn, withRef } from '@udecode/cn';
-import { Check } from 'lucide-react';
-
-export const Checkbox = withRef<typeof CheckboxPrimitive.Root>(
- ({ className, ...props }, ref) => (
- <CheckboxPrimitive.Root
- ref={ref}
- className={cn(
- 'peer size-4 shrink-0 rounded-sm border border-primary bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
- className
- )}
- {...props}
- >
- <CheckboxPrimitive.Indicator
- className={cn('flex items-center justify-center text-current')}
- >
- <Check className="size-4" />
- </CheckboxPrimitive.Indicator>
- </CheckboxPrimitive.Root>
- )
-);
diff --git a/apps/web/app/components/plate-ui/code-block-combobox.tsx b/apps/web/app/components/plate-ui/code-block-combobox.tsx
deleted file mode 100644
index 7ec8055b..00000000
--- a/apps/web/app/components/plate-ui/code-block-combobox.tsx
+++ /dev/null
@@ -1,202 +0,0 @@
-'use client';
-
-import React, { useState } from 'react';
-
-import { cn } from '@udecode/cn';
-import {
- useCodeBlockCombobox,
- useCodeBlockComboboxState,
-} from '@udecode/plate-code-block/react';
-import { Check, ChevronsUpDown } from 'lucide-react';
-
-// Prism must be imported before all language files
-import Prism from 'prismjs';
-
-import { Button } from './button';
-import {
- Command,
- CommandEmpty,
- CommandInput,
- CommandItem,
- CommandList,
-} from './command';
-import { Popover, PopoverContent, PopoverTrigger } from './popover';
-
-import 'prismjs/components/prism-antlr4.js';
-import 'prismjs/components/prism-bash.js';
-import 'prismjs/components/prism-c.js';
-import 'prismjs/components/prism-cmake.js';
-import 'prismjs/components/prism-coffeescript.js';
-import 'prismjs/components/prism-cpp.js';
-import 'prismjs/components/prism-csharp.js';
-import 'prismjs/components/prism-css.js';
-import 'prismjs/components/prism-dart.js';
-
-// import 'prismjs/components/prism-django.js';
-import 'prismjs/components/prism-docker.js';
-
-// import 'prismjs/components/prism-ejs.js';
-import 'prismjs/components/prism-erlang.js';
-import 'prismjs/components/prism-git.js';
-import 'prismjs/components/prism-go.js';
-import 'prismjs/components/prism-graphql.js';
-import 'prismjs/components/prism-groovy.js';
-import 'prismjs/components/prism-java.js';
-import 'prismjs/components/prism-javascript.js';
-import 'prismjs/components/prism-json.js';
-import 'prismjs/components/prism-jsx.js';
-import 'prismjs/components/prism-kotlin.js';
-import 'prismjs/components/prism-latex.js';
-import 'prismjs/components/prism-less.js';
-import 'prismjs/components/prism-lua.js';
-import 'prismjs/components/prism-makefile.js';
-import 'prismjs/components/prism-markdown.js';
-import 'prismjs/components/prism-matlab.js';
-import 'prismjs/components/prism-mermaid.js';
-import 'prismjs/components/prism-objectivec.js';
-import 'prismjs/components/prism-perl.js';
-
-// import 'prismjs/components/prism-php.js';
-import 'prismjs/components/prism-powershell.js';
-import 'prismjs/components/prism-properties.js';
-import 'prismjs/components/prism-protobuf.js';
-import 'prismjs/components/prism-python.js';
-import 'prismjs/components/prism-r.js';
-import 'prismjs/components/prism-ruby.js';
-import 'prismjs/components/prism-sass.js';
-import 'prismjs/components/prism-scala.js';
-import 'prismjs/components/prism-scheme.js';
-import 'prismjs/components/prism-scss.js';
-import 'prismjs/components/prism-sql.js';
-import 'prismjs/components/prism-swift.js';
-import 'prismjs/components/prism-tsx.js';
-import 'prismjs/components/prism-typescript.js';
-import 'prismjs/components/prism-wasm.js';
-import 'prismjs/components/prism-yaml.js';
-
-export { Prism };
-
-const languages: { label: string; value: string }[] = [
- { label: 'Plain Text', value: 'text' },
- { label: 'Bash', value: 'bash' },
- { label: 'CSS', value: 'css' },
- { label: 'Git', value: 'git' },
- { label: 'GraphQL', value: 'graphql' },
- { label: 'HTML', value: 'html' },
- { label: 'JavaScript', value: 'javascript' },
- { label: 'JSON', value: 'json' },
- { label: 'JSX', value: 'jsx' },
- { label: 'Markdown', value: 'markdown' },
- { label: 'SQL', value: 'sql' },
- { label: 'SVG', value: 'svg' },
- { label: 'TSX', value: 'tsx' },
- { label: 'TypeScript', value: 'typescript' },
- { label: 'WebAssembly', value: 'wasm' },
- { label: 'ANTLR4', value: 'antlr4' },
- { label: 'C', value: 'c' },
- { label: 'CMake', value: 'cmake' },
- { label: 'CoffeeScript', value: 'coffeescript' },
- { label: 'C#', value: 'csharp' },
- { label: 'Dart', value: 'dart' },
- { label: 'Django', value: 'django' },
- { label: 'Docker', value: 'docker' },
- { label: 'EJS', value: 'ejs' },
- { label: 'Erlang', value: 'erlang' },
- { label: 'Go', value: 'go' },
- { label: 'Groovy', value: 'groovy' },
- { label: 'Java', value: 'java' },
- { label: 'Kotlin', value: 'kotlin' },
- { label: 'LaTeX', value: 'latex' },
- { label: 'Less', value: 'less' },
- { label: 'Lua', value: 'lua' },
- { label: 'Makefile', value: 'makefile' },
- { label: 'Markup', value: 'markup' },
- { label: 'MATLAB', value: 'matlab' },
- { label: 'Mermaid', value: 'mermaid' },
- { label: 'Objective-C', value: 'objectivec' },
- { label: 'Perl', value: 'perl' },
- { label: 'PHP', value: 'php' },
- { label: 'PowerShell', value: 'powershell' },
- { label: '.properties', value: 'properties' },
- { label: 'Protocol Buffers', value: 'protobuf' },
- { label: 'Python', value: 'python' },
- { label: 'R', value: 'r' },
- { label: 'Ruby', value: 'ruby' },
- { label: 'Sass (Sass)', value: 'sass' },
- // FIXME: Error with current scala grammar
- { label: 'Scala', value: 'scala' },
- { label: 'Scheme', value: 'scheme' },
- { label: 'Sass (Scss)', value: 'scss' },
- { label: 'Shell', value: 'shell' },
- { label: 'Swift', value: 'swift' },
- { label: 'XML', value: 'xml' },
- { label: 'YAML', value: 'yaml' },
-];
-
-export function CodeBlockCombobox() {
- const state = useCodeBlockComboboxState();
- const { commandItemProps } = useCodeBlockCombobox(state);
-
- const [open, setOpen] = useState(false);
- const [value, setValue] = useState('');
-
- if (state.readOnly) return null;
-
- const items = languages.filter(
- (language) =>
- !value ||
- language.label.toLowerCase().includes(value.toLowerCase()) ||
- language.value.toLowerCase().includes(value.toLowerCase())
- );
-
- return (
- <Popover open={open} onOpenChange={setOpen}>
- <PopoverTrigger asChild>
- <Button
- size="xs"
- variant="ghost"
- className="h-5 justify-between px-1 text-xs"
- aria-expanded={open}
- role="combobox"
- >
- {state.value
- ? languages.find((language) => language.value === state.value)
- ?.label
- : 'Plain Text'}
- <ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-[200px] p-0">
- <Command shouldFilter={false}>
- <CommandInput
- value={value}
- onValueChange={(value) => setValue(value)}
- placeholder="Search language..."
- />
- <CommandEmpty>No language found.</CommandEmpty>
-
- <CommandList>
- {items.map((language) => (
- <CommandItem
- key={language.value}
- className="cursor-pointer"
- value={language.value}
- onSelect={(_value) => {
- commandItemProps.onSelect(_value);
- setOpen(false);
- }}
- >
- <Check
- className={cn(
- state.value === language.value ? 'opacity-100' : 'opacity-0'
- )}
- />
- {language.label}
- </CommandItem>
- ))}
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- );
-}
diff --git a/apps/web/app/components/plate-ui/code-block-element.css b/apps/web/app/components/plate-ui/code-block-element.css
deleted file mode 100644
index ef6d891a..00000000
--- a/apps/web/app/components/plate-ui/code-block-element.css
+++ /dev/null
@@ -1,404 +0,0 @@
-.slate-code_block code[class*='language-'],
-.slate-code_block pre[class*='language-'] {
- background: hsl(230, 1%, 98%);
- color: hsl(230, 8%, 24%);
- font-family: 'Fira Code', 'Fira Mono', Menlo, Consolas, 'DejaVu Sans Mono',
- monospace;
- direction: ltr;
- text-align: left;
- white-space: pre;
- word-spacing: normal;
- word-break: normal;
- line-height: 1.5;
- -moz-tab-size: 2;
- -o-tab-size: 2;
- tab-size: 2;
- -webkit-hyphens: none;
- -moz-hyphens: none;
- -ms-hyphens: none;
- hyphens: none;
-}
-
-/* Selection */
-.slate-code_block code[class*='language-']::-moz-selection,
-.slate-code_block code[class*='language-'] *::-moz-selection,
-.slate-code_block pre[class*='language-'] *::-moz-selection {
- background: hsl(230, 1%, 90%);
- color: inherit;
-}
-
-.slate-code_block code[class*='language-']::selection,
-.slate-code_block code[class*='language-'] *::selection,
-.slate-code_block pre[class*='language-'] *::selection {
- background: hsl(230, 1%, 90%);
- color: inherit;
-}
-
-/* Code blocks */
-.slate-code_block pre[class*='language-'] {
- padding: 1em;
- margin: 0.5em 0;
- overflow: auto;
- border-radius: 0.3em;
-}
-
-/* Inline code */
-.slate-code_block :not(pre) > code[class*='language-'] {
- padding: 0.2em 0.3em;
- border-radius: 0.3em;
- white-space: normal;
-}
-
-.token.comment,
-.token.prolog,
-.token.cdata {
- color: hsl(230, 4%, 64%);
-}
-
-.token.doctype,
-.token.punctuation,
-.token.entity {
- color: hsl(230, 8%, 24%);
-}
-
-.token.attr-name,
-.token.class-name,
-.token.boolean,
-.token.constant,
-.token.number,
-.token.atrule {
- color: hsl(35, 99%, 36%);
-}
-
-.token.keyword {
- color: hsl(301, 63%, 40%);
-}
-
-.token.property,
-.token.tag,
-.token.symbol,
-.token.deleted,
-.token.important {
- color: hsl(5, 74%, 59%);
-}
-
-.token.selector,
-.token.string,
-.token.char,
-.token.builtin,
-.token.inserted,
-.token.regex,
-.token.attr-value,
-.token.attr-value > .token.punctuation {
- color: hsl(119, 34%, 47%);
-}
-
-.token.variable,
-.token.operator,
-.token.function {
- color: hsl(221, 87%, 60%);
-}
-
-.token.url {
- color: hsl(198, 99%, 37%);
-}
-
-/* HTML overrides */
-.token.attr-value > .token.punctuation.attr-equals,
-.token.special-attr > .token.attr-value > .token.value.css {
- color: hsl(230, 8%, 24%);
-}
-
-/* CSS overrides */
-.language-css .token.selector {
- color: hsl(5, 74%, 59%);
-}
-
-.language-css .token.property {
- color: hsl(230, 8%, 24%);
-}
-
-.language-css .token.function,
-.language-css .token.url > .token.function {
- color: hsl(198, 99%, 37%);
-}
-
-.language-css .token.url > .token.string.url {
- color: hsl(119, 34%, 47%);
-}
-
-.language-css .token.important,
-.language-css .token.atrule .token.rule {
- color: hsl(301, 63%, 40%);
-}
-
-/* JS overrides */
-.language-javascript .token.operator {
- color: hsl(301, 63%, 40%);
-}
-
-.language-javascript
- .token.template-string
- > .token.interpolation
- > .token.interpolation-punctuation.punctuation {
- color: hsl(344, 84%, 43%);
-}
-
-/* JSON overrides */
-.language-json .token.operator {
- color: hsl(230, 8%, 24%);
-}
-
-.language-json .token.null.keyword {
- color: hsl(35, 99%, 36%);
-}
-
-/* MD overrides */
-.language-markdown .token.url,
-.language-markdown .token.url > .token.operator,
-.language-markdown .token.url-reference.url > .token.string {
- color: hsl(230, 8%, 24%);
-}
-
-.language-markdown .token.url > .token.content {
- color: hsl(221, 87%, 60%);
-}
-
-.language-markdown .token.url > .token.url,
-.language-markdown .token.url-reference.url {
- color: hsl(198, 99%, 37%);
-}
-
-.language-markdown .token.blockquote.punctuation,
-.language-markdown .token.hr.punctuation {
- color: hsl(230, 4%, 64%);
- font-style: italic;
-}
-
-.language-markdown .token.code-snippet {
- color: hsl(119, 34%, 47%);
-}
-
-.language-markdown .token.bold .token.content {
- color: hsl(35, 99%, 36%);
-}
-
-.language-markdown .token.italic .token.content {
- color: hsl(301, 63%, 40%);
-}
-
-.language-markdown .token.strike .token.content,
-.language-markdown .token.strike .token.punctuation,
-.language-markdown .token.list.punctuation,
-.language-markdown .token.title.important > .token.punctuation {
- color: hsl(5, 74%, 59%);
-}
-
-/* General */
-.token.bold {
- font-weight: bold;
-}
-
-.token.comment,
-.token.italic {
- font-style: italic;
-}
-
-.token.entity {
- cursor: help;
-}
-
-.token.namespace {
- opacity: 0.8;
-}
-
-/* Plugin overrides */
-/* Selectors should have higher specificity than those in the plugins' default stylesheets */
-
-/* Show Invisibles plugin overrides */
-.token.token.tab:not(:empty):before,
-.token.token.cr:before,
-.token.token.lf:before,
-.token.token.space:before {
- color: hsla(230, 8%, 24%, 0.2);
-}
-
-/* Toolbar plugin overrides */
-/* Space out all buttons and move them away from the right edge of the code block */
-div.code-toolbar > .toolbar.toolbar > .toolbar-item {
- margin-right: 0.4em;
-}
-
-/* Styling the buttons */
-div.code-toolbar > .toolbar.toolbar > .toolbar-item > button,
-div.code-toolbar > .toolbar.toolbar > .toolbar-item > a,
-div.code-toolbar > .toolbar.toolbar > .toolbar-item > span {
- background: hsl(230, 1%, 90%);
- color: hsl(230, 6%, 44%);
- padding: 0.1em 0.4em;
- border-radius: 0.3em;
-}
-
-div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:hover,
-div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:focus,
-div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:hover,
-div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:focus,
-div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:hover,
-div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:focus {
- background: hsl(230, 1%, 78%); /* custom: darken(--syntax-bg, 20%) */
- color: hsl(230, 8%, 24%);
-}
-
-/* Line Highlight plugin overrides */
-/* The highlighted line itself */
-.line-highlight.line-highlight {
- background: hsla(230, 8%, 24%, 0.05);
-}
-
-/* Default line numbers in Line Highlight plugin */
-.line-highlight.line-highlight:before,
-.line-highlight.line-highlight[data-end]:after {
- background: hsl(230, 1%, 90%);
- color: hsl(230, 8%, 24%);
- padding: 0.1em 0.6em;
- border-radius: 0.3em;
- box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2); /* same as Toolbar plugin default */
-}
-
-/* Hovering over a linkable line number (in the gutter area) */
-/* Requires Line Numbers plugin as well */
-pre[id].linkable-line-numbers.linkable-line-numbers
- span.line-numbers-rows
- > span:hover:before {
- background-color: hsla(230, 8%, 24%, 0.05);
-}
-
-/* Line Numbers and Command Line plugins overrides */
-/* Line separating gutter from coding area */
-.line-numbers.line-numbers .line-numbers-rows,
-.command-line .command-line-prompt {
- border-right-color: hsla(230, 8%, 24%, 0.2);
-}
-
-/* Stuff in the gutter */
-.line-numbers .line-numbers-rows > span:before,
-.command-line .command-line-prompt > span:before {
- color: hsl(230, 1%, 62%);
-}
-
-/* Match Braces plugin overrides */
-/* Note: Outline colour is inherited from the braces */
-.rainbow-braces .token.token.punctuation.brace-level-1,
-.rainbow-braces .token.token.punctuation.brace-level-5,
-.rainbow-braces .token.token.punctuation.brace-level-9 {
- color: hsl(5, 74%, 59%);
-}
-
-.rainbow-braces .token.token.punctuation.brace-level-2,
-.rainbow-braces .token.token.punctuation.brace-level-6,
-.rainbow-braces .token.token.punctuation.brace-level-10 {
- color: hsl(119, 34%, 47%);
-}
-
-.rainbow-braces .token.token.punctuation.brace-level-3,
-.rainbow-braces .token.token.punctuation.brace-level-7,
-.rainbow-braces .token.token.punctuation.brace-level-11 {
- color: hsl(221, 87%, 60%);
-}
-
-.rainbow-braces .token.token.punctuation.brace-level-4,
-.rainbow-braces .token.token.punctuation.brace-level-8,
-.rainbow-braces .token.token.punctuation.brace-level-12 {
- color: hsl(301, 63%, 40%);
-}
-
-/* Diff Highlight plugin overrides */
-/* Taken from https://github.com/atom/github/blob/master/styles/variables.less */
-pre.diff-highlight > code .token.token.deleted:not(.prefix),
-pre > code.diff-highlight .token.token.deleted:not(.prefix) {
- background-color: hsla(353, 100%, 66%, 0.15);
-}
-
-pre.diff-highlight > code .token.token.deleted:not(.prefix)::-moz-selection,
-pre.diff-highlight > code .token.token.deleted:not(.prefix) *::-moz-selection,
-pre > code.diff-highlight .token.token.deleted:not(.prefix)::-moz-selection,
-pre > code.diff-highlight .token.token.deleted:not(.prefix) *::-moz-selection {
- background-color: hsla(353, 95%, 66%, 0.25);
-}
-
-pre.diff-highlight > code .token.token.deleted:not(.prefix)::selection,
-pre.diff-highlight > code .token.token.deleted:not(.prefix) *::selection,
-pre > code.diff-highlight .token.token.deleted:not(.prefix)::selection,
-pre > code.diff-highlight .token.token.deleted:not(.prefix) *::selection {
- background-color: hsla(353, 95%, 66%, 0.25);
-}
-
-pre.diff-highlight > code .token.token.inserted:not(.prefix),
-pre > code.diff-highlight .token.token.inserted:not(.prefix) {
- background-color: hsla(137, 100%, 55%, 0.15);
-}
-
-pre.diff-highlight > code .token.token.inserted:not(.prefix)::-moz-selection,
-pre.diff-highlight > code .token.token.inserted:not(.prefix) *::-moz-selection,
-pre > code.diff-highlight .token.token.inserted:not(.prefix)::-moz-selection,
-pre > code.diff-highlight .token.token.inserted:not(.prefix) *::-moz-selection {
- background-color: hsla(135, 73%, 55%, 0.25);
-}
-
-pre.diff-highlight > code .token.token.inserted:not(.prefix)::selection,
-pre.diff-highlight > code .token.token.inserted:not(.prefix) *::selection,
-pre > code.diff-highlight .token.token.inserted:not(.prefix)::selection,
-pre > code.diff-highlight .token.token.inserted:not(.prefix) *::selection {
- background-color: hsla(135, 73%, 55%, 0.25);
-}
-
-/* Previewers plugin overrides */
-/* Based on https://github.com/atom-community/atom-ide-datatip/blob/master/styles/atom-ide-datatips.less and https://github.com/atom/atom/blob/master/packages/one-light-ui */
-/* Border around popup */
-.prism-previewer.prism-previewer:before,
-.prism-previewer-gradient.prism-previewer-gradient div {
- border-color: hsl(0, 0, 95%);
-}
-
-/* Angle and time should remain as circles and are hence not included */
-.prism-previewer-color.prism-previewer-color:before,
-.prism-previewer-gradient.prism-previewer-gradient div,
-.prism-previewer-easing.prism-previewer-easing:before {
- border-radius: 0.3em;
-}
-
-/* Triangles pointing to the code */
-.prism-previewer.prism-previewer:after {
- border-top-color: hsl(0, 0, 95%);
-}
-
-.prism-previewer-flipped.prism-previewer-flipped.after {
- border-bottom-color: hsl(0, 0, 95%);
-}
-
-/* Background colour within the popup */
-.prism-previewer-angle.prism-previewer-angle:before,
-.prism-previewer-time.prism-previewer-time:before,
-.prism-previewer-easing.prism-previewer-easing {
- background: hsl(0, 0%, 100%);
-}
-
-/* For angle, this is the positive area (eg. 90deg will display one quadrant in this colour) */
-/* For time, this is the alternate colour */
-.prism-previewer-angle.prism-previewer-angle circle,
-.prism-previewer-time.prism-previewer-time circle {
- stroke: hsl(230, 8%, 24%);
- stroke-opacity: 1;
-}
-
-/* Stroke colours of the handle, direction point, and vector itself */
-.prism-previewer-easing.prism-previewer-easing circle,
-.prism-previewer-easing.prism-previewer-easing path,
-.prism-previewer-easing.prism-previewer-easing line {
- stroke: hsl(230, 8%, 24%);
-}
-
-/* Fill colour of the handle */
-.prism-previewer-easing.prism-previewer-easing circle {
- fill: transparent;
-}
diff --git a/apps/web/app/components/plate-ui/code-block-element.tsx b/apps/web/app/components/plate-ui/code-block-element.tsx
deleted file mode 100644
index afc18a52..00000000
--- a/apps/web/app/components/plate-ui/code-block-element.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn, withRef } from '@udecode/cn';
-import { useCodeBlockElementState } from '@udecode/plate-code-block/react';
-
-import { CodeBlockCombobox } from './code-block-combobox';
-import { PlateElement } from './plate-element';
-
-import './code-block-element.css';
-
-export const CodeBlockElement = withRef<typeof PlateElement>(
- ({ children, className, ...props }, ref) => {
- const { element } = props;
- const state = useCodeBlockElementState({ element });
-
- return (
- <PlateElement
- ref={ref}
- className={cn('relative py-1', state.className, className)}
- {...props}
- >
- <pre className="overflow-x-auto rounded-md bg-muted px-6 py-8 font-mono text-sm leading-[normal] [tab-size:2]">
- <code>{children}</code>
- </pre>
-
- {state.syntax && (
- <div
- className="absolute right-2 top-2 z-10 select-none"
- contentEditable={false}
- >
- <CodeBlockCombobox />
- </div>
- )}
- </PlateElement>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/code-leaf.tsx b/apps/web/app/components/plate-ui/code-leaf.tsx
deleted file mode 100644
index dd0c48a2..00000000
--- a/apps/web/app/components/plate-ui/code-leaf.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn, withRef } from '@udecode/cn';
-import { PlateLeaf } from '@udecode/plate-common/react';
-
-export const CodeLeaf = withRef<typeof PlateLeaf>(
- ({ children, className, ...props }, ref) => {
- return (
- <PlateLeaf
- ref={ref}
- asChild
- className={cn(
- 'whitespace-pre-wrap rounded-md bg-muted px-[0.3em] py-[0.2em] font-mono text-sm',
- className
- )}
- {...props}
- >
- <code>{children}</code>
- </PlateLeaf>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/code-line-element.tsx b/apps/web/app/components/plate-ui/code-line-element.tsx
deleted file mode 100644
index aa23472c..00000000
--- a/apps/web/app/components/plate-ui/code-line-element.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-'use client';
-
-
-import { withRef } from '@udecode/cn';
-
-import { PlateElement } from './plate-element';
-
-export const CodeLineElement = withRef<typeof PlateElement>((props, ref) => (
- <PlateElement ref={ref} {...props} />
-));
diff --git a/apps/web/app/components/plate-ui/code-syntax-leaf.tsx b/apps/web/app/components/plate-ui/code-syntax-leaf.tsx
deleted file mode 100644
index 5698f0b7..00000000
--- a/apps/web/app/components/plate-ui/code-syntax-leaf.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-'use client';
-
-
-import { withRef } from '@udecode/cn';
-import { useCodeSyntaxLeaf } from '@udecode/plate-code-block/react';
-import { PlateLeaf } from '@udecode/plate-common/react';
-
-export const CodeSyntaxLeaf = withRef<typeof PlateLeaf>(
- ({ children, ...props }, ref) => {
- const { leaf } = props;
-
- const { tokenProps } = useCodeSyntaxLeaf({ leaf });
-
- return (
- <PlateLeaf ref={ref} {...props}>
- <span {...tokenProps}>{children}</span>
- </PlateLeaf>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/color-constants.ts b/apps/web/app/components/plate-ui/color-constants.ts
deleted file mode 100644
index 1508c25f..00000000
--- a/apps/web/app/components/plate-ui/color-constants.ts
+++ /dev/null
@@ -1,436 +0,0 @@
-export const DEFAULT_COLORS = [
- {
- isBrightColor: false,
- name: 'black',
- value: '#000000',
- },
- {
- isBrightColor: false,
- name: 'dark grey 4',
- value: '#434343',
- },
- {
- isBrightColor: false,
- name: 'dark grey 3',
- value: '#666666',
- },
- {
- isBrightColor: false,
- name: 'dark grey 2',
- value: '#999999',
- },
- {
- isBrightColor: false,
- name: 'dark grey 1',
- value: '#B7B7B7',
- },
- {
- isBrightColor: false,
- name: 'grey',
- value: '#CCCCCC',
- },
- {
- isBrightColor: false,
- name: 'light grey 1',
- value: '#D9D9D9',
- },
- {
- isBrightColor: true,
- name: 'light grey 2',
- value: '#EFEFEF',
- },
- {
- isBrightColor: true,
- name: 'light grey 3',
- value: '#F3F3F3',
- },
- {
- isBrightColor: true,
- name: 'white',
- value: '#FFFFFF',
- },
- {
- isBrightColor: false,
- name: 'red berry',
- value: '#980100',
- },
- {
- isBrightColor: false,
- name: 'red',
- value: '#FE0000',
- },
- {
- isBrightColor: false,
- name: 'orange',
- value: '#FE9900',
- },
- {
- isBrightColor: true,
- name: 'yellow',
- value: '#FEFF00',
- },
- {
- isBrightColor: false,
- name: 'green',
- value: '#00FF00',
- },
- {
- isBrightColor: false,
- name: 'cyan',
- value: '#00FFFF',
- },
- {
- isBrightColor: false,
- name: 'cornflower blue',
- value: '#4B85E8',
- },
- {
- isBrightColor: false,
- name: 'blue',
- value: '#1300FF',
- },
- {
- isBrightColor: false,
- name: 'purple',
- value: '#9900FF',
- },
- {
- isBrightColor: false,
- name: 'magenta',
- value: '#FF00FF',
- },
-
- {
- isBrightColor: false,
- name: 'light red berry 3',
- value: '#E6B8AF',
- },
- {
- isBrightColor: false,
- name: 'light red 3',
- value: '#F4CCCC',
- },
- {
- isBrightColor: true,
- name: 'light orange 3',
- value: '#FCE4CD',
- },
- {
- isBrightColor: true,
- name: 'light yellow 3',
- value: '#FFF2CC',
- },
- {
- isBrightColor: true,
- name: 'light green 3',
- value: '#D9EAD3',
- },
- {
- isBrightColor: false,
- name: 'light cyan 3',
- value: '#D0DFE3',
- },
- {
- isBrightColor: false,
- name: 'light cornflower blue 3',
- value: '#C9DAF8',
- },
- {
- isBrightColor: true,
- name: 'light blue 3',
- value: '#CFE1F3',
- },
- {
- isBrightColor: true,
- name: 'light purple 3',
- value: '#D9D2E9',
- },
- {
- isBrightColor: true,
- name: 'light magenta 3',
- value: '#EAD1DB',
- },
-
- {
- isBrightColor: false,
- name: 'light red berry 2',
- value: '#DC7E6B',
- },
- {
- isBrightColor: false,
- name: 'light red 2',
- value: '#EA9999',
- },
- {
- isBrightColor: false,
- name: 'light orange 2',
- value: '#F9CB9C',
- },
- {
- isBrightColor: true,
- name: 'light yellow 2',
- value: '#FFE598',
- },
- {
- isBrightColor: false,
- name: 'light green 2',
- value: '#B7D6A8',
- },
- {
- isBrightColor: false,
- name: 'light cyan 2',
- value: '#A1C4C9',
- },
- {
- isBrightColor: false,
- name: 'light cornflower blue 2',
- value: '#A4C2F4',
- },
- {
- isBrightColor: false,
- name: 'light blue 2',
- value: '#9FC5E8',
- },
- {
- isBrightColor: false,
- name: 'light purple 2',
- value: '#B5A7D5',
- },
- {
- isBrightColor: false,
- name: 'light magenta 2',
- value: '#D5A6BD',
- },
-
- {
- isBrightColor: false,
- name: 'light red berry 1',
- value: '#CC4125',
- },
- {
- isBrightColor: false,
- name: 'light red 1',
- value: '#E06666',
- },
- {
- isBrightColor: false,
- name: 'light orange 1',
- value: '#F6B26B',
- },
- {
- isBrightColor: false,
- name: 'light yellow 1',
- value: '#FFD966',
- },
- {
- isBrightColor: false,
- name: 'light green 1',
- value: '#93C47D',
- },
- {
- isBrightColor: false,
- name: 'light cyan 1',
- value: '#76A5AE',
- },
- {
- isBrightColor: false,
- name: 'light cornflower blue 1',
- value: '#6C9EEB',
- },
- {
- isBrightColor: false,
- name: 'light blue 1',
- value: '#6FA8DC',
- },
- {
- isBrightColor: false,
- name: 'light purple 1',
- value: '#8D7CC3',
- },
- {
- isBrightColor: false,
- name: 'light magenta 1',
- value: '#C27BA0',
- },
-
- {
- isBrightColor: false,
- name: 'dark red berry 1',
- value: '#A61B00',
- },
- {
- isBrightColor: false,
- name: 'dark red 1',
- value: '#CC0000',
- },
- {
- isBrightColor: false,
- name: 'dark orange 1',
- value: '#E59138',
- },
- {
- isBrightColor: false,
- name: 'dark yellow 1',
- value: '#F1C231',
- },
- {
- isBrightColor: false,
- name: 'dark green 1',
- value: '#6AA74F',
- },
- {
- isBrightColor: false,
- name: 'dark cyan 1',
- value: '#45818E',
- },
- {
- isBrightColor: false,
- name: 'dark cornflower blue 1',
- value: '#3B78D8',
- },
- {
- isBrightColor: false,
- name: 'dark blue 1',
- value: '#3E84C6',
- },
- {
- isBrightColor: false,
- name: 'dark purple 1',
- value: '#664EA6',
- },
- {
- isBrightColor: false,
- name: 'dark magenta 1',
- value: '#A64D78',
- },
-
- {
- isBrightColor: false,
- name: 'dark red berry 2',
- value: '#84200D',
- },
- {
- isBrightColor: false,
- name: 'dark red 2',
- value: '#990001',
- },
- {
- isBrightColor: false,
- name: 'dark orange 2',
- value: '#B45F05',
- },
- {
- isBrightColor: false,
- name: 'dark yellow 2',
- value: '#BF9002',
- },
- {
- isBrightColor: false,
- name: 'dark green 2',
- value: '#38761D',
- },
- {
- isBrightColor: false,
- name: 'dark cyan 2',
- value: '#124F5C',
- },
- {
- isBrightColor: false,
- name: 'dark cornflower blue 2',
- value: '#1155CB',
- },
- {
- isBrightColor: false,
- name: 'dark blue 2',
- value: '#0C5394',
- },
- {
- isBrightColor: false,
- name: 'dark purple 2',
- value: '#351C75',
- },
- {
- isBrightColor: false,
- name: 'dark magenta 2',
- value: '#741B47',
- },
-
- {
- isBrightColor: false,
- name: 'dark red berry 3',
- value: '#5B0F00',
- },
- {
- isBrightColor: false,
- name: 'dark red 3',
- value: '#660000',
- },
- {
- isBrightColor: false,
- name: 'dark orange 3',
- value: '#783F04',
- },
- {
- isBrightColor: false,
- name: 'dark yellow 3',
- value: '#7E6000',
- },
- {
- isBrightColor: false,
- name: 'dark green 3',
- value: '#274E12',
- },
- {
- isBrightColor: false,
- name: 'dark cyan 3',
- value: '#0D343D',
- },
- {
- isBrightColor: false,
- name: 'dark cornflower blue 3',
- value: '#1B4487',
- },
- {
- isBrightColor: false,
- name: 'dark blue 3',
- value: '#083763',
- },
- {
- isBrightColor: false,
- name: 'dark purple 3',
- value: '#1F124D',
- },
- {
- isBrightColor: false,
- name: 'dark magenta 3',
- value: '#4C1130',
- },
-];
-
-export const DEFAULT_CUSTOM_COLORS = [
- {
- isBrightColor: false,
- name: 'dark orange 3',
- value: '#783F04',
- },
- {
- isBrightColor: false,
- name: 'dark grey 3',
- value: '#666666',
- },
- {
- isBrightColor: false,
- name: 'dark grey 2',
- value: '#999999',
- },
- {
- isBrightColor: false,
- name: 'light cornflower blue 1',
- value: '#6C9EEB',
- },
- {
- isBrightColor: false,
- name: 'dark magenta 3',
- value: '#4C1130',
- },
-];
diff --git a/apps/web/app/components/plate-ui/color-dropdown-menu-items.tsx b/apps/web/app/components/plate-ui/color-dropdown-menu-items.tsx
deleted file mode 100644
index 8714bdd9..00000000
--- a/apps/web/app/components/plate-ui/color-dropdown-menu-items.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { DropdownMenuItemProps } from '@radix-ui/react-dropdown-menu';
-
-import { cn } from '@udecode/cn';
-import { Check } from 'lucide-react';
-
-import { buttonVariants } from './button';
-import { DropdownMenuItem } from './dropdown-menu';
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from './tooltip';
-
-export type TColor = {
- isBrightColor: boolean;
- name: string;
- value: string;
-};
-
-type ColorDropdownMenuItemProps = {
- isBrightColor: boolean;
- isSelected: boolean;
- updateColor: (color: string) => void;
- value: string;
- name?: string;
-} & DropdownMenuItemProps;
-
-export function ColorDropdownMenuItem({
- className,
- isBrightColor,
- isSelected,
- name,
- updateColor,
- value,
- ...props
-}: ColorDropdownMenuItemProps) {
- const content = (
- <DropdownMenuItem
- className={cn(
- buttonVariants({
- isMenu: true,
- size: 'icon',
- variant: 'outline',
- }),
- 'my-1 flex size-6 items-center justify-center rounded-full border border-solid border-muted p-0 transition-all hover:scale-125',
- !isBrightColor && 'border-transparent text-white hover:!text-white',
- className
- )}
- style={{ backgroundColor: value }}
- onSelect={(e) => {
- e.preventDefault();
- updateColor(value);
- }}
- {...props}
- >
- {isSelected ? <Check className="!size-3" /> : null}
- </DropdownMenuItem>
- );
-
- return name ? (
- <Tooltip>
- <TooltipTrigger>{content}</TooltipTrigger>
- <TooltipContent className="mb-1 capitalize">{name}</TooltipContent>
- </Tooltip>
- ) : (
- content
- );
-}
-
-type ColorDropdownMenuItemsProps = {
- colors: TColor[];
- updateColor: (color: string) => void;
- color?: string;
-} & React.HTMLAttributes<HTMLDivElement>;
-
-export function ColorDropdownMenuItems({
- className,
- color,
- colors,
- updateColor,
- ...props
-}: ColorDropdownMenuItemsProps) {
- return (
- <div
- className={cn(
- 'grid grid-cols-[repeat(10,1fr)] place-items-center',
- className
- )}
- {...props}
- >
- <TooltipProvider>
- {colors.map(({ isBrightColor, name, value }) => (
- <ColorDropdownMenuItem
- name={name}
- key={name ?? value}
- value={value}
- isBrightColor={isBrightColor}
- isSelected={color === value}
- updateColor={updateColor}
- />
- ))}
- {props.children}
- </TooltipProvider>
- </div>
- );
-}
diff --git a/apps/web/app/components/plate-ui/color-dropdown-menu.tsx b/apps/web/app/components/plate-ui/color-dropdown-menu.tsx
deleted file mode 100644
index 924f77c2..00000000
--- a/apps/web/app/components/plate-ui/color-dropdown-menu.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-'use client';
-
-import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
-
-import {
- useColorDropdownMenu,
- useColorDropdownMenuState,
-} from '@udecode/plate-font/react';
-
-import { DEFAULT_COLORS, DEFAULT_CUSTOM_COLORS } from './color-constants';
-import { ColorPicker } from './color-picker';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuTrigger,
-} from './dropdown-menu';
-import { ToolbarButton } from './toolbar';
-
-type ColorDropdownMenuProps = {
- nodeType: string;
- tooltip?: string;
-} & DropdownMenuProps;
-
-export function ColorDropdownMenu({
- children,
- nodeType,
- tooltip,
-}: ColorDropdownMenuProps) {
- const state = useColorDropdownMenuState({
- closeOnSelect: true,
- colors: DEFAULT_COLORS,
- customColors: DEFAULT_CUSTOM_COLORS,
- nodeType,
- });
-
- const { buttonProps, menuProps } = useColorDropdownMenu(state);
-
- return (
- <DropdownMenu modal={false} {...menuProps}>
- <DropdownMenuTrigger asChild>
- <ToolbarButton tooltip={tooltip} {...buttonProps}>
- {children}
- </ToolbarButton>
- </DropdownMenuTrigger>
-
- <DropdownMenuContent align="start">
- <ColorPicker
- color={state.selectedColor || state.color}
- clearColor={state.clearColor}
- colors={state.colors}
- customColors={state.customColors}
- updateColor={state.updateColorAndClose}
- updateCustomColor={state.updateColor}
- />
- </DropdownMenuContent>
- </DropdownMenu>
- );
-}
diff --git a/apps/web/app/components/plate-ui/color-input.tsx b/apps/web/app/components/plate-ui/color-input.tsx
deleted file mode 100644
index 749b9ec8..00000000
--- a/apps/web/app/components/plate-ui/color-input.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn, withRef } from '@udecode/cn';
-import { useComposedRef } from '@udecode/plate-common/react';
-import { useColorInput } from '@udecode/plate-font/react';
-
-export const ColorInput = withRef<'input'>(
- ({ children, className, value = '#000000', ...props }, ref) => {
- const { childProps, inputRef } = useColorInput();
-
- return (
- <div className="flex flex-col items-center">
- {React.Children.map(children, (child) => {
- if (!child) return child;
-
- return React.cloneElement(child as React.ReactElement, childProps);
- })}
-
- <input
- ref={useComposedRef(ref, inputRef)}
- className={cn('size-0 overflow-hidden border-0 p-0', className)}
- value={value}
- type="color"
- {...props}
- />
- </div>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/color-picker.tsx b/apps/web/app/components/plate-ui/color-picker.tsx
deleted file mode 100644
index e51c8dc5..00000000
--- a/apps/web/app/components/plate-ui/color-picker.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn, withRef } from '@udecode/cn';
-import { EraserIcon } from 'lucide-react';
-
-import {
- type TColor,
- ColorDropdownMenuItems,
-} from './color-dropdown-menu-items';
-import { ColorCustom } from './colors-custom';
-import { DropdownMenuGroup, DropdownMenuItem } from './dropdown-menu';
-
-export const ColorPickerContent = withRef<
- 'div',
- {
- clearColor: () => void;
- colors: TColor[];
- customColors: TColor[];
- updateColor: (color: string) => void;
- updateCustomColor: (color: string) => void;
- color?: string;
- }
->(
- (
- {
- className,
- clearColor,
- color,
- colors,
- customColors,
- updateColor,
- updateCustomColor,
- ...props
- },
- ref
- ) => {
- return (
- <div ref={ref} className={cn('flex flex-col', className)} {...props}>
- <DropdownMenuGroup label="Custom Colors">
- <ColorCustom
- color={color}
- className="px-2"
- colors={colors}
- customColors={customColors}
- updateColor={updateColor}
- updateCustomColor={updateCustomColor}
- />
- </DropdownMenuGroup>
- <DropdownMenuGroup label="Default Colors">
- <ColorDropdownMenuItems
- color={color}
- className="px-2"
- colors={colors}
- updateColor={updateColor}
- />
- </DropdownMenuGroup>
- {color && (
- <DropdownMenuGroup>
- <DropdownMenuItem className="p-2" onClick={clearColor}>
- <EraserIcon />
- <span>Clear</span>
- </DropdownMenuItem>
- </DropdownMenuGroup>
- )}
- </div>
- );
- }
-);
-
-export const ColorPicker = React.memo(
- ColorPickerContent,
- (prev, next) =>
- prev.color === next.color &&
- prev.colors === next.colors &&
- prev.customColors === next.customColors
-);
diff --git a/apps/web/app/components/plate-ui/colors-custom.tsx b/apps/web/app/components/plate-ui/colors-custom.tsx
deleted file mode 100644
index e5568e72..00000000
--- a/apps/web/app/components/plate-ui/colors-custom.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-'use client';
-
-import React, { type ComponentPropsWithoutRef } from 'react';
-
-import { cn } from '@udecode/cn';
-import {
- useColorsCustom,
- useColorsCustomState,
-} from '@udecode/plate-font/react';
-import { PlusIcon } from 'lucide-react';
-
-import { buttonVariants } from './button';
-import {
- type TColor,
- ColorDropdownMenuItems,
-} from './color-dropdown-menu-items';
-import { ColorInput } from './color-input';
-
-// import { ColorInput } from './color-input';
-import { DropdownMenuItem } from './dropdown-menu';
-
-type ColorCustomProps = {
- colors: TColor[];
- customColors: TColor[];
- updateColor: (color: string) => void;
- updateCustomColor: (color: string) => void;
- color?: string;
-} & ComponentPropsWithoutRef<'div'>;
-
-export function ColorCustom({
- className,
- color,
- colors,
- customColors,
- updateColor,
- updateCustomColor,
- ...props
-}: ColorCustomProps) {
- const state = useColorsCustomState({
- color,
- colors,
- customColors,
- updateCustomColor,
- });
- const { inputProps, menuItemProps } = useColorsCustom(state);
-
- return (
- <div className={cn('relative flex flex-col gap-4', className)} {...props}>
- <ColorDropdownMenuItems
- color={color}
- colors={state.computedColors}
- updateColor={updateColor}
- >
- <ColorInput {...inputProps}>
- <DropdownMenuItem
- className={cn(
- buttonVariants({
- isMenu: true,
- size: 'icon',
- variant: 'outline',
- }),
- 'absolute bottom-2 right-2 top-1.5 flex size-7 items-center justify-center rounded-full'
- )}
- {...menuItemProps}
- >
- <span className="sr-only">Custom</span>
- <PlusIcon />
- </DropdownMenuItem>
- </ColorInput>
- </ColorDropdownMenuItems>
- </div>
- );
-}
diff --git a/apps/web/app/components/plate-ui/column-element.tsx b/apps/web/app/components/plate-ui/column-element.tsx
deleted file mode 100644
index 7d22126f..00000000
--- a/apps/web/app/components/plate-ui/column-element.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { TColumnElement } from '@udecode/plate-layout';
-
-import { cn, withRef } from '@udecode/cn';
-import { useElement, withHOC } from '@udecode/plate-common/react';
-import { ResizableProvider } from '@udecode/plate-resizable';
-import { useReadOnly } from 'slate-react';
-
-import { PlateElement } from './plate-element';
-
-export const ColumnElement = withHOC(
- ResizableProvider,
- withRef<typeof PlateElement>(({ children, className, ...props }, ref) => {
- const readOnly = useReadOnly();
- const { width } = useElement<TColumnElement>();
-
- return (
- <PlateElement
- ref={ref}
- className={cn(
- className,
- !readOnly && 'rounded-lg border border-dashed p-1.5'
- )}
- style={{ width: width ?? '100%' }}
- {...props}
- >
- {children}
- </PlateElement>
- );
- })
-);
diff --git a/apps/web/app/components/plate-ui/column-group-element.tsx b/apps/web/app/components/plate-ui/column-group-element.tsx
deleted file mode 100644
index 10d903bd..00000000
--- a/apps/web/app/components/plate-ui/column-group-element.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { TColumnElement } from '@udecode/plate-layout';
-
-import { cn, withRef } from '@udecode/cn';
-import { useElement, useRemoveNodeButton } from '@udecode/plate-common/react';
-import {
- ColumnItemPlugin,
- useColumnState,
- useDebouncePopoverOpen,
-} from '@udecode/plate-layout/react';
-import { type LucideProps, Trash2Icon } from 'lucide-react';
-import { useReadOnly } from 'slate-react';
-
-import { Button } from './button';
-import { PlateElement } from './plate-element';
-import { Popover, PopoverAnchor, PopoverContent } from './popover';
-import { Separator } from './separator';
-
-export const ColumnGroupElement = withRef<typeof PlateElement>(
- ({ children, className, ...props }, ref) => {
- return (
- <PlateElement ref={ref} className={cn(className, 'my-2')} {...props}>
- <ColumnFloatingToolbar>
- <div className={cn('flex size-full gap-4 rounded')}>{children}</div>
- </ColumnFloatingToolbar>
- </PlateElement>
- );
- }
-);
-
-export function ColumnFloatingToolbar({ children }: React.PropsWithChildren) {
- const readOnly = useReadOnly();
-
- const {
- setDoubleColumn,
- setDoubleSideDoubleColumn,
- setLeftSideDoubleColumn,
- setRightSideDoubleColumn,
- setThreeColumn,
- } = useColumnState();
-
- const element = useElement<TColumnElement>(ColumnItemPlugin.key);
-
- const { props: buttonProps } = useRemoveNodeButton({ element });
-
- const isOpen = useDebouncePopoverOpen();
-
- if (readOnly) return <>{children}</>;
-
- return (
- <Popover open={isOpen} modal={false}>
- <PopoverAnchor>{children}</PopoverAnchor>
- <PopoverContent
- className="w-auto p-1"
- onOpenAutoFocus={(e) => e.preventDefault()}
- align="center"
- side="top"
- sideOffset={10}
- >
- <div className="box-content flex items-center [&_svg]:size-4 [&_svg]:text-muted-foreground">
- <Button size="icon" variant="ghost" onClick={setDoubleColumn}>
- <DoubleColumnOutlined />
- </Button>
- <Button size="icon" variant="ghost" onClick={setThreeColumn}>
- <ThreeColumnOutlined />
- </Button>
- <Button
- size="icon"
- variant="ghost"
- onClick={setRightSideDoubleColumn}
- >
- <RightSideDoubleColumnOutlined />
- </Button>
- <Button size="icon" variant="ghost" onClick={setLeftSideDoubleColumn}>
- <LeftSideDoubleColumnOutlined />
- </Button>
- <Button
- size="icon"
- variant="ghost"
- onClick={setDoubleSideDoubleColumn}
- >
- <DoubleSideDoubleColumnOutlined />
- </Button>
-
- <Separator orientation="vertical" className="mx-1 h-6" />
- <Button size="icon" variant="ghost" {...buttonProps}>
- <Trash2Icon />
- </Button>
- </div>
- </PopoverContent>
- </Popover>
- );
-}
-
-const DoubleColumnOutlined = (props: LucideProps) => (
- <svg
- fill="none"
- height="16"
- viewBox="0 0 16 16"
- width="16"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <path
- clipRule="evenodd"
- d="M8.5 3H13V13H8.5V3ZM7.5 2H8.5H13C13.5523 2 14 2.44772 14 3V13C14 13.5523 13.5523 14 13 14H8.5H7.5H3C2.44772 14 2 13.5523 2 13V3C2 2.44772 2.44772 2 3 2H7.5ZM7.5 13H3L3 3H7.5V13Z"
- fill="#595E6F"
- fillRule="evenodd"
- />
- </svg>
-);
-
-const ThreeColumnOutlined = (props: LucideProps) => (
- <svg
- fill="none"
- height="16"
- viewBox="0 0 16 16"
- width="16"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <path
- clipRule="evenodd"
- d="M9.25 3H6.75V13H9.25V3ZM9.25 2H6.75H5.75H3C2.44772 2 2 2.44772 2 3V13C2 13.5523 2.44772 14 3 14H5.75H6.75H9.25H10.25H13C13.5523 14 14 13.5523 14 13V3C14 2.44772 13.5523 2 13 2H10.25H9.25ZM10.25 3V13H13V3H10.25ZM3 13H5.75V3H3L3 13Z"
- fill="#4C5161"
- fillRule="evenodd"
- />
- </svg>
-);
-
-const RightSideDoubleColumnOutlined = (props: LucideProps) => (
- <svg
- fill="none"
- height="16"
- viewBox="0 0 16 16"
- width="16"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <path
- clipRule="evenodd"
- d="M11.25 3H13V13H11.25V3ZM10.25 2H11.25H13C13.5523 2 14 2.44772 14 3V13C14 13.5523 13.5523 14 13 14H11.25H10.25H3C2.44772 14 2 13.5523 2 13V3C2 2.44772 2.44772 2 3 2H10.25ZM10.25 13H3L3 3H10.25V13Z"
- fill="#595E6F"
- fillRule="evenodd"
- />
- </svg>
-);
-
-const LeftSideDoubleColumnOutlined = (props: LucideProps) => (
- <svg
- fill="none"
- height="16"
- viewBox="0 0 16 16"
- width="16"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <path
- clipRule="evenodd"
- d="M5.75 3H13V13H5.75V3ZM4.75 2H5.75H13C13.5523 2 14 2.44772 14 3V13C14 13.5523 13.5523 14 13 14H5.75H4.75H3C2.44772 14 2 13.5523 2 13V3C2 2.44772 2.44772 2 3 2H4.75ZM4.75 13H3L3 3H4.75V13Z"
- fill="#595E6F"
- fillRule="evenodd"
- />
- </svg>
-);
-
-const DoubleSideDoubleColumnOutlined = (props: LucideProps) => (
- <svg
- fill="none"
- height="16"
- viewBox="0 0 16 16"
- width="16"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <path
- clipRule="evenodd"
- d="M10.25 3H5.75V13H10.25V3ZM10.25 2H5.75H4.75H3C2.44772 2 2 2.44772 2 3V13C2 13.5523 2.44772 14 3 14H4.75H5.75H10.25H11.25H13C13.5523 14 14 13.5523 14 13V3C14 2.44772 13.5523 2 13 2H11.25H10.25ZM11.25 3V13H13V3H11.25ZM3 13H4.75V3H3L3 13Z"
- fill="#595E6F"
- fillRule="evenodd"
- />
- </svg>
-);
diff --git a/apps/web/app/components/plate-ui/command.tsx b/apps/web/app/components/plate-ui/command.tsx
deleted file mode 100644
index 0a4b9dc1..00000000
--- a/apps/web/app/components/plate-ui/command.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-'use client';
-
-import * as React from 'react';
-
-import type { DialogProps } from '@radix-ui/react-dialog';
-
-import {
- cn,
- createPrimitiveElement,
- withCn,
- withRef,
- withVariants,
-} from '@udecode/cn';
-import { Command as CommandPrimitive } from 'cmdk';
-import { Search } from 'lucide-react';
-
-import { Dialog, DialogContent, DialogTitle } from './dialog';
-import { inputVariants } from './input';
-
-export const Command = withCn(
- CommandPrimitive,
- 'flex size-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground'
-);
-
-export function CommandDialog({ children, ...props }: DialogProps) {
- return (
- <Dialog {...props}>
- <DialogContent className="overflow-hidden p-0 shadow-lg">
- <DialogTitle className="sr-only">Command Dialog</DialogTitle>
- <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:size-5">
- {children}
- </Command>
- </DialogContent>
- </Dialog>
- );
-}
-
-export const CommandInput = withRef<typeof CommandPrimitive.Input>(
- ({ className, ...props }, ref) => (
- <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
- <Search className="mr-2 size-4 shrink-0 opacity-50" />
- <CommandPrimitive.Input
- ref={ref}
- className={cn(
- 'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
- className
- )}
- {...props}
- />
- </div>
- )
-);
-
-export const InputCommand = withVariants(
- CommandPrimitive.Input,
- inputVariants,
- ['variant']
-);
-
-export const CommandList = withCn(
- CommandPrimitive.List,
- 'max-h-[500px] overflow-y-auto overflow-x-hidden'
-);
-
-export const CommandEmpty = withCn(
- CommandPrimitive.Empty,
- 'py-6 text-center text-sm'
-);
-
-export const CommandGroup = withCn(
- CommandPrimitive.Group,
- 'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground'
-);
-
-export const CommandSeparator = withCn(
- CommandPrimitive.Separator,
- '-mx-1 h-px bg-border'
-);
-
-export const CommandItem = withCn(
- CommandPrimitive.Item,
- 'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0'
-);
-
-export const CommandShortcut = withCn(
- createPrimitiveElement('span'),
- 'ml-auto text-xs tracking-widest text-muted-foreground'
-);
diff --git a/apps/web/app/components/plate-ui/comment-avatar.tsx b/apps/web/app/components/plate-ui/comment-avatar.tsx
deleted file mode 100644
index f9a963ac..00000000
--- a/apps/web/app/components/plate-ui/comment-avatar.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { CommentsPlugin } from '@udecode/plate-comments/react';
-import { useEditorPlugin } from '@udecode/plate-common/react';
-
-import { Avatar, AvatarFallback, AvatarImage } from './avatar';
-
-export function CommentAvatar({ userId }: { userId: string | null }) {
- const { useOption } = useEditorPlugin(CommentsPlugin);
- const user = useOption('userById', userId);
-
- if (!user) return null;
-
- return (
- <Avatar className="size-5">
- <AvatarImage alt={user.name} src={user.avatarUrl} />
- <AvatarFallback>{user.name?.[0]}</AvatarFallback>
- </Avatar>
- );
-}
diff --git a/apps/web/app/components/plate-ui/comment-create-form.tsx b/apps/web/app/components/plate-ui/comment-create-form.tsx
deleted file mode 100644
index 3ccc6f82..00000000
--- a/apps/web/app/components/plate-ui/comment-create-form.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn } from '@udecode/cn';
-import {
- CommentNewSubmitButton,
- CommentNewTextarea,
- CommentsPlugin,
-} from '@udecode/plate-comments/react';
-import { useEditorPlugin } from '@udecode/plate-common/react';
-
-import { buttonVariants } from './button';
-import { CommentAvatar } from './comment-avatar';
-import { inputVariants } from './input';
-
-export function CommentCreateForm() {
- const { useOption } = useEditorPlugin(CommentsPlugin);
-
- const myUserId = useOption('myUserId');
-
- return (
- <div className="flex w-full space-x-2">
- <CommentAvatar userId={myUserId} />
-
- <div className="flex grow flex-col items-end gap-2">
- <CommentNewTextarea className={inputVariants()} />
-
- <CommentNewSubmitButton
- className={cn(buttonVariants({ size: 'sm' }), 'w-[90px]')}
- >
- Comment
- </CommentNewSubmitButton>
- </div>
- </div>
- );
-}
diff --git a/apps/web/app/components/plate-ui/comment-item.tsx b/apps/web/app/components/plate-ui/comment-item.tsx
deleted file mode 100644
index 07fcc476..00000000
--- a/apps/web/app/components/plate-ui/comment-item.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import {
- CommentProvider,
- CommentsPlugin,
- useCommentItemContentState,
-} from '@udecode/plate-comments/react';
-import { useEditorPlugin } from '@udecode/plate-common/react';
-import { formatDistance } from 'date-fns';
-
-import { CommentAvatar } from './comment-avatar';
-import { CommentMoreDropdown } from './comment-more-dropdown';
-import { CommentResolveButton } from './comment-resolve-button';
-import { CommentValue } from './comment-value';
-
-type PlateCommentProps = {
- commentId: string;
-};
-
-function CommentItemContent() {
- const {
- comment,
- commentText,
- editingValue,
- isMyComment,
- isReplyComment,
- user,
- } = useCommentItemContentState();
-
- return (
- <div>
- <div className="relative flex items-center gap-2">
- <CommentAvatar userId={comment.userId} />
-
- <h4 className="text-sm font-semibold leading-none">{user?.name}</h4>
-
- <div className="text-xs leading-none text-muted-foreground">
- {formatDistance(comment.createdAt, Date.now())} ago
- </div>
-
- {isMyComment && (
- <div className="absolute -right-0.5 -top-0.5 flex space-x-1">
- {isReplyComment ? null : <CommentResolveButton />}
-
- <CommentMoreDropdown />
- </div>
- )}
- </div>
-
- <div className="mb-4 pl-7 pt-0.5">
- {editingValue ? (
- <CommentValue />
- ) : (
- <div className="whitespace-pre-wrap text-sm">{commentText}</div>
- )}
- </div>
- </div>
- );
-}
-
-export function CommentItem({ commentId }: PlateCommentProps) {
- const { useOption } = useEditorPlugin(CommentsPlugin);
- const comment = useOption('commentById', commentId);
-
- if (!comment) return null;
-
- return (
- <CommentProvider id={commentId} key={commentId}>
- <CommentItemContent />
- </CommentProvider>
- );
-}
diff --git a/apps/web/app/components/plate-ui/comment-leaf.tsx b/apps/web/app/components/plate-ui/comment-leaf.tsx
deleted file mode 100644
index 0751d01d..00000000
--- a/apps/web/app/components/plate-ui/comment-leaf.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { TCommentText } from '@udecode/plate-comments';
-
-import { cn } from '@udecode/cn';
-import {
- useCommentLeaf,
- useCommentLeafState,
-} from '@udecode/plate-comments/react';
-import { type PlateLeafProps, PlateLeaf } from '@udecode/plate-common/react';
-
-export function CommentLeaf({
- className,
- ...props
-}: PlateLeafProps<TCommentText>) {
- const { children, leaf, nodeProps } = props;
-
- const state = useCommentLeafState({ leaf });
- const { props: rootProps } = useCommentLeaf(state);
-
- if (!state.commentCount) return <>{children}</>;
-
- let aboveChildren = <>{children}</>;
-
- if (!state.isActive) {
- for (let i = 1; i < state.commentCount; i++) {
- aboveChildren = <span className="bg-highlight/25">{aboveChildren}</span>;
- }
- }
-
- return (
- <PlateLeaf
- {...props}
- className={cn(
- 'border-b-2 border-b-highlight/35 hover:bg-highlight/25',
- state.isActive ? 'bg-highlight/25' : 'bg-highlight/15',
- className
- )}
- nodeProps={{
- ...rootProps,
- ...nodeProps,
- }}
- >
- {aboveChildren}
- </PlateLeaf>
- );
-}
diff --git a/apps/web/app/components/plate-ui/comment-more-dropdown.tsx b/apps/web/app/components/plate-ui/comment-more-dropdown.tsx
deleted file mode 100644
index 2a5316e5..00000000
--- a/apps/web/app/components/plate-ui/comment-more-dropdown.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn } from '@udecode/cn';
-import {
- useCommentDeleteButton,
- useCommentDeleteButtonState,
- useCommentEditButton,
- useCommentEditButtonState,
-} from '@udecode/plate-comments/react';
-import { MoreHorizontal } from 'lucide-react';
-
-import { Button } from './button';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from './dropdown-menu';
-
-export function CommentMoreDropdown() {
- const editButtonState = useCommentEditButtonState();
- const { props: editProps } = useCommentEditButton(editButtonState);
- const deleteButtonState = useCommentDeleteButtonState();
- const { props: deleteProps } = useCommentDeleteButton(deleteButtonState);
-
- return (
- <DropdownMenu modal={false}>
- <DropdownMenuTrigger asChild>
- <Button variant="ghost" className={cn('h-6 p-1 text-muted-foreground')}>
- <MoreHorizontal className="size-4" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent>
- <DropdownMenuGroup>
- <DropdownMenuItem {...editProps}>Edit comment</DropdownMenuItem>
- <DropdownMenuItem {...deleteProps}>Delete comment</DropdownMenuItem>
- </DropdownMenuGroup>
- </DropdownMenuContent>
- </DropdownMenu>
- );
-}
diff --git a/apps/web/app/components/plate-ui/comment-reply-items.tsx b/apps/web/app/components/plate-ui/comment-reply-items.tsx
deleted file mode 100644
index 543a13f5..00000000
--- a/apps/web/app/components/plate-ui/comment-reply-items.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import {
- SCOPE_ACTIVE_COMMENT,
- useCommentReplies,
-} from '@udecode/plate-comments/react';
-
-import { CommentItem } from './comment-item';
-
-export function CommentReplyItems() {
- const commentReplies = useCommentReplies(SCOPE_ACTIVE_COMMENT);
-
- return (
- <>
- {Object.keys(commentReplies).map((id) => (
- <CommentItem key={id} commentId={id} />
- ))}
- </>
- );
-}
diff --git a/apps/web/app/components/plate-ui/comment-resolve-button.tsx b/apps/web/app/components/plate-ui/comment-resolve-button.tsx
deleted file mode 100644
index 239a59ac..00000000
--- a/apps/web/app/components/plate-ui/comment-resolve-button.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn } from '@udecode/cn';
-import {
- CommentResolveButton as CommentResolveButtonPrimitive,
- useComment,
-} from '@udecode/plate-comments/react';
-import { Check, RotateCcw } from 'lucide-react';
-
-import { buttonVariants } from './button';
-
-export function CommentResolveButton() {
- const comment = useComment()!;
-
- return (
- <CommentResolveButtonPrimitive
- className={cn(
- buttonVariants({ variant: 'ghost' }),
- 'h-6 p-1 text-muted-foreground'
- )}
- >
- {comment.isResolved ? (
- <RotateCcw className="size-4" />
- ) : (
- <Check className="size-4" />
- )}
- </CommentResolveButtonPrimitive>
- );
-}
diff --git a/apps/web/app/components/plate-ui/comment-toolbar-button.tsx b/apps/web/app/components/plate-ui/comment-toolbar-button.tsx
deleted file mode 100644
index 16f8ac08..00000000
--- a/apps/web/app/components/plate-ui/comment-toolbar-button.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { useCommentAddButton } from '@udecode/plate-comments/react';
-import { MessageSquarePlus } from 'lucide-react';
-
-import { ToolbarButton } from './toolbar';
-
-export function CommentToolbarButton() {
- const { hidden, props } = useCommentAddButton();
-
- if (hidden) return null;
-
- return (
- <ToolbarButton tooltip="Comment (⌘+⇧+M)" {...props}>
- <MessageSquarePlus />
- </ToolbarButton>
- );
-}
diff --git a/apps/web/app/components/plate-ui/comment-value.tsx b/apps/web/app/components/plate-ui/comment-value.tsx
deleted file mode 100644
index 9c531484..00000000
--- a/apps/web/app/components/plate-ui/comment-value.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn } from '@udecode/cn';
-import {
- CommentEditActions,
- CommentEditTextarea,
-} from '@udecode/plate-comments/react';
-
-import { buttonVariants } from './button';
-import { inputVariants } from './input';
-
-export function CommentValue() {
- return (
- <div className="my-2 flex flex-col items-end gap-2">
- <CommentEditTextarea className={cn(inputVariants(), 'min-h-[60px]')} />
-
- <div className="flex space-x-2">
- <CommentEditActions.CancelButton
- className={buttonVariants({ size: 'xs', variant: 'outline' })}
- >
- Cancel
- </CommentEditActions.CancelButton>
-
- <CommentEditActions.SaveButton
- className={buttonVariants({ size: 'xs', variant: 'default' })}
- >
- Save
- </CommentEditActions.SaveButton>
- </div>
- </div>
- );
-}
diff --git a/apps/web/app/components/plate-ui/comments-popover.tsx b/apps/web/app/components/plate-ui/comments-popover.tsx
deleted file mode 100644
index 298e19a9..00000000
--- a/apps/web/app/components/plate-ui/comments-popover.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn } from '@udecode/cn';
-import {
- CommentProvider,
- CommentsPositioner,
- SCOPE_ACTIVE_COMMENT,
- useFloatingCommentsContentState,
- useFloatingCommentsState,
-} from '@udecode/plate-comments/react';
-import { PortalBody } from '@udecode/plate-common/react';
-
-import { CommentCreateForm } from './comment-create-form';
-import { CommentItem } from './comment-item';
-import { CommentReplyItems } from './comment-reply-items';
-import { popoverVariants } from './popover';
-
-export type FloatingCommentsContentProps = {
- disableForm?: boolean;
-};
-
-export function CommentsPopoverContent(props: FloatingCommentsContentProps) {
- const { disableForm } = props;
-
- const { activeCommentId, hasNoComment, myUserId, ref } =
- useFloatingCommentsContentState();
-
- return (
- <CommentProvider
- id={activeCommentId!}
- key={activeCommentId}
- scope={SCOPE_ACTIVE_COMMENT}
- >
- <div ref={ref} className={cn(popoverVariants(), 'relative w-[310px]')}>
- {!hasNoComment && (
- <>
- <CommentItem key={activeCommentId} commentId={activeCommentId!} />
-
- <CommentReplyItems />
- </>
- )}
-
- {!!myUserId && !disableForm && <CommentCreateForm />}
- </div>
- </CommentProvider>
- );
-}
-
-export function CommentsPopover() {
- const { activeCommentId, loaded } = useFloatingCommentsState();
-
- if (!loaded || !activeCommentId) return null;
-
- return (
- <PortalBody>
- <CommentsPositioner className="absolute z-50 w-[418px] pb-4" data-popover>
- <CommentsPopoverContent />
- </CommentsPositioner>
- </PortalBody>
- );
-}
diff --git a/apps/web/app/components/plate-ui/context-menu.tsx b/apps/web/app/components/plate-ui/context-menu.tsx
deleted file mode 100644
index baf1d1f1..00000000
--- a/apps/web/app/components/plate-ui/context-menu.tsx
+++ /dev/null
@@ -1,229 +0,0 @@
-'use client';
-
-import * as React from 'react';
-
-import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
-import { cn } from '@udecode/cn';
-import { Check, ChevronRight, Circle } from 'lucide-react';
-
-const ContextMenu = ContextMenuPrimitive.Root;
-
-const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
-
-const ContextMenuGroup = React.forwardRef<
- HTMLDivElement,
- { label?: React.ReactNode } & React.ComponentPropsWithoutRef<
- typeof ContextMenuPrimitive.Group
- >
->(({ label, ...props }, ref) => {
- return (
- <>
- <ContextMenuSeparator
- className={cn(
- 'hidden',
- 'mb-0 shrink-0 peer-has-[[role=menuitem]]/menu-group:block peer-has-[[role=menuitemcheckbox]]/menu-group:block peer-has-[[role=option]]/menu-group:block'
- )}
- />
-
- <ContextMenuPrimitive.Group
- ref={ref}
- {...props}
- className={cn(
- 'hidden',
- 'peer/menu-group group/menu-group my-1.5 has-[[role=menuitem]]:block has-[[role=menuitemcheckbox]]:block has-[[role=option]]:block',
- props.className
- )}
- >
- {label && <ContextMenuLabel>{label}</ContextMenuLabel>}
- {props.children}
- </ContextMenuPrimitive.Group>
- </>
- );
-});
-
-const ContextMenuPortal = ContextMenuPrimitive.Portal;
-
-const ContextMenuSub = ContextMenuPrimitive.Sub;
-
-const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
-
-const ContextMenuSubTrigger = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
- inset?: boolean;
- }
->(({ children, className, inset, ...props }, ref) => (
- <ContextMenuPrimitive.SubTrigger
- ref={ref}
- className={cn(
- 'mx-1 flex h-[28px] cursor-default select-none items-center rounded-sm px-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
- inset && 'pl-8',
- className
- )}
- {...props}
- >
- {children}
- <ChevronRight className="ml-auto size-4" />
- </ContextMenuPrimitive.SubTrigger>
-));
-ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
-
-const ContextMenuSubContent = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
->(({ className, ...props }, ref) => (
- <ContextMenuPrimitive.SubContent
- ref={ref}
- className={cn(
- 'z-50 min-w-32 overflow-hidden rounded-md border bg-popover py-1.5 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
- className
- )}
- {...props}
- />
-));
-ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
-
-const ContextMenuContent = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
->(({ className, ...props }, ref) => (
- <ContextMenuPrimitive.Portal>
- <ContextMenuPrimitive.Content
- ref={ref}
- className={cn(
- 'z-50 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
- className
- )}
- {...props}
- />
- </ContextMenuPrimitive.Portal>
-));
-ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
-
-const ContextMenuItem = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
- inset?: boolean;
- }
->(({ className, inset, ...props }, ref) => (
- <ContextMenuPrimitive.Item
- ref={ref}
- className={cn(
- 'relative mx-1 flex h-[28px] cursor-default select-none items-center rounded-sm px-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
- inset && 'pl-8',
- className
- )}
- {...props}
- />
-));
-ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
-
-const ContextMenuCheckboxItem = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
->(({ checked, children, className, ...props }, ref) => (
- <ContextMenuPrimitive.CheckboxItem
- ref={ref}
- className={cn(
- 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
- className
- )}
- checked={checked}
- {...props}
- >
- <span className="absolute left-2 flex size-3.5 items-center justify-center">
- <ContextMenuPrimitive.ItemIndicator>
- <Check className="size-4" />
- </ContextMenuPrimitive.ItemIndicator>
- </span>
- {children}
- </ContextMenuPrimitive.CheckboxItem>
-));
-ContextMenuCheckboxItem.displayName =
- ContextMenuPrimitive.CheckboxItem.displayName;
-
-const ContextMenuRadioItem = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
->(({ children, className, ...props }, ref) => (
- <ContextMenuPrimitive.RadioItem
- ref={ref}
- className={cn(
- 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
- className
- )}
- {...props}
- >
- <span className="absolute left-2 flex size-3.5 items-center justify-center">
- <ContextMenuPrimitive.ItemIndicator>
- <Circle className="size-2 fill-current" />
- </ContextMenuPrimitive.ItemIndicator>
- </span>
- {children}
- </ContextMenuPrimitive.RadioItem>
-));
-ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
-
-const ContextMenuLabel = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.Label>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
- inset?: boolean;
- }
->(({ className, inset, ...props }, ref) => (
- <ContextMenuPrimitive.Label
- ref={ref}
- className={cn(
- 'px-2 py-1.5 text-sm font-semibold text-foreground',
- inset && 'pl-8',
- className
- )}
- {...props}
- />
-));
-ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
-
-const ContextMenuSeparator = React.forwardRef<
- React.ElementRef<typeof ContextMenuPrimitive.Separator>,
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
->(({ className, ...props }, ref) => (
- <ContextMenuPrimitive.Separator
- ref={ref}
- className={cn('-mx-1 my-1 h-px bg-border', className)}
- {...props}
- />
-));
-ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
-
-const ContextMenuShortcut = ({
- className,
- ...props
-}: React.HTMLAttributes<HTMLSpanElement>) => {
- return (
- <span
- className={cn(
- 'ml-auto text-xs tracking-widest text-muted-foreground',
- className
- )}
- {...props}
- />
- );
-};
-ContextMenuShortcut.displayName = 'ContextMenuShortcut';
-
-export {
- ContextMenu,
- ContextMenuCheckboxItem,
- ContextMenuContent,
- ContextMenuGroup,
- ContextMenuItem,
- ContextMenuLabel,
- ContextMenuPortal,
- ContextMenuRadioGroup,
- ContextMenuRadioItem,
- ContextMenuSeparator,
- ContextMenuShortcut,
- ContextMenuSub,
- ContextMenuSubContent,
- ContextMenuSubTrigger,
- ContextMenuTrigger,
-};
diff --git a/apps/web/app/components/plate-ui/cursor-overlay.tsx b/apps/web/app/components/plate-ui/cursor-overlay.tsx
deleted file mode 100644
index f964d93c..00000000
--- a/apps/web/app/components/plate-ui/cursor-overlay.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn } from '@udecode/cn';
-import { isCollapsed } from '@udecode/plate-common';
-import {
- type CursorData,
- type CursorOverlayState,
- useCursorOverlay,
-} from '@udecode/plate-selection/react';
-
-export function Cursor({
- id,
- caretPosition,
- data,
- selection,
- selectionRects,
-}: CursorOverlayState<CursorData>) {
- const { style, selectionStyle = style } = data ?? ({} as CursorData);
- const isCursor = isCollapsed(selection);
-
- return (
- <>
- {selectionRects.map((position, i) => {
- return (
- <div
- key={i}
- className={cn(
- 'pointer-events-none absolute z-10',
- id === 'selection' && 'bg-brand/25',
- id === 'selection' && isCursor && 'bg-primary'
- )}
- style={{
- ...selectionStyle,
- ...position,
- }}
- />
- );
- })}
- {caretPosition && (
- <div
- className={cn(
- 'pointer-events-none absolute z-10 w-0.5',
- id === 'drag' && 'w-px bg-brand'
- )}
- style={{ ...caretPosition, ...style }}
- />
- )}
- </>
- );
-}
-
-export function CursorOverlay() {
- const { cursors } = useCursorOverlay();
-
- return (
- <>
- {cursors.map((cursor) => (
- <Cursor key={cursor.id} {...cursor} />
- ))}
- </>
- );
-}
diff --git a/apps/web/app/components/plate-ui/date-element.tsx b/apps/web/app/components/plate-ui/date-element.tsx
deleted file mode 100644
index a9b12737..00000000
--- a/apps/web/app/components/plate-ui/date-element.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-'use client';
-
-import { cn, withRef } from '@udecode/cn';
-import { setNodes } from '@udecode/plate-common';
-import { findNodePath } from '@udecode/plate-common/react';
-
-import { Calendar } from './calendar';
-import { PlateElement } from './plate-element';
-import { Popover, PopoverContent, PopoverTrigger } from './popover';
-
-export const DateElement = withRef<typeof PlateElement>(
- ({ children, className, ...props }, ref) => {
- const { editor, element } = props;
-
- return (
- <PlateElement
- ref={ref}
- className={cn('inline-block', className)}
- contentEditable={false}
- {...props}
- >
- <Popover>
- <PopoverTrigger asChild>
- <span
- className={cn(
- 'w-fit cursor-pointer rounded-sm bg-muted px-1 text-muted-foreground'
- )}
- contentEditable={false}
- draggable
- >
- {element.date ? (
- (() => {
- const today = new Date();
- const elementDate = new Date(element.date as string);
- const isToday =
- elementDate.getDate() === today.getDate() &&
- elementDate.getMonth() === today.getMonth() &&
- elementDate.getFullYear() === today.getFullYear();
-
- const isYesterday =
- new Date(
- today.setDate(today.getDate() - 1)
- ).toDateString() === elementDate.toDateString();
- const isTomorrow =
- new Date(
- today.setDate(today.getDate() + 2)
- ).toDateString() === elementDate.toDateString();
-
- if (isToday) return 'Today';
- if (isYesterday) return 'Yesterday';
- if (isTomorrow) return 'Tomorrow';
-
- return elementDate.toLocaleDateString(undefined, {
- day: 'numeric',
- month: 'long',
- year: 'numeric',
- });
- })()
- ) : (
- <span>Pick a date</span>
- )}
- </span>
- </PopoverTrigger>
- <PopoverContent className="w-auto p-0">
- <Calendar
- selected={new Date(element.date as string)}
- onSelect={(date) => {
- if (!date) return;
-
- setNodes(
- editor,
- { date: date.toDateString() },
- { at: findNodePath(editor, element) }
- );
- }}
- mode="single"
- initialFocus
- />
- </PopoverContent>
- </Popover>
- {children}
- </PlateElement>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/dialog.tsx b/apps/web/app/components/plate-ui/dialog.tsx
deleted file mode 100644
index b89bc9a9..00000000
--- a/apps/web/app/components/plate-ui/dialog.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-'use client';
-
-import * as React from 'react';
-
-import * as DialogPrimitive from '@radix-ui/react-dialog';
-import { cn, createPrimitiveElement, withCn, withRef } from '@udecode/cn';
-import { X } from 'lucide-react';
-
-export const Dialog = DialogPrimitive.Root;
-
-export const DialogTrigger = DialogPrimitive.Trigger;
-
-export const DialogPortal = DialogPrimitive.Portal;
-
-export const DialogClose = DialogPrimitive.Close;
-
-export const DialogOverlay = withCn(
- DialogPrimitive.Overlay,
- 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0'
-);
-
-export const DialogContent = withRef<typeof DialogPrimitive.Content>(
- ({ children, className, ...props }, ref) => (
- <DialogPortal>
- <DialogOverlay />
- <DialogPrimitive.Content
- ref={ref}
- className={cn(
- 'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
- className
- )}
- {...props}
- >
- {children}
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
- <X className="size-4" />
- <span className="sr-only">Close</span>
- </DialogPrimitive.Close>
- </DialogPrimitive.Content>
- </DialogPortal>
- )
-);
-
-export const DialogHeader = withCn(
- createPrimitiveElement('div'),
- 'flex flex-col space-y-1.5 text-center sm:text-left'
-);
-
-export const DialogFooter = withCn(
- createPrimitiveElement('div'),
- 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2'
-);
-
-export const DialogTitle = withCn(
- DialogPrimitive.Title,
- 'text-lg font-semibold leading-none tracking-tight'
-);
-
-export const DialogDescription = withCn(
- DialogPrimitive.Description,
- 'text-sm text-muted-foreground'
-);
diff --git a/apps/web/app/components/plate-ui/draggable.tsx b/apps/web/app/components/plate-ui/draggable.tsx
deleted file mode 100644
index 6b27a5aa..00000000
--- a/apps/web/app/components/plate-ui/draggable.tsx
+++ /dev/null
@@ -1,187 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { TEditor } from '@udecode/plate-common';
-import type { DropTargetMonitor } from 'react-dnd';
-
-import { cn, withRef } from '@udecode/cn';
-import {
- type PlateElementProps,
- MemoizedChildren,
- useEditorPlugin,
- useEditorRef,
- withHOC,
-} from '@udecode/plate-common/react';
-import {
- type DragItemNode,
- DraggableProvider,
- useDraggable,
- useDraggableGutter,
- useDraggableState,
- useDropLine,
-} from '@udecode/plate-dnd';
-import { BlockSelectionPlugin } from '@udecode/plate-selection/react';
-import { GripVertical } from 'lucide-react';
-import { useSelected } from 'slate-react';
-
-import { useMounted } from '~/lib/hooks/use-mounted';
-
-import {
- Tooltip,
- TooltipContent,
- TooltipPortal,
- TooltipProvider,
- TooltipTrigger,
-} from './tooltip';
-
-export interface DraggableProps extends PlateElementProps {
- /**
- * Intercepts the drop handling. If `false` is returned, the default drop
- * behavior is called after. If `true` is returned, the default behavior is
- * not called.
- */
- onDropHandler?: (
- editor: TEditor,
- props: {
- id: string;
- dragItem: DragItemNode;
- monitor: DropTargetMonitor<DragItemNode, unknown>;
- nodeRef: any;
- }
- ) => boolean;
-}
-
-export const Draggable = withHOC(
- DraggableProvider,
- withRef<'div', DraggableProps>(
- ({ className, onDropHandler, ...props }, ref) => {
- const { children, element } = props;
-
- const state = useDraggableState({ element, onDropHandler });
- const { isDragging } = state;
- const { previewRef, handleRef } = useDraggable(state);
- const mounted = useMounted();
-
- return (
- <div
- ref={ref}
- className={cn(
- 'relative',
- isDragging && 'opacity-50',
- 'group',
- className
- )}
- >
- <Gutter>
- <div className={cn('slate-blockToolbarWrapper', 'flex h-[1.5em]')}>
- <div
- className={cn(
- 'slate-blockToolbar',
- 'pointer-events-auto mr-1 flex items-center'
- )}
- >
- <div
- ref={handleRef}
- className="size-4"
- data-key={mounted ? (element.id as string) : undefined}
- >
- <DragHandle />
- </div>
- </div>
- </div>
- </Gutter>
-
- <div ref={previewRef} className="slate-blockWrapper">
- <MemoizedChildren>{children}</MemoizedChildren>
-
- <DropLine />
- </div>
- </div>
- );
- }
- )
-);
-
-const Gutter = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
->(({ children, className, ...props }, ref) => {
- const { useOption } = useEditorPlugin(BlockSelectionPlugin);
- const isSelectionAreaVisible = useOption('isSelectionAreaVisible');
- const gutter = useDraggableGutter();
- const selected = useSelected();
-
- return (
- <div
- ref={ref}
- className={cn(
- 'slate-gutterLeft',
- 'absolute -top-px z-50 flex h-full -translate-x-full cursor-text hover:opacity-100 sm:opacity-0 main-hover:group-hover:opacity-100',
- isSelectionAreaVisible && 'hidden',
- !selected && 'opacity-0',
- className
- )}
- {...props}
- {...gutter.props}
- >
- {children}
- </div>
- );
-});
-
-const DragHandle = React.memo(() => {
- const editor = useEditorRef();
-
- return (
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger type="button">
- <GripVertical
- className="size-4 text-muted-foreground"
- onClick={(event) => {
- event.stopPropagation();
- event.preventDefault();
- }}
- onMouseDown={() => {
- editor
- .getApi(BlockSelectionPlugin)
- .blockSelection?.resetSelectedIds();
- }}
- />
- </TooltipTrigger>
- <TooltipPortal>
- <TooltipContent>Drag to move</TooltipContent>
- </TooltipPortal>
- </Tooltip>
- </TooltipProvider>
- );
-});
-
-const DropLine = React.memo(
- React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
- ({ children, className, ...props }, ref) => {
- const state = useDropLine();
-
- if (!state.dropLine) return null;
-
- return (
- <div
- ref={ref}
- {...props}
- {...state.props}
- className={cn(
- 'slate-dropLine',
- 'absolute inset-x-0 h-0.5 opacity-100 transition-opacity',
- 'bg-brand/50',
- state.dropLine === 'top' && '-top-px',
- state.dropLine === 'bottom' && '-bottom-px',
- className
- )}
- >
- {children}
- </div>
- );
- }
- )
-);
diff --git a/apps/web/app/components/plate-ui/dropdown-menu.tsx b/apps/web/app/components/plate-ui/dropdown-menu.tsx
deleted file mode 100644
index 2f4eb47a..00000000
--- a/apps/web/app/components/plate-ui/dropdown-menu.tsx
+++ /dev/null
@@ -1,240 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import { useCallback, useState } from 'react';
-
-import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
-import {
- cn,
- createPrimitiveElement,
- withCn,
- withProps,
- withRef,
- withVariants,
-} from '@udecode/cn';
-import { cva } from 'class-variance-authority';
-import { Check, ChevronRight } from 'lucide-react';
-
-export const DropdownMenu = DropdownMenuPrimitive.Root;
-
-export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
-
-export const DropdownMenuGroup = React.forwardRef<
- HTMLDivElement,
- { label?: React.ReactNode } & React.ComponentPropsWithoutRef<
- typeof DropdownMenuPrimitive.Group
- >
->(({ label, ...props }, ref) => {
- return (
- <>
- <DropdownMenuSeparator
- className={cn(
- 'hidden',
- 'mb-0 shrink-0 peer-has-[[role=menuitem]]/menu-group:block peer-has-[[role=menuitemcheckbox]]/menu-group:block peer-has-[[role=option]]/menu-group:block'
- )}
- />
-
- <DropdownMenuPrimitive.Group
- ref={ref}
- {...props}
- className={cn(
- 'hidden',
- 'peer/menu-group group/menu-group my-1.5 has-[[role=menuitem]]:block has-[[role=menuitemcheckbox]]:block has-[[role=option]]:block',
- props.className
- )}
- >
- {label && <DropdownMenuLabel>{label}</DropdownMenuLabel>}
- {props.children}
- </DropdownMenuPrimitive.Group>
- </>
- );
-});
-
-export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
-
-export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
-
-export const DropdownMenuRadioGroup = React.forwardRef<
- HTMLDivElement,
- { label?: React.ReactNode } & React.ComponentPropsWithoutRef<
- typeof DropdownMenuPrimitive.RadioGroup
- >
->(({ label, ...props }, ref) => {
- return (
- <>
- <DropdownMenuSeparator
- className={cn(
- 'hidden',
- 'mb-0 shrink-0 peer-has-[[role=menuitemradio]]/menu-group:block peer-has-[[role=option]]/menu-group:block'
- )}
- />
-
- <DropdownMenuPrimitive.RadioGroup
- ref={ref}
- {...props}
- className={cn(
- 'hidden',
- 'peer/menu-group group/menu-group my-1.5 has-[[role=menuitemradio]]:block has-[[role=option]]:block',
- props.className
- )}
- >
- {label && <DropdownMenuLabel>{label}</DropdownMenuLabel>}
- {props.children}
- </DropdownMenuPrimitive.RadioGroup>
- </>
- );
-});
-
-export const DropdownMenuSubTrigger = withRef<
- typeof DropdownMenuPrimitive.SubTrigger,
- {
- inset?: boolean;
- }
->(({ children, className, inset, ...props }, ref) => (
- <DropdownMenuPrimitive.SubTrigger
- ref={ref}
- className={cn(
- 'mx-1 flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1 text-sm outline-none focus:bg-accent data-[disabled]:pointer-events-none data-[state=open]:bg-accent data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
- inset && 'pl-8',
- className
- )}
- {...props}
- >
- {children}
- <ChevronRight className="ml-auto" />
- </DropdownMenuPrimitive.SubTrigger>
-));
-
-export const DropdownMenuSubContent = withCn(
- DropdownMenuPrimitive.SubContent,
- 'z-50 min-w-32 overflow-hidden rounded-md border bg-popover py-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2'
-);
-
-const DropdownMenuContentVariants = withProps(DropdownMenuPrimitive.Content, {
- className: cn(
- 'z-50 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2'
- ),
- sideOffset: 4,
-});
-
-export const DropdownMenuContent = withRef<
- typeof DropdownMenuPrimitive.Content
->(({ ...props }, ref) => (
- <DropdownMenuPrimitive.Portal>
- <DropdownMenuContentVariants
- ref={ref}
- onCloseAutoFocus={(e) => {
- e.preventDefault();
- }}
- {...props}
- />
- </DropdownMenuPrimitive.Portal>
-));
-
-const menuItemVariants = cva(
- 'relative mx-1 flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
- {
- variants: {
- inset: {
- true: 'pl-8',
- },
- },
- }
-);
-
-export const DropdownMenuItem = withVariants(
- DropdownMenuPrimitive.Item,
- menuItemVariants,
- ['inset']
-);
-
-export const DropdownMenuCheckboxItem = withRef<
- typeof DropdownMenuPrimitive.CheckboxItem
->(({ children, className, ...props }, ref) => (
- <DropdownMenuPrimitive.CheckboxItem
- ref={ref}
- className={cn(
- 'relative mx-1 flex select-none items-center gap-2 rounded-sm py-1 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0',
- 'cursor-pointer',
- className
- )}
- {...props}
- >
- <span className="absolute left-2 flex size-3.5 items-center justify-center">
- <DropdownMenuPrimitive.ItemIndicator>
- <Check className="size-4" />
- </DropdownMenuPrimitive.ItemIndicator>
- </span>
- {children}
- </DropdownMenuPrimitive.CheckboxItem>
-));
-
-export const DropdownMenuRadioItem = withRef<
- typeof DropdownMenuPrimitive.RadioItem,
- {
- hideIcon?: boolean;
- }
->(({ children, className, hideIcon, ...props }, ref) => (
- <DropdownMenuPrimitive.RadioItem
- ref={ref}
- className={cn(
- 'relative mx-1 flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[state=checked]:text-accent-foreground data-[disabled]:opacity-50 [&_svg]:size-4',
- className
- )}
- {...props}
- >
- {!hideIcon && (
- <span className="absolute right-2 flex size-3.5 items-center justify-center">
- <DropdownMenuPrimitive.ItemIndicator>
- <Check />
- </DropdownMenuPrimitive.ItemIndicator>
- </span>
- )}
- {children}
- </DropdownMenuPrimitive.RadioItem>
-));
-
-const dropdownMenuLabelVariants = cva(
- cn(
- 'mx-1 select-none px-2 pb-2 pt-1.5 text-xs font-semibold text-muted-foreground'
- ),
- {
- variants: {
- inset: {
- true: 'pl-8',
- },
- },
- }
-);
-
-export const DropdownMenuLabel = withVariants(
- DropdownMenuPrimitive.Label,
- dropdownMenuLabelVariants,
- ['inset']
-);
-
-export const DropdownMenuSeparator = withCn(
- DropdownMenuPrimitive.Separator,
- '-mx-1 my-1 h-px bg-muted'
-);
-
-export const DropdownMenuShortcut = withCn(
- createPrimitiveElement('span'),
- 'ml-auto text-xs tracking-widest opacity-60'
-);
-
-export const useOpenState = () => {
- const [open, setOpen] = useState(false);
-
- const onOpenChange = useCallback(
- (_value = !open) => {
- setOpen(_value);
- },
- [open]
- );
-
- return {
- open,
- onOpenChange,
- };
-};
diff --git a/apps/web/app/components/plate-ui/editor.tsx b/apps/web/app/components/plate-ui/editor.tsx
deleted file mode 100644
index c17d66e8..00000000
--- a/apps/web/app/components/plate-ui/editor.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { PlateContentProps } from '@udecode/plate-common/react';
-import type { VariantProps } from 'class-variance-authority';
-
-import { cn } from '@udecode/cn';
-import {
- PlateContent,
- useEditorContainerRef,
- useEditorRef,
-} from '@udecode/plate-common/react';
-import { cva } from 'class-variance-authority';
-
-const editorContainerVariants = cva(
- 'relative w-full cursor-text overflow-y-auto caret-primary selection:bg-brand/25 [&_.slate-selection-area]:border [&_.slate-selection-area]:border-brand/25 [&_.slate-selection-area]:bg-brand/15',
- {
- defaultVariants: {
- variant: 'default',
- },
- variants: {
- variant: {
- default: 'h-full',
- demo: 'h-[650px]',
- },
- },
- }
-);
-
-export const EditorContainer = ({
- className,
- variant,
- ...props
-}: React.HTMLAttributes<HTMLDivElement> &
- VariantProps<typeof editorContainerVariants>) => {
- const editor = useEditorRef();
- const containerRef = useEditorContainerRef();
-
- return (
- <div
- id={editor.uid}
- ref={containerRef}
- className={cn(
- 'ignore-click-outside/toolbar',
- editorContainerVariants({ variant }),
- className
- )}
- role="button"
- {...props}
- />
- );
-};
-
-EditorContainer.displayName = 'EditorContainer';
-
-const editorVariants = cva(
- cn(
- 'group/editor',
- 'relative w-full overflow-x-hidden whitespace-pre-wrap break-words',
- 'rounded-md ring-offset-background placeholder:text-muted-foreground/80 focus-visible:outline-none',
- '[&_[data-slate-placeholder]]:text-muted-foreground/80 [&_[data-slate-placeholder]]:!opacity-100',
- '[&_[data-slate-placeholder]]:top-[auto_!important]',
- '[&_strong]:font-bold'
- ),
- {
- defaultVariants: {
- variant: 'default',
- },
- variants: {
- disabled: {
- true: 'cursor-not-allowed opacity-50',
- },
- focused: {
- true: 'ring-2 ring-ring ring-offset-2',
- },
- variant: {
- ai: 'w-full px-0 text-sm',
- aiChat:
- 'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-3 py-2 text-sm',
- default:
- 'size-full px-16 pb-72 pt-4 text-base sm:px-[max(64px,calc(50%-350px))]',
- demo: 'size-full px-16 pb-72 pt-4 text-base sm:px-[max(64px,calc(50%-350px))]',
- fullWidth: 'size-full px-16 pb-72 pt-4 text-base sm:px-24',
- },
- },
- }
-);
-
-export type EditorProps = PlateContentProps &
- VariantProps<typeof editorVariants>;
-
-export const Editor = React.forwardRef<HTMLDivElement, EditorProps>(
- ({ className, disabled, focused, variant, ...props }, ref) => {
- return (
- <PlateContent
- ref={ref}
- className={cn(
- editorVariants({
- disabled,
- focused,
- variant,
- }),
- className
- )}
- disabled={disabled}
- disableDefaultStyles
- {...props}
- />
- );
- }
-);
-
-Editor.displayName = 'Editor';
diff --git a/apps/web/app/components/plate-ui/emoji-picker-navigation.tsx b/apps/web/app/components/plate-ui/emoji-picker-navigation.tsx
deleted file mode 100644
index 2549c8ca..00000000
--- a/apps/web/app/components/plate-ui/emoji-picker-navigation.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import type { EmojiCategoryList } from '@udecode/plate-emoji';
-import type { UseEmojiPickerType } from '@udecode/plate-emoji/react';
-
-import { cn } from '@udecode/cn';
-
-import { Button } from './button';
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from './tooltip';
-
-export type EmojiPickerNavigationProps = {
- onClick: (id: EmojiCategoryList) => void;
-} & Pick<
- UseEmojiPickerType,
- 'emojiLibrary' | 'focusedCategory' | 'i18n' | 'icons'
->;
-
-// KEEP: This is for the animated idicator bar under the icon - Opt in if needed
-// const getBarProperty = (
-// emojiLibrary: IEmojiFloatingLibrary,
-// focusedCategory?: EmojiCategoryList
-// ) => {
-// let width = 0;
-// let position = 0;
-
-// if (focusedCategory) {
-// width = 100 / emojiLibrary.getGrid().size;
-// position = focusedCategory
-// ? emojiLibrary.indexOf(focusedCategory) * 100
-// : 0;
-// }
-
-// return { position, width };
-// };
-
-export function EmojiPickerNavigation({
- emojiLibrary,
- focusedCategory,
- i18n,
- icons,
- onClick,
-}: EmojiPickerNavigationProps) {
- // KEEP: This is for the animated idicator bar under the icon - Opt in if needed
- // const { position, width } = useMemo(
- // () => getBarProperty(emojiLibrary, focusedCategory),
- // [emojiLibrary, focusedCategory]
- // );
-
- return (
- <TooltipProvider delayDuration={500}>
- <nav
- id="emoji-nav"
- className="mb-2.5 border-0 border-b border-solid border-b-border p-1.5"
- >
- <div className="relative flex items-center justify-evenly">
- {emojiLibrary
- .getGrid()
- .sections()
- .map(({ id }) => (
- <Tooltip key={id}>
- <TooltipTrigger asChild>
- <Button
- size="sm"
- variant="ghost"
- className={cn(
- 'h-fit rounded-full fill-current p-1.5 text-muted-foreground hover:bg-muted hover:text-muted-foreground',
- id === focusedCategory &&
- 'pointer-events-none bg-accent fill-current text-accent-foreground'
- )}
- onClick={() => {
- onClick(id);
- }}
- aria-label={i18n.categories[id]}
- type="button"
- >
- <span className="inline-flex size-5 items-center justify-center">
- {icons.categories[id].outline}
- </span>
- </Button>
- </TooltipTrigger>
- <TooltipContent side="bottom">
- {i18n.categories[id]}
- </TooltipContent>
- </Tooltip>
- ))}
-
- {/* This is the animated indicator - Opt In if needed */}
- {/* <div
- className={cn(
- 'absolute -bottom-1.5 left-0 h-0.5 w-full rounded-t-lg bg-accent opacity-100 transition-transform duration-200'
- )}
- style={{
- transform: `translateX(${position}%)`,
- visibility: `${focusedCategory ? 'visible' : 'hidden'}`,
- width: `${width}%`,
- }}
- /> */}
- </div>
- </nav>
- </TooltipProvider>
- );
-}
diff --git a/apps/web/app/components/plate-ui/emoji-picker-preview.tsx b/apps/web/app/components/plate-ui/emoji-picker-preview.tsx
deleted file mode 100644
index ac33d7ce..00000000
--- a/apps/web/app/components/plate-ui/emoji-picker-preview.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-'use client';
-
-import type { UseEmojiPickerType } from '@udecode/plate-emoji/react';
-
-export type EmojiPickerPreviewProps = Pick<
- UseEmojiPickerType,
- 'emoji' | 'hasFound' | 'i18n' | 'isSearching'
->;
-
-export type EmojiPreviewProps = Pick<UseEmojiPickerType, 'emoji'>;
-
-export type NoEmojiPreviewProps = Pick<UseEmojiPickerType, 'i18n'>;
-
-export type PickAnEmojiPreviewProps = NoEmojiPreviewProps;
-
-function EmojiPreview({ emoji }: EmojiPreviewProps) {
- return (
- <div className="flex h-14 max-h-14 min-h-14 items-center border-t border-muted p-2">
- <div className="flex items-center justify-center text-2xl">
- {emoji?.skins[0].native}
- </div>
- <div className="overflow-hidden pl-2">
- <div className="truncate text-sm font-semibold">{emoji?.name}</div>
- <div className="truncate text-sm">{`:${emoji?.id}:`}</div>
- </div>
- </div>
- );
-}
-
-function NoEmoji({ i18n }: NoEmojiPreviewProps) {
- return (
- <div className="flex h-14 max-h-14 min-h-14 items-center border-t border-muted p-2">
- <div className="flex items-center justify-center text-2xl">😢</div>
- <div className="overflow-hidden pl-2">
- <div className="truncate text-sm font-bold">
- {i18n.searchNoResultsTitle}
- </div>
- <div className="truncate text-sm">{i18n.searchNoResultsSubtitle}</div>
- </div>
- </div>
- );
-}
-
-function PickAnEmoji({ i18n }: PickAnEmojiPreviewProps) {
- return (
- <div className="flex h-14 max-h-14 min-h-14 items-center border-t border-muted p-2">
- <div className="flex items-center justify-center text-2xl">☝️</div>
- <div className="overflow-hidden pl-2">
- <div className="truncate text-sm font-semibold">{i18n.pick}</div>
- </div>
- </div>
- );
-}
-
-export function EmojiPickerPreview({
- emoji,
- hasFound = true,
- i18n,
- isSearching = false,
- ...props
-}: EmojiPickerPreviewProps) {
- const showPickEmoji = !emoji && (!isSearching || hasFound);
- const showNoEmoji = isSearching && !hasFound;
- const showPreview = emoji && !showNoEmoji && !showNoEmoji;
-
- return (
- <>
- {showPreview && <EmojiPreview emoji={emoji} {...props} />}
- {showPickEmoji && <PickAnEmoji i18n={i18n} {...props} />}
- {showNoEmoji && <NoEmoji i18n={i18n} {...props} />}
- </>
- );
-}
diff --git a/apps/web/app/components/plate-ui/emoji-picker-search-and-clear.tsx b/apps/web/app/components/plate-ui/emoji-picker-search-and-clear.tsx
deleted file mode 100644
index 117c745f..00000000
--- a/apps/web/app/components/plate-ui/emoji-picker-search-and-clear.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-'use client';
-
-import type { UseEmojiPickerType } from '@udecode/plate-emoji/react';
-
-import { cn } from '@udecode/cn';
-
-import { Button } from './button';
-import { emojiSearchIcons } from './emoji-icons';
-
-export type EmojiPickerSearchAndClearProps = Pick<
- UseEmojiPickerType,
- 'clearSearch' | 'i18n' | 'searchValue'
->;
-
-export function EmojiPickerSearchAndClear({
- clearSearch,
- i18n,
- searchValue,
-}: EmojiPickerSearchAndClearProps) {
- return (
- <div className="flex items-center text-foreground">
- <div
- className={cn(
- 'absolute left-2.5 top-1/2 z-10 flex size-5 -translate-y-1/2 items-center justify-center text-foreground'
- )}
- >
- {emojiSearchIcons.loupe}
- </div>
- {searchValue && (
- <Button
- size="icon"
- variant="ghost"
- className={cn(
- 'absolute right-0.5 top-1/2 flex size-8 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full border-none bg-transparent text-popover-foreground hover:bg-transparent'
- )}
- onClick={clearSearch}
- title={i18n.clear}
- aria-label="Clear"
- type="button"
- >
- {emojiSearchIcons.delete}
- </Button>
- )}
- </div>
- );
-}
diff --git a/apps/web/app/components/plate-ui/emoji-picker-search-bar.tsx b/apps/web/app/components/plate-ui/emoji-picker-search-bar.tsx
deleted file mode 100644
index bc6ce9d4..00000000
--- a/apps/web/app/components/plate-ui/emoji-picker-search-bar.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-'use client';
-
-import type { ReactNode } from 'react';
-
-import type { UseEmojiPickerType } from '@udecode/plate-emoji/react';
-
-export type EmojiPickerSearchBarProps = {
- children: ReactNode;
-} & Pick<UseEmojiPickerType, 'i18n' | 'searchValue' | 'setSearch'>;
-
-export function EmojiPickerSearchBar({
- children,
- i18n,
- searchValue,
- setSearch,
-}: EmojiPickerSearchBarProps) {
- return (
- <div className="flex items-center px-2">
- <div className="relative flex grow items-center">
- <input
- className="block w-full appearance-none rounded-full border-0 bg-muted px-10 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:outline-none"
- value={searchValue}
- onChange={(event) => setSearch(event.target.value)}
- placeholder={i18n.search}
- aria-label="Search"
- autoComplete="off"
- type="text"
- />
- {children}
- </div>
- </div>
- );
-}
diff --git a/apps/web/app/components/plate-ui/emoji-picker.tsx b/apps/web/app/components/plate-ui/emoji-picker.tsx
deleted file mode 100644
index 41b47668..00000000
--- a/apps/web/app/components/plate-ui/emoji-picker.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { UseEmojiPickerType } from '@udecode/plate-emoji/react';
-
-import { cn } from '@udecode/cn';
-import { EmojiSettings } from '@udecode/plate-emoji';
-
-import { EmojiPickerContent } from './emoji-picker-content';
-import { EmojiPickerNavigation } from './emoji-picker-navigation';
-import { EmojiPickerPreview } from './emoji-picker-preview';
-import { EmojiPickerSearchAndClear } from './emoji-picker-search-and-clear';
-import { EmojiPickerSearchBar } from './emoji-picker-search-bar';
-
-export function EmojiPicker({
- clearSearch,
- emoji,
- emojiLibrary,
- focusedCategory,
- hasFound,
- i18n,
- icons,
- isSearching,
- refs,
- searchResult,
- searchValue,
- setSearch,
- settings = EmojiSettings,
- visibleCategories,
- handleCategoryClick,
- onMouseOver,
- onSelectEmoji,
-}: UseEmojiPickerType) {
- return (
- <div
- className={cn(
- 'flex flex-col rounded-xl bg-popover text-popover-foreground',
- 'h-[23rem] w-80 border shadow-md'
- )}
- >
- <EmojiPickerNavigation
- onClick={handleCategoryClick}
- emojiLibrary={emojiLibrary}
- focusedCategory={focusedCategory}
- i18n={i18n}
- icons={icons}
- />
- <EmojiPickerSearchBar
- i18n={i18n}
- searchValue={searchValue}
- setSearch={setSearch}
- >
- <EmojiPickerSearchAndClear
- clearSearch={clearSearch}
- i18n={i18n}
- searchValue={searchValue}
- />
- </EmojiPickerSearchBar>
- <EmojiPickerContent
- onMouseOver={onMouseOver}
- onSelectEmoji={onSelectEmoji}
- emojiLibrary={emojiLibrary}
- i18n={i18n}
- isSearching={isSearching}
- refs={refs}
- searchResult={searchResult}
- settings={settings}
- visibleCategories={visibleCategories}
- />
- <EmojiPickerPreview
- emoji={emoji}
- hasFound={hasFound}
- i18n={i18n}
- isSearching={isSearching}
- />
- </div>
- );
-}
diff --git a/apps/web/app/components/plate-ui/emoji-toolbar-dropdown.tsx b/apps/web/app/components/plate-ui/emoji-toolbar-dropdown.tsx
deleted file mode 100644
index 189056f7..00000000
--- a/apps/web/app/components/plate-ui/emoji-toolbar-dropdown.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-'use client';
-
-import React, { type ReactNode } from 'react';
-
-import * as Popover from '@radix-ui/react-popover';
-
-type EmojiToolbarDropdownProps = {
- children: ReactNode;
- control: ReactNode;
- isOpen: boolean;
- setIsOpen: (open: boolean) => void;
-};
-
-export function EmojiToolbarDropdown({
- children,
- control,
- isOpen,
- setIsOpen,
-}: EmojiToolbarDropdownProps) {
- return (
- <Popover.Root open={isOpen} onOpenChange={setIsOpen}>
- <Popover.Trigger asChild>{control}</Popover.Trigger>
-
- <Popover.Portal>
- <Popover.Content className="z-[100]">{children}</Popover.Content>
- </Popover.Portal>
- </Popover.Root>
- );
-}
diff --git a/apps/web/app/components/plate-ui/excalidraw-element.tsx b/apps/web/app/components/plate-ui/excalidraw-element.tsx
deleted file mode 100644
index 1f6275a0..00000000
--- a/apps/web/app/components/plate-ui/excalidraw-element.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-'use client';
-import React from 'react';
-
-import { withRef } from '@udecode/cn';
-import { useExcalidrawElement } from '@udecode/plate-excalidraw/react';
-
-import { PlateElement } from './plate-element';
-
-export const ExcalidrawElement = withRef<typeof PlateElement>(
- ({ nodeProps, ...props }, ref) => {
- const { children, element } = props;
-
- const { Excalidraw, excalidrawProps } = useExcalidrawElement({
- element,
- });
-
- return (
- <PlateElement ref={ref} {...props}>
- <div contentEditable={false}>
- <div className="mx-auto aspect-video h-[600px] w-[min(100%,600px)] overflow-hidden rounded-sm border">
- {Excalidraw && (
- <Excalidraw {...nodeProps} {...(excalidrawProps as any)} />
- )}
- </div>
- </div>
- {children}
- </PlateElement>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/fixed-toolbar-buttons.tsx b/apps/web/app/components/plate-ui/fixed-toolbar-buttons.tsx
deleted file mode 100644
index 8d9ed744..00000000
--- a/apps/web/app/components/plate-ui/fixed-toolbar-buttons.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import {
- BoldPlugin,
- CodePlugin,
- ItalicPlugin,
- StrikethroughPlugin,
- UnderlinePlugin,
-} from '@udecode/plate-basic-marks/react';
-import { useEditorReadOnly } from '@udecode/plate-common/react';
-import {
- FontBackgroundColorPlugin,
- FontColorPlugin,
-} from '@udecode/plate-font/react';
-import { ListStyleType } from '@udecode/plate-indent-list';
-import { ImagePlugin } from '@udecode/plate-media/react';
-import {
- BaselineIcon,
- BoldIcon,
- Code2Icon,
- ItalicIcon,
- PaintBucketIcon,
- SparklesIcon,
- StrikethroughIcon,
- UnderlineIcon,
-} from 'lucide-react';
-
-import { AIToolbarButton } from './ai-toolbar-button';
-import { AlignDropdownMenu } from './align-dropdown-menu';
-import { ColorDropdownMenu } from './color-dropdown-menu';
-import { CommentToolbarButton } from './comment-toolbar-button';
-import { IndentListToolbarButton } from './indent-list-toolbar-button';
-import { IndentTodoToolbarButton } from './indent-todo-toolbar-button';
-import { IndentToolbarButton } from './indent-toolbar-button';
-import { InsertDropdownMenu } from './insert-dropdown-menu';
-import { LineHeightDropdownMenu } from './line-height-dropdown-menu';
-import { LinkToolbarButton } from './link-toolbar-button';
-import { MarkToolbarButton } from './mark-toolbar-button';
-import { MediaToolbarButton } from './media-toolbar-button';
-import { ModeDropdownMenu } from './mode-dropdown-menu';
-import { MoreDropdownMenu } from './more-dropdown-menu';
-import { OutdentToolbarButton } from './outdent-toolbar-button';
-import { TableDropdownMenu } from './table-dropdown-menu';
-import { ToggleToolbarButton } from './toggle-toolbar-button';
-import { ToolbarGroup } from './toolbar';
-import { TurnIntoDropdownMenu } from './turn-into-dropdown-menu';
-
-export function FixedToolbarButtons() {
- const readOnly = useEditorReadOnly();
-
- return (
- <div className="flex w-full">
- {!readOnly && (
- <>
- <ToolbarGroup>
- <AIToolbarButton
- className="gap-1.5 text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-500"
- tooltip="Edit, generate, and more"
- >
- <SparklesIcon className="!size-3.5" />
- Ask AI
- </AIToolbarButton>
- </ToolbarGroup>
-
- <ToolbarGroup>
- <InsertDropdownMenu />
- <TurnIntoDropdownMenu />
- </ToolbarGroup>
-
- <ToolbarGroup>
- <MarkToolbarButton nodeType={BoldPlugin.key} tooltip="Bold (⌘+B)">
- <BoldIcon />
- </MarkToolbarButton>
-
- <MarkToolbarButton
- nodeType={ItalicPlugin.key}
- tooltip="Italic (⌘+I)"
- >
- <ItalicIcon />
- </MarkToolbarButton>
-
- <MarkToolbarButton
- nodeType={UnderlinePlugin.key}
- tooltip="Underline (⌘+U)"
- >
- <UnderlineIcon />
- </MarkToolbarButton>
-
- <MarkToolbarButton
- nodeType={StrikethroughPlugin.key}
- tooltip="Strikethrough (⌘+⇧+M)"
- >
- <StrikethroughIcon />
- </MarkToolbarButton>
-
- <MarkToolbarButton nodeType={CodePlugin.key} tooltip="Code (⌘+E)">
- <Code2Icon />
- </MarkToolbarButton>
-
- <ColorDropdownMenu
- nodeType={FontColorPlugin.key}
- tooltip="Text color"
- >
- <BaselineIcon />
- </ColorDropdownMenu>
-
- <ColorDropdownMenu
- nodeType={FontBackgroundColorPlugin.key}
- tooltip="Background color"
- >
- <PaintBucketIcon />
- </ColorDropdownMenu>
- </ToolbarGroup>
-
- <ToolbarGroup>
- <AlignDropdownMenu />
- <LineHeightDropdownMenu />
-
- <IndentListToolbarButton nodeType={ListStyleType.Disc} />
- <IndentListToolbarButton nodeType={ListStyleType.Decimal} />
- <IndentTodoToolbarButton />
-
- <OutdentToolbarButton />
- <IndentToolbarButton />
- </ToolbarGroup>
-
- <ToolbarGroup>
- <LinkToolbarButton />
- <ToggleToolbarButton />
- <MediaToolbarButton nodeType={ImagePlugin.key} />
- <TableDropdownMenu />
- <MoreDropdownMenu />
- </ToolbarGroup>
- </>
- )}
-
- <div className="grow" />
-
- <ToolbarGroup>
- <CommentToolbarButton />
- <ModeDropdownMenu />
- </ToolbarGroup>
- </div>
- );
-}
diff --git a/apps/web/app/components/plate-ui/fixed-toolbar.tsx b/apps/web/app/components/plate-ui/fixed-toolbar.tsx
deleted file mode 100644
index b9fdcf68..00000000
--- a/apps/web/app/components/plate-ui/fixed-toolbar.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-'use client';
-
-import { withCn } from '@udecode/cn';
-
-import { Toolbar } from './toolbar';
-
-export const FixedToolbar = withCn(
- Toolbar,
- 'supports-backdrop-blur:bg-background/60 sticky left-0 top-0 z-50 w-full justify-between overflow-x-auto rounded-t-lg border-b border-b-border bg-background/95 p-1 backdrop-blur scrollbar-hide'
-);
diff --git a/apps/web/app/components/plate-ui/floating-toolbar-buttons.tsx b/apps/web/app/components/plate-ui/floating-toolbar-buttons.tsx
deleted file mode 100644
index f2995fc0..00000000
--- a/apps/web/app/components/plate-ui/floating-toolbar-buttons.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import {
- BoldPlugin,
- CodePlugin,
- ItalicPlugin,
- StrikethroughPlugin,
- UnderlinePlugin,
-} from '@udecode/plate-basic-marks/react';
-import { useEditorReadOnly } from '@udecode/plate-common/react';
-import {
- BoldIcon,
- Code2Icon,
- ItalicIcon,
- SparklesIcon,
- StrikethroughIcon,
- UnderlineIcon,
-} from 'lucide-react';
-
-import { AIToolbarButton } from './ai-toolbar-button';
-import { CommentToolbarButton } from './comment-toolbar-button';
-import { LinkToolbarButton } from './link-toolbar-button';
-import { MarkToolbarButton } from './mark-toolbar-button';
-import { MoreDropdownMenu } from './more-dropdown-menu';
-import { ToolbarGroup } from './toolbar';
-import { TurnIntoDropdownMenu } from './turn-into-dropdown-menu';
-
-export function FloatingToolbarButtons() {
- const readOnly = useEditorReadOnly();
-
- return (
- <>
- {!readOnly && (
- <>
- <ToolbarGroup>
- <AIToolbarButton
- className="gap-1.5 text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-500"
- tooltip="Edit, generate, and more"
- >
- <SparklesIcon className="!size-3.5" />
- Ask AI
- </AIToolbarButton>
- </ToolbarGroup>
-
- <ToolbarGroup>
- <TurnIntoDropdownMenu />
-
- <MarkToolbarButton nodeType={BoldPlugin.key} tooltip="Bold (⌘+B)">
- <BoldIcon />
- </MarkToolbarButton>
-
- <MarkToolbarButton
- nodeType={ItalicPlugin.key}
- tooltip="Italic (⌘+I)"
- >
- <ItalicIcon />
- </MarkToolbarButton>
-
- <MarkToolbarButton
- nodeType={UnderlinePlugin.key}
- tooltip="Underline (⌘+U)"
- >
- <UnderlineIcon />
- </MarkToolbarButton>
-
- <MarkToolbarButton
- nodeType={StrikethroughPlugin.key}
- tooltip="Strikethrough (⌘+⇧+M)"
- >
- <StrikethroughIcon />
- </MarkToolbarButton>
-
- <MarkToolbarButton nodeType={CodePlugin.key} tooltip="Code (⌘+E)">
- <Code2Icon />
- </MarkToolbarButton>
-
- <LinkToolbarButton />
- </ToolbarGroup>
- </>
- )}
-
- <ToolbarGroup>
- <CommentToolbarButton />
-
- {!readOnly && <MoreDropdownMenu />}
- </ToolbarGroup>
- </>
- );
-}
diff --git a/apps/web/app/components/plate-ui/floating-toolbar.tsx b/apps/web/app/components/plate-ui/floating-toolbar.tsx
deleted file mode 100644
index 23c418dd..00000000
--- a/apps/web/app/components/plate-ui/floating-toolbar.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn, withRef } from '@udecode/cn';
-import {
- useComposedRef,
- useEditorId,
- useEditorRef,
- useEventEditorSelectors,
-} from '@udecode/plate-common/react';
-import {
- type FloatingToolbarState,
- flip,
- offset,
- useFloatingToolbar,
- useFloatingToolbarState,
-} from '@udecode/plate-floating';
-
-import { Toolbar } from './toolbar';
-
-export const FloatingToolbar = withRef<
- typeof Toolbar,
- {
- state?: FloatingToolbarState;
- }
->(({ children, state, ...props }, componentRef) => {
- const editor = useEditorRef();
- const editorId = useEditorId();
- const focusedEditorId = useEventEditorSelectors.focus();
- const isFloatingLinkOpen = !!editor.useOption({ key: 'a' }, 'mode');
- const isAIChatOpen = editor.useOption({ key: 'aiChat' }, 'open');
-
- const floatingToolbarState = useFloatingToolbarState({
- editorId,
- focusedEditorId,
- hideToolbar: isFloatingLinkOpen || isAIChatOpen,
- ...state,
- floatingOptions: {
- middleware: [
- offset(12),
- flip({
- fallbackPlacements: [
- 'top-start',
- 'top-end',
- 'bottom-start',
- 'bottom-end',
- ],
- padding: 12,
- }),
- ],
- placement: 'top',
- ...state?.floatingOptions,
- },
- });
-
- const {
- clickOutsideRef,
- hidden,
- props: rootProps,
- ref: floatingRef,
- } = useFloatingToolbar(floatingToolbarState);
-
- const ref = useComposedRef<HTMLDivElement>(componentRef, floatingRef);
-
- if (hidden) return null;
-
- return (
- <div ref={clickOutsideRef}>
- <Toolbar
- ref={ref}
- className={cn(
- 'absolute z-50 overflow-x-auto whitespace-nowrap rounded-md border bg-popover p-1 opacity-100 shadow-md scrollbar-hide print:hidden',
- 'max-w-[80vw]'
- )}
- {...rootProps}
- {...props}
- >
- {children}
- </Toolbar>
- </div>
- );
-});
diff --git a/apps/web/app/components/plate-ui/ghost-text.tsx b/apps/web/app/components/plate-ui/ghost-text.tsx
deleted file mode 100644
index e99bb787..00000000
--- a/apps/web/app/components/plate-ui/ghost-text.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { CopilotPluginConfig } from '@udecode/plate-ai/react';
-
-import { useEditorPlugin, useElement } from '@udecode/plate-common/react';
-
-export const GhostText = () => {
- const { useOption } = useEditorPlugin<CopilotPluginConfig>({
- key: 'copilot',
- });
- const element = useElement();
-
- const isSuggested = useOption('isSuggested', element.id as string);
-
- if (!isSuggested) return null;
-
- return <GhostTextContent />;
-};
-
-export function GhostTextContent() {
- const { useOption } = useEditorPlugin<CopilotPluginConfig>({
- key: 'copilot',
- });
-
- const suggestionText = useOption('suggestionText');
-
- return (
- <span
- className="max-sm:hidden pointer-events-none text-muted-foreground/70"
- contentEditable={false}
- >
- {suggestionText && suggestionText}
- </span>
- );
-}
diff --git a/apps/web/app/components/plate-ui/heading-element.tsx b/apps/web/app/components/plate-ui/heading-element.tsx
deleted file mode 100644
index 9386d1df..00000000
--- a/apps/web/app/components/plate-ui/heading-element.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { withRef, withVariants } from '@udecode/cn';
-import { cva } from 'class-variance-authority';
-
-import { PlateElement } from './plate-element';
-
-const headingVariants = cva('relative mb-1', {
- variants: {
- variant: {
- h1: 'mt-[1.6em] pb-1 font-heading text-4xl font-bold',
- h2: 'mt-[1.4em] pb-px font-heading text-2xl font-semibold tracking-tight',
- h3: 'mt-[1em] pb-px font-heading text-xl font-semibold tracking-tight',
- h4: 'mt-[0.75em] font-heading text-lg font-semibold tracking-tight',
- h5: 'mt-[0.75em] text-lg font-semibold tracking-tight',
- h6: 'mt-[0.75em] text-base font-semibold tracking-tight',
- },
- },
-});
-
-const HeadingElementVariants = withVariants(PlateElement, headingVariants, [
- 'variant',
-]);
-
-export const HeadingElement = withRef<typeof HeadingElementVariants>(
- ({ children, variant = 'h1', ...props }, ref) => {
- return (
- <HeadingElementVariants
- ref={ref}
- as={variant!}
- variant={variant}
- {...props}
- >
- {children}
- </HeadingElementVariants>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/highlight-leaf.tsx b/apps/web/app/components/plate-ui/highlight-leaf.tsx
deleted file mode 100644
index 09f7f2d7..00000000
--- a/apps/web/app/components/plate-ui/highlight-leaf.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn, withRef } from '@udecode/cn';
-import { PlateLeaf } from '@udecode/plate-common/react';
-
-export const HighlightLeaf = withRef<typeof PlateLeaf>(
- ({ children, className, ...props }, ref) => (
- <PlateLeaf
- ref={ref}
- asChild
- className={cn('bg-highlight/30 text-inherit', className)}
- {...props}
- >
- <mark>{children}</mark>
- </PlateLeaf>
- )
-);
diff --git a/apps/web/app/components/plate-ui/hr-element.tsx b/apps/web/app/components/plate-ui/hr-element.tsx
deleted file mode 100644
index ea60a650..00000000
--- a/apps/web/app/components/plate-ui/hr-element.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn, withRef } from '@udecode/cn';
-import { useFocused, useSelected } from 'slate-react';
-
-import { PlateElement } from './plate-element';
-
-export const HrElement = withRef<typeof PlateElement>(
- ({ className, nodeProps, ...props }, ref) => {
- const { children } = props;
-
- const selected = useSelected();
- const focused = useFocused();
-
- return (
- <PlateElement ref={ref} className={className} {...props}>
- <div className="py-6" contentEditable={false}>
- <hr
- {...nodeProps}
- className={cn(
- 'h-0.5 cursor-pointer rounded-sm border-none bg-muted bg-clip-content',
- selected && focused && 'ring-2 ring-ring ring-offset-2'
- )}
- />
- </div>
- {children}
- </PlateElement>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/image-element.tsx b/apps/web/app/components/plate-ui/image-element.tsx
deleted file mode 100644
index d5915ce9..00000000
--- a/apps/web/app/components/plate-ui/image-element.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn, withRef } from '@udecode/cn';
-import { withHOC } from '@udecode/plate-common/react';
-import { Image, ImagePlugin, useMediaState } from '@udecode/plate-media/react';
-import { ResizableProvider, useResizableStore } from '@udecode/plate-resizable';
-
-import { Caption, CaptionTextarea } from './caption';
-import { MediaPopover } from './media-popover';
-import { PlateElement } from './plate-element';
-import {
- Resizable,
- ResizeHandle,
- mediaResizeHandleVariants,
-} from './resizable';
-
-export const ImageElement = withHOC(
- ResizableProvider,
- withRef<typeof PlateElement>(
- ({ children, className, nodeProps, ...props }, ref) => {
- const { align = 'center', focused, readOnly, selected } = useMediaState();
-
- const width = useResizableStore().get.width();
-
- return (
- <MediaPopover plugin={ImagePlugin}>
- <PlateElement
- ref={ref}
- className={cn('py-2.5', className)}
- {...props}
- >
- <figure className="group relative m-0" contentEditable={false}>
- <Resizable
- align={align}
- options={{
- align,
- readOnly,
- }}
- >
- <ResizeHandle
- className={mediaResizeHandleVariants({ direction: 'left' })}
- options={{ direction: 'left' }}
- />
- <Image
- className={cn(
- 'block w-full max-w-full cursor-pointer object-cover px-0',
- 'rounded-sm',
- focused && selected && 'ring-2 ring-ring ring-offset-2'
- )}
- alt=""
- {...nodeProps}
- />
- <ResizeHandle
- className={mediaResizeHandleVariants({
- direction: 'right',
- })}
- options={{ direction: 'right' }}
- />
- </Resizable>
-
- <Caption style={{ width }} align={align}>
- <CaptionTextarea
- readOnly={readOnly}
- onFocus={(e) => {
- e.preventDefault();
- }}
- placeholder="Write a caption..."
- />
- </Caption>
- </figure>
-
- {children}
- </PlateElement>
- </MediaPopover>
- );
- }
- )
-);
diff --git a/apps/web/app/components/plate-ui/image-preview.tsx b/apps/web/app/components/plate-ui/image-preview.tsx
deleted file mode 100644
index c9f3191b..00000000
--- a/apps/web/app/components/plate-ui/image-preview.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-'use client';
-
-import { cn, createPrimitiveComponent } from '@udecode/cn';
-import {
- PreviewImage,
- useImagePreview,
- useImagePreviewState,
- useScaleInput,
- useScaleInputState,
-} from '@udecode/plate-media/react';
-import { cva } from 'class-variance-authority';
-import { ArrowLeft, ArrowRight, Download, Minus, Plus, X } from 'lucide-react';
-
-const toolButtonVariants = cva('rounded bg-[rgba(0,0,0,0.5)] px-1', {
- defaultVariants: {
- variant: 'default',
- },
- variants: {
- variant: {
- default: 'text-white',
- disabled: 'cursor-not-allowed text-gray-400',
- },
- },
-});
-
-const ScaleInput = createPrimitiveComponent('input')({
- propsHook: useScaleInput,
- stateHook: useScaleInputState,
-});
-
-const SCROLL_SPEED = 4;
-
-export const ImagePreview = () => {
- const state = useImagePreviewState({ scrollSpeed: SCROLL_SPEED });
-
- const {
- closeProps,
- currentUrlIndex,
- maskLayerProps,
- nextDisabled,
- nextProps,
- prevDisabled,
- prevProps,
- scaleTextProps,
- zommOutProps,
- zoomInDisabled,
- zoomInProps,
- zoomOutDisabled,
- } = useImagePreview(state);
-
- const { isOpen, scale } = state;
-
- return (
- <div
- className={cn(
- 'fixed left-0 top-0 z-50 h-screen w-screen',
- !isOpen && 'hidden'
- )}
- {...maskLayerProps}
- >
- <div className="absolute inset-0 size-full bg-black opacity-30"></div>
- <div className="absolute inset-0 size-full bg-black opacity-30"></div>
- <div className="absolute inset-0 flex items-center justify-center ">
- <div className="relative flex max-h-screen w-full items-center">
- <PreviewImage
- className={cn(
- 'mx-auto block max-h-[calc(100vh-4rem)] w-auto object-contain transition-transform'
- )}
- />
- <div
- className="absolute bottom-0 left-1/2 z-40 flex w-fit -translate-x-1/2 justify-center gap-4 p-2 text-center text-white"
- onClick={(e) => e.stopPropagation()}
- >
- <div className="flex gap-1">
- <button
- {...prevProps}
- className={cn(
- toolButtonVariants({
- variant: prevDisabled ? 'disabled' : 'default',
- })
- )}
- type="button"
- >
- <ArrowLeft />
- </button>
- {(currentUrlIndex ?? 0) + 1}
- <button
- {...nextProps}
- className={cn(
- toolButtonVariants({
- variant: nextDisabled ? 'disabled' : 'default',
- })
- )}
- type="button"
- >
- <ArrowRight />
- </button>
- </div>
- <div className="flex ">
- <button
- className={cn(
- toolButtonVariants({
- variant: zoomOutDisabled ? 'disabled' : 'default',
- })
- )}
- {...zommOutProps}
- type="button"
- >
- <Minus className="size-4" />
- </button>
- <div className="mx-px">
- {state.isEditingScale ? (
- <>
- <ScaleInput className="w-10 rounded px-1 text-slate-500 outline" />{' '}
- <span>%</span>
- </>
- ) : (
- <span {...scaleTextProps}>{scale * 100 + '%'}</span>
- )}
- </div>
- <button
- className={cn(
- toolButtonVariants({
- variant: zoomInDisabled ? 'disabled' : 'default',
- })
- )}
- {...zoomInProps}
- type="button"
- >
- <Plus className="size-4" />
- </button>
- </div>
- {/* TODO: downLoad the image */}
- <button className={cn(toolButtonVariants())} type="button">
- <Download className="size-4" />
- </button>
- <button
- {...closeProps}
- className={cn(toolButtonVariants())}
- type="button"
- >
- <X className="size-4" />
- </button>
- </div>
- </div>
- </div>
- </div>
- );
-};
diff --git a/apps/web/app/components/plate-ui/indent-fire-marker.tsx b/apps/web/app/components/plate-ui/indent-fire-marker.tsx
deleted file mode 100644
index 5887dcd5..00000000
--- a/apps/web/app/components/plate-ui/indent-fire-marker.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import type { PlateRenderElementProps } from '@udecode/plate-common/react';
-import type { TIndentElement } from '@udecode/plate-indent';
-
-export const FireMarker = (
- props: Omit<PlateRenderElementProps, 'children'>
-) => {
- const { element } = props;
-
- return (
- <div contentEditable={false}>
- <span style={{ left: -26, position: 'absolute', top: -1 }}>
- {(element as TIndentElement).indent % 2 === 0 ? '🔥' : '🚀'}
- </span>
- </div>
- );
-};
-
-export const FireLiComponent = (props: PlateRenderElementProps) => {
- const { children } = props;
-
- return <span>{children}</span>;
-};
diff --git a/apps/web/app/components/plate-ui/indent-list-toolbar-button.tsx b/apps/web/app/components/plate-ui/indent-list-toolbar-button.tsx
deleted file mode 100644
index 7e863d6d..00000000
--- a/apps/web/app/components/plate-ui/indent-list-toolbar-button.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { withRef } from '@udecode/cn';
-import { ListStyleType } from '@udecode/plate-indent-list';
-import {
- useIndentListToolbarButton,
- useIndentListToolbarButtonState,
-} from '@udecode/plate-indent-list/react';
-import { List, ListOrdered } from 'lucide-react';
-
-import { ToolbarButton } from './toolbar';
-
-export const IndentListToolbarButton = withRef<
- typeof ToolbarButton,
- {
- nodeType?: ListStyleType;
- }
->(({ nodeType = ListStyleType.Disc }, ref) => {
- const state = useIndentListToolbarButtonState({ nodeType });
- const { props } = useIndentListToolbarButton(state);
-
- return (
- <ToolbarButton
- ref={ref}
- tooltip={
- nodeType === ListStyleType.Disc ? 'Bulleted List' : 'Numbered List'
- }
- {...props}
- >
- {nodeType === ListStyleType.Disc ? <List /> : <ListOrdered />}
- </ToolbarButton>
- );
-});
diff --git a/apps/web/app/components/plate-ui/indent-todo-marker.tsx b/apps/web/app/components/plate-ui/indent-todo-marker.tsx
deleted file mode 100644
index 9d441367..00000000
--- a/apps/web/app/components/plate-ui/indent-todo-marker.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-'use client';
-
-import type { PlateRenderElementProps } from '@udecode/plate-common/react';
-
-import { cn } from '@udecode/cn';
-import {
- useIndentTodoListElement,
- useIndentTodoListElementState,
-} from '@udecode/plate-indent-list/react';
-
-import { Checkbox } from './checkbox';
-
-export const TodoMarker = ({
- element,
-}: Omit<PlateRenderElementProps, 'children'>) => {
- const state = useIndentTodoListElementState({ element });
- const { checkboxProps } = useIndentTodoListElement(state);
-
- return (
- <div contentEditable={false}>
- <Checkbox
- style={{ left: -24, position: 'absolute', top: 4 }}
- {...checkboxProps}
- />
- </div>
- );
-};
-
-export const TodoLi = (props: PlateRenderElementProps) => {
- const { children, element } = props;
-
- return (
- <span
- className={cn(
- (element.checked as boolean) && 'text-muted-foreground line-through'
- )}
- >
- {children}
- </span>
- );
-};
diff --git a/apps/web/app/components/plate-ui/indent-todo-toolbar-button.tsx b/apps/web/app/components/plate-ui/indent-todo-toolbar-button.tsx
deleted file mode 100644
index f8fe8504..00000000
--- a/apps/web/app/components/plate-ui/indent-todo-toolbar-button.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-'use client';
-
-import { withRef } from '@udecode/cn';
-import {
- useIndentTodoToolBarButton,
- useIndentTodoToolBarButtonState,
-} from '@udecode/plate-indent-list/react';
-import { Square } from 'lucide-react';
-
-import { ToolbarButton } from './toolbar';
-
-export const IndentTodoToolbarButton = withRef<typeof ToolbarButton>(
- (rest, ref) => {
- const state = useIndentTodoToolBarButtonState({ nodeType: 'todo' });
- const { props } = useIndentTodoToolBarButton(state);
-
- return (
- <ToolbarButton ref={ref} tooltip="Todo" {...props} {...rest}>
- <Square />
- </ToolbarButton>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/indent-toolbar-button.tsx b/apps/web/app/components/plate-ui/indent-toolbar-button.tsx
deleted file mode 100644
index 73f7e450..00000000
--- a/apps/web/app/components/plate-ui/indent-toolbar-button.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { withRef } from '@udecode/cn';
-import { useIndentButton } from '@udecode/plate-indent/react';
-import { Indent } from 'lucide-react';
-
-import { ToolbarButton } from './toolbar';
-
-export const IndentToolbarButton = withRef<typeof ToolbarButton>(
- (rest, ref) => {
- const { props } = useIndentButton();
-
- return (
- <ToolbarButton ref={ref} tooltip="Indent" {...props} {...rest}>
- <Indent />
- </ToolbarButton>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/inline-combobox.tsx b/apps/web/app/components/plate-ui/inline-combobox.tsx
deleted file mode 100644
index 33af3db2..00000000
--- a/apps/web/app/components/plate-ui/inline-combobox.tsx
+++ /dev/null
@@ -1,405 +0,0 @@
-'use client';
-
-import React, {
- type HTMLAttributes,
- type ReactNode,
- type RefObject,
- createContext,
- forwardRef,
- startTransition,
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useState,
-} from 'react';
-
-import type { PointRef } from 'slate';
-
-import {
- type ComboboxItemProps,
- Combobox,
- ComboboxGroup,
- ComboboxGroupLabel,
- ComboboxItem,
- ComboboxPopover,
- ComboboxProvider,
- ComboboxRow,
- Portal,
- useComboboxContext,
- useComboboxStore,
-} from '@ariakit/react';
-import { cn, withCn } from '@udecode/cn';
-import { filterWords } from '@udecode/plate-combobox';
-import {
- type UseComboboxInputResult,
- useComboboxInput,
- useHTMLInputCursorState,
-} from '@udecode/plate-combobox/react';
-import {
- type TElement,
- createPointRef,
- getPointBefore,
- insertText,
- moveSelection,
-} from '@udecode/plate-common';
-import {
- findNodePath,
- useComposedRef,
- useEditorRef,
-} from '@udecode/plate-common/react';
-import { cva } from 'class-variance-authority';
-
-type FilterFn = (
- item: { value: string; group?: string; keywords?: string[]; label?: string },
- search: string
-) => boolean;
-
-interface InlineComboboxContextValue {
- filter: FilterFn | false;
- inputProps: UseComboboxInputResult['props'];
- inputRef: RefObject<HTMLInputElement | null>;
- removeInput: UseComboboxInputResult['removeInput'];
- setHasEmpty: (hasEmpty: boolean) => void;
- showTrigger: boolean;
- trigger: string;
-}
-
-const InlineComboboxContext = createContext<InlineComboboxContextValue>(
- null as any
-);
-
-export const defaultFilter: FilterFn = (
- { group, keywords = [], label, value },
- search
-) => {
- const uniqueTerms = new Set(
- [value, ...keywords, group, label].filter(Boolean)
- );
-
- return Array.from(uniqueTerms).some((keyword) =>
- filterWords(keyword!, search)
- );
-};
-
-interface InlineComboboxProps {
- children: ReactNode;
- element: TElement;
- trigger: string;
- filter?: FilterFn | false;
- hideWhenNoValue?: boolean;
- setValue?: (value: string) => void;
- showTrigger?: boolean;
- value?: string;
-}
-
-const InlineCombobox = ({
- children,
- element,
- filter = defaultFilter,
- hideWhenNoValue = false,
- setValue: setValueProp,
- showTrigger = true,
- trigger,
- value: valueProp,
-}: InlineComboboxProps) => {
- const editor = useEditorRef();
- const inputRef = React.useRef<HTMLInputElement>(null);
- const cursorState = useHTMLInputCursorState(inputRef);
-
- const [valueState, setValueState] = useState('');
- const hasValueProp = valueProp !== undefined;
- const value = hasValueProp ? valueProp : valueState;
-
- const setValue = useCallback(
- (newValue: string) => {
- setValueProp?.(newValue);
-
- if (!hasValueProp) {
- setValueState(newValue);
- }
- },
- [setValueProp, hasValueProp]
- );
-
- /**
- * Track the point just before the input element so we know where to
- * insertText if the combobox closes due to a selection change.
- */
- const [insertPoint, setInsertPoint] = useState<PointRef | null>(null);
-
- useEffect(() => {
- const path = findNodePath(editor, element);
-
- if (!path) return;
-
- const point = getPointBefore(editor, path);
-
- if (!point) return;
-
- const pointRef = createPointRef(editor, point);
- setInsertPoint(pointRef);
-
- return () => {
- pointRef.unref();
- };
- }, [editor, element]);
-
- const { props: inputProps, removeInput } = useComboboxInput({
- cancelInputOnBlur: false,
- cursorState,
- ref: inputRef,
- onCancelInput: (cause) => {
- if (cause !== 'backspace') {
- insertText(editor, trigger + value, {
- at: insertPoint?.current ?? undefined,
- });
- }
- if (cause === 'arrowLeft' || cause === 'arrowRight') {
- moveSelection(editor, {
- distance: 1,
- reverse: cause === 'arrowLeft',
- });
- }
- },
- });
-
- const [hasEmpty, setHasEmpty] = useState(false);
-
- const contextValue: InlineComboboxContextValue = useMemo(
- () => ({
- filter,
- inputProps,
- inputRef,
- removeInput,
- setHasEmpty,
- showTrigger,
- trigger,
- }),
- [
- trigger,
- showTrigger,
- filter,
- inputRef,
- inputProps,
- removeInput,
- setHasEmpty,
- ]
- );
-
- const store = useComboboxStore({
- // open: ,
- setValue: (newValue) => startTransition(() => setValue(newValue)),
- });
-
- const items = store.useState('items');
-
- /**
- * If there is no active ID and the list of items changes, select the first
- * item.
- */
- useEffect(() => {
- if (!store.getState().activeId) {
- store.setActiveId(store.first());
- }
-
- }, [items, store]);
-
- return (
- <span contentEditable={false}>
- <ComboboxProvider
- open={
- (items.length > 0 || hasEmpty) &&
- (!hideWhenNoValue || value.length > 0)
- }
- store={store}
- >
- <InlineComboboxContext.Provider value={contextValue}>
- {children}
- </InlineComboboxContext.Provider>
- </ComboboxProvider>
- </span>
- );
-};
-
-const InlineComboboxInput = forwardRef<
- HTMLInputElement,
- HTMLAttributes<HTMLInputElement>
->(({ className, ...props }, propRef) => {
- const {
- inputProps,
- inputRef: contextRef,
- showTrigger,
- trigger,
- } = useContext(InlineComboboxContext);
-
- const store = useComboboxContext()!;
- const value = store.useState('value');
-
- const ref = useComposedRef(propRef, contextRef);
-
- /**
- * To create an auto-resizing input, we render a visually hidden span
- * containing the input value and position the input element on top of it.
- * This works well for all cases except when input exceeds the width of the
- * container.
- */
-
- return (
- <>
- {showTrigger && trigger}
-
- <span className="relative min-h-[1lh]">
- <span
- className="invisible overflow-hidden text-nowrap"
- aria-hidden="true"
- >
- {value || '\u200B'}
- </span>
-
- <Combobox
- ref={ref}
- className={cn(
- 'absolute left-0 top-0 size-full bg-transparent outline-none',
- className
- )}
- value={value}
- autoSelect
- {...inputProps}
- {...props}
- />
- </span>
- </>
- );
-});
-
-InlineComboboxInput.displayName = 'InlineComboboxInput';
-
-const InlineComboboxContent: typeof ComboboxPopover = ({
- className,
- ...props
-}) => {
- // Portal prevents CSS from leaking into popover
- return (
- <Portal>
- <ComboboxPopover
- className={cn(
- 'z-[500] max-h-[288px] w-[300px] overflow-y-auto rounded-md bg-popover shadow-md',
- className
- )}
- {...props}
- />
- </Portal>
- );
-};
-
-const comboboxItemVariants = cva(
- 'relative mx-1 flex h-[28px] select-none items-center rounded-sm px-2 text-sm text-foreground outline-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
- {
- defaultVariants: {
- interactive: true,
- },
- variants: {
- interactive: {
- false: '',
- true: 'cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground data-[active-item=true]:bg-accent data-[active-item=true]:text-accent-foreground',
- },
- },
- }
-);
-
-export type InlineComboboxItemProps = {
- focusEditor?: boolean;
- group?: string;
- keywords?: string[];
- label?: string;
-} & ComboboxItemProps &
- Required<Pick<ComboboxItemProps, 'value'>>;
-
-const InlineComboboxItem = ({
- className,
- focusEditor = true,
- group,
- keywords,
- label,
- onClick,
- ...props
-}: InlineComboboxItemProps) => {
- const { value } = props;
-
- const { filter, removeInput } = useContext(InlineComboboxContext);
-
- const store = useComboboxContext()!;
-
- // Optimization: Do not subscribe to value if filter is false
- const search = filter && store.useState('value');
-
- const visible = useMemo(
- () =>
- !filter || filter({ group, keywords, label, value }, search as string),
- [filter, group, keywords, label, value, search]
- );
-
- if (!visible) return null;
-
- return (
- <ComboboxItem
- className={cn(comboboxItemVariants(), className)}
- onClick={(event) => {
- removeInput(focusEditor);
- onClick?.(event);
- }}
- {...props}
- />
- );
-};
-
-const InlineComboboxEmpty = ({
- children,
- className,
-}: HTMLAttributes<HTMLDivElement>) => {
- const { setHasEmpty } = useContext(InlineComboboxContext);
- const store = useComboboxContext()!;
- const items = store.useState('items');
-
- useEffect(() => {
- setHasEmpty(true);
-
- return () => {
- setHasEmpty(false);
- };
- }, [setHasEmpty]);
-
- if (items.length > 0) return null;
-
- return (
- <div
- className={cn(comboboxItemVariants({ interactive: false }), className)}
- >
- {children}
- </div>
- );
-};
-
-const InlineComboboxRow = ComboboxRow;
-
-const InlineComboboxGroup = withCn(
- ComboboxGroup,
- 'hidden py-1.5 [&:has([role=option])]:block [&:not(:last-child)]:border-b'
-);
-
-const InlineComboboxGroupLabel = withCn(
- ComboboxGroupLabel,
- 'mb-2 mt-1.5 px-3 text-xs font-medium text-muted-foreground'
-);
-
-export {
- InlineCombobox,
- InlineComboboxContent,
- InlineComboboxEmpty,
- InlineComboboxGroup,
- InlineComboboxGroupLabel,
- InlineComboboxInput,
- InlineComboboxItem,
- InlineComboboxRow,
-};
diff --git a/apps/web/app/components/plate-ui/input.tsx b/apps/web/app/components/plate-ui/input.tsx
deleted file mode 100644
index 42119f71..00000000
--- a/apps/web/app/components/plate-ui/input.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { withVariants } from '@udecode/cn';
-import { cva } from 'class-variance-authority';
-
-export const inputVariants = cva(
- 'flex w-full rounded-md bg-transparent text-sm file:border-0 file:bg-background file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
- {
- defaultVariants: {
- h: 'md',
- variant: 'default',
- },
- variants: {
- h: {
- md: 'h-10 px-3 py-2',
- sm: 'h-[28px] px-1.5 py-1',
- },
- variant: {
- default:
- 'border border-input ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
- ghost: 'border-none focus-visible:ring-transparent',
- },
- },
- }
-);
-
-export const Input = withVariants('input', inputVariants, ['variant', 'h']);
diff --git a/apps/web/app/components/plate-ui/insert-dropdown-menu.tsx b/apps/web/app/components/plate-ui/insert-dropdown-menu.tsx
deleted file mode 100644
index 58b436f7..00000000
--- a/apps/web/app/components/plate-ui/insert-dropdown-menu.tsx
+++ /dev/null
@@ -1,261 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
-
-import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
-import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
-import {
- type PlateEditor,
- ParagraphPlugin,
- focusEditor,
- useEditorRef,
-} from '@udecode/plate-common/react';
-import { DatePlugin } from '@udecode/plate-date/react';
-import { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react';
-import { HEADING_KEYS } from '@udecode/plate-heading';
-import { TocPlugin } from '@udecode/plate-heading/react';
-import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
-import { INDENT_LIST_KEYS, ListStyleType } from '@udecode/plate-indent-list';
-import { LinkPlugin } from '@udecode/plate-link/react';
-import { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react';
-import { TablePlugin } from '@udecode/plate-table/react';
-import { TogglePlugin } from '@udecode/plate-toggle/react';
-import {
- CalendarIcon,
- ChevronRightIcon,
- Columns3Icon,
- FileCodeIcon,
- FilmIcon,
- Heading1Icon,
- Heading2Icon,
- Heading3Icon,
- ImageIcon,
- Link2Icon,
- ListIcon,
- ListOrderedIcon,
- MinusIcon,
- PenToolIcon,
- PilcrowIcon,
- PlusIcon,
- QuoteIcon,
- SquareIcon,
- TableIcon,
- TableOfContentsIcon,
-} from 'lucide-react';
-
-import {
- insertBlock,
- insertInlineElement,
-} from '~/components/editor/transforms';
-
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuTrigger,
- useOpenState,
-} from './dropdown-menu';
-import { ToolbarButton } from './toolbar';
-
-type Group = {
- group: string;
- items: Item[];
-};
-
-interface Item {
- icon: React.ReactNode;
- onSelect: (editor: PlateEditor, value: string) => void;
- value: string;
- focusEditor?: boolean;
- label?: string;
-}
-
-const groups: Group[] = [
- {
- group: 'Basic blocks',
- items: [
- {
- icon: <PilcrowIcon />,
- label: 'Paragraph',
- value: ParagraphPlugin.key,
- },
- {
- icon: <Heading1Icon />,
- label: 'Heading 1',
- value: HEADING_KEYS.h1,
- },
- {
- icon: <Heading2Icon />,
- label: 'Heading 2',
- value: HEADING_KEYS.h2,
- },
- {
- icon: <Heading3Icon />,
- label: 'Heading 3',
- value: HEADING_KEYS.h3,
- },
- {
- icon: <TableIcon />,
- label: 'Table',
- value: TablePlugin.key,
- },
- {
- icon: <FileCodeIcon />,
- label: 'Code',
- value: CodeBlockPlugin.key,
- },
- {
- icon: <QuoteIcon />,
- label: 'Quote',
- value: BlockquotePlugin.key,
- },
- {
- icon: <MinusIcon />,
- label: 'Divider',
- value: HorizontalRulePlugin.key,
- },
- ].map((item) => ({
- ...item,
- onSelect: (editor, value) => {
- insertBlock(editor, value);
- },
- })),
- },
- {
- group: 'Lists',
- items: [
- {
- icon: <ListIcon />,
- label: 'Bulleted list',
- value: ListStyleType.Disc,
- },
- {
- icon: <ListOrderedIcon />,
- label: 'Numbered list',
- value: ListStyleType.Decimal,
- },
- {
- icon: <SquareIcon />,
- label: 'To-do list',
- value: INDENT_LIST_KEYS.todo,
- },
- {
- icon: <ChevronRightIcon />,
- label: 'Toggle list',
- value: TogglePlugin.key,
- },
- ].map((item) => ({
- ...item,
- onSelect: (editor, value) => {
- insertBlock(editor, value);
- },
- })),
- },
- {
- group: 'Media',
- items: [
- {
- icon: <ImageIcon />,
- label: 'Image',
- value: ImagePlugin.key,
- },
- {
- icon: <FilmIcon />,
- label: 'Embed',
- value: MediaEmbedPlugin.key,
- },
- {
- icon: <PenToolIcon />,
- label: 'Excalidraw',
- value: ExcalidrawPlugin.key,
- },
- ].map((item) => ({
- ...item,
- onSelect: (editor, value) => {
- insertBlock(editor, value);
- },
- })),
- },
- {
- group: 'Advanced blocks',
- items: [
- {
- icon: <TableOfContentsIcon />,
- label: 'Table of contents',
- value: TocPlugin.key,
- },
- {
- icon: <Columns3Icon />,
- label: '3 columns',
- value: 'action_three_columns',
- },
- ].map((item) => ({
- ...item,
- onSelect: (editor, value) => {
- insertBlock(editor, value);
- },
- })),
- },
- {
- group: 'Inline',
- items: [
- {
- icon: <Link2Icon />,
- label: 'Link',
- value: LinkPlugin.key,
- },
- {
- focusEditor: true,
- icon: <CalendarIcon />,
- label: 'Date',
- value: DatePlugin.key,
- },
- ].map((item) => ({
- ...item,
- onSelect: (editor, value) => {
- insertInlineElement(editor, value);
- },
- })),
- },
-];
-
-export function InsertDropdownMenu(props: DropdownMenuProps) {
- const editor = useEditorRef();
- const openState = useOpenState();
-
- return (
- <DropdownMenu modal={false} {...openState} {...props}>
- <DropdownMenuTrigger asChild>
- <ToolbarButton pressed={openState.open} tooltip="Insert" isDropdown>
- <PlusIcon />
- </ToolbarButton>
- </DropdownMenuTrigger>
-
- <DropdownMenuContent
- className="flex max-h-[500px] min-w-0 flex-col overflow-y-auto"
- align="start"
- >
- {groups.map(({ group, items: nestedItems }) => (
- <DropdownMenuGroup key={group} label={group}>
- {nestedItems.map(({ icon, label, value, onSelect }) => (
- <DropdownMenuItem
- key={value}
- className="min-w-[180px]"
- onSelect={() => {
- onSelect(editor, value);
- focusEditor(editor);
- }}
- >
- {icon}
- {label}
- </DropdownMenuItem>
- ))}
- </DropdownMenuGroup>
- ))}
- </DropdownMenuContent>
- </DropdownMenu>
- );
-}
diff --git a/apps/web/app/components/plate-ui/kbd-leaf.tsx b/apps/web/app/components/plate-ui/kbd-leaf.tsx
deleted file mode 100644
index e0187c19..00000000
--- a/apps/web/app/components/plate-ui/kbd-leaf.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn, withRef } from '@udecode/cn';
-import { PlateLeaf } from '@udecode/plate-common/react';
-
-export const KbdLeaf = withRef<typeof PlateLeaf>(
- ({ children, className, ...props }, ref) => (
- <PlateLeaf
- ref={ref}
- asChild
- className={cn(
- 'rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-sm shadow-[rgba(255,_255,_255,_0.1)_0px_0.5px_0px_0px_inset,_rgb(248,_249,_250)_0px_1px_5px_0px_inset,_rgb(193,_200,_205)_0px_0px_0px_0.5px,_rgb(193,_200,_205)_0px_2px_1px_-1px,_rgb(193,_200,_205)_0px_1px_0px_0px] dark:shadow-[rgba(255,_255,_255,_0.1)_0px_0.5px_0px_0px_inset,_rgb(26,_29,_30)_0px_1px_5px_0px_inset,_rgb(76,_81,_85)_0px_0px_0px_0.5px,_rgb(76,_81,_85)_0px_2px_1px_-1px,_rgb(76,_81,_85)_0px_1px_0px_0px]',
- className
- )}
- {...props}
- >
- <kbd>{children}</kbd>
- </PlateLeaf>
- )
-);
diff --git a/apps/web/app/components/plate-ui/line-height-dropdown-menu.tsx b/apps/web/app/components/plate-ui/line-height-dropdown-menu.tsx
deleted file mode 100644
index 0472c088..00000000
--- a/apps/web/app/components/plate-ui/line-height-dropdown-menu.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
-
-import {
- useLineHeightDropdownMenu,
- useLineHeightDropdownMenuState,
-} from '@udecode/plate-line-height/react';
-import { WrapText } from 'lucide-react';
-
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuTrigger,
- useOpenState,
-} from './dropdown-menu';
-import { ToolbarButton } from './toolbar';
-
-export function LineHeightDropdownMenu({ ...props }: DropdownMenuProps) {
- const openState = useOpenState();
- const state = useLineHeightDropdownMenuState();
- const { radioGroupProps } = useLineHeightDropdownMenu(state);
-
- return (
- <DropdownMenu modal={false} {...openState} {...props}>
- <DropdownMenuTrigger asChild>
- <ToolbarButton
- pressed={openState.open}
- tooltip="Line height"
- isDropdown
- >
- <WrapText />
- </ToolbarButton>
- </DropdownMenuTrigger>
-
- <DropdownMenuContent className="min-w-0" align="start">
- <DropdownMenuRadioGroup {...radioGroupProps}>
- {state.values.map((_value) => (
- <DropdownMenuRadioItem
- key={_value}
- className="min-w-[180px]"
- value={_value}
- >
- {_value}
- </DropdownMenuRadioItem>
- ))}
- </DropdownMenuRadioGroup>
- </DropdownMenuContent>
- </DropdownMenu>
- );
-}
diff --git a/apps/web/app/components/plate-ui/link-element.tsx b/apps/web/app/components/plate-ui/link-element.tsx
deleted file mode 100644
index 9ca65032..00000000
--- a/apps/web/app/components/plate-ui/link-element.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { TLinkElement } from '@udecode/plate-link';
-
-import { cn, withRef } from '@udecode/cn';
-import { useElement } from '@udecode/plate-common/react';
-import { useLink } from '@udecode/plate-link/react';
-
-import { PlateElement } from './plate-element';
-
-export const LinkElement = withRef<typeof PlateElement>(
- ({ children, className, ...props }, ref) => {
- const element = useElement<TLinkElement>();
- const { props: linkProps } = useLink({ element });
-
- return (
- <PlateElement
- ref={ref}
- as="a"
- className={cn(
- 'font-medium text-primary underline decoration-primary underline-offset-4',
- className
- )}
- {...(linkProps as any)}
- {...props}
- >
- {children}
- </PlateElement>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/link-floating-toolbar.tsx b/apps/web/app/components/plate-ui/link-floating-toolbar.tsx
deleted file mode 100644
index d8e83583..00000000
--- a/apps/web/app/components/plate-ui/link-floating-toolbar.tsx
+++ /dev/null
@@ -1,162 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn } from '@udecode/cn';
-import { useFormInputProps } from '@udecode/plate-common/react';
-import {
- type UseVirtualFloatingOptions,
- flip,
- offset,
-} from '@udecode/plate-floating';
-import {
- type LinkFloatingToolbarState,
- FloatingLinkUrlInput,
- LinkOpenButton,
- useFloatingLinkEdit,
- useFloatingLinkEditState,
- useFloatingLinkInsert,
- useFloatingLinkInsertState,
-} from '@udecode/plate-link/react';
-import { ExternalLink, Link, Text, Unlink } from 'lucide-react';
-
-import { buttonVariants } from './button';
-import { inputVariants } from './input';
-import { popoverVariants } from './popover';
-import { Separator } from './separator';
-
-const floatingOptions: UseVirtualFloatingOptions = {
- middleware: [
- offset(12),
- flip({
- fallbackPlacements: ['bottom-end', 'top-start', 'top-end'],
- padding: 12,
- }),
- ],
- placement: 'bottom-start',
-};
-
-export interface LinkFloatingToolbarProps {
- state?: LinkFloatingToolbarState;
-}
-
-export function LinkFloatingToolbar({ state }: LinkFloatingToolbarProps) {
- const insertState = useFloatingLinkInsertState({
- ...state,
- floatingOptions: {
- ...floatingOptions,
- ...state?.floatingOptions,
- },
- });
- const {
- hidden,
- props: insertProps,
- ref: insertRef,
- textInputProps,
- } = useFloatingLinkInsert(insertState);
-
- const editState = useFloatingLinkEditState({
- ...state,
- floatingOptions: {
- ...floatingOptions,
- ...state?.floatingOptions,
- },
- });
- const {
- editButtonProps,
- props: editProps,
- ref: editRef,
- unlinkButtonProps,
- } = useFloatingLinkEdit(editState);
- const inputProps = useFormInputProps({
- preventDefaultOnEnterKeydown: true,
- });
-
- if (hidden) return null;
-
- const input = (
- <div className="flex w-[330px] flex-col" {...inputProps}>
- <div className="flex items-center">
- <div className="flex items-center pl-2 pr-1 text-muted-foreground">
- <Link className="size-4" />
- </div>
-
- <FloatingLinkUrlInput
- className={inputVariants({ h: 'sm', variant: 'ghost' })}
- placeholder="Paste link"
- data-plate-focus
- />
- </div>
- <Separator className="my-1" />
- <div className="flex items-center">
- <div className="flex items-center pl-2 pr-1 text-muted-foreground">
- <Text className="size-4" />
- </div>
- <input
- className={inputVariants({ h: 'sm', variant: 'ghost' })}
- placeholder="Text to display"
- data-plate-focus
- {...textInputProps}
- />
- </div>
- </div>
- );
-
- const editContent = editState.isEditing ? (
- input
- ) : (
- <div className="box-content flex items-center">
- <button
- className={buttonVariants({ size: 'sm', variant: 'ghost' })}
- type="button"
- {...editButtonProps}
- >
- Edit link
- </button>
-
- <Separator orientation="vertical" />
-
- <LinkOpenButton
- className={buttonVariants({
- size: 'icon',
- variant: 'ghost',
- })}
- >
- <ExternalLink width={18} />
- </LinkOpenButton>
-
- <Separator orientation="vertical" />
-
- <button
- className={buttonVariants({
- size: 'icon',
- variant: 'ghost',
- })}
- type="button"
- {...unlinkButtonProps}
- >
- <Unlink width={18} />
- </button>
- </div>
- );
-
- return (
- <>
- <div
- ref={insertRef}
- className={cn(popoverVariants(), 'w-auto p-1')}
- {...insertProps}
- >
- {input}
- </div>
-
- <div
- ref={editRef}
- className={cn(popoverVariants(), 'w-auto p-1')}
- {...editProps}
- >
- {editContent}
- </div>
- </>
- );
-}
diff --git a/apps/web/app/components/plate-ui/link-toolbar-button.tsx b/apps/web/app/components/plate-ui/link-toolbar-button.tsx
deleted file mode 100644
index bf71f721..00000000
--- a/apps/web/app/components/plate-ui/link-toolbar-button.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { withRef } from '@udecode/cn';
-import {
- useLinkToolbarButton,
- useLinkToolbarButtonState,
-} from '@udecode/plate-link/react';
-import { Link } from 'lucide-react';
-
-import { ToolbarButton } from './toolbar';
-
-export const LinkToolbarButton = withRef<typeof ToolbarButton>((rest, ref) => {
- const state = useLinkToolbarButtonState();
- const { props } = useLinkToolbarButton(state);
-
- return (
- <ToolbarButton
- ref={ref}
- data-plate-focus
- tooltip="Link"
- {...props}
- {...rest}
- >
- <Link />
- </ToolbarButton>
- );
-});
diff --git a/apps/web/app/components/plate-ui/mark-toolbar-button.tsx b/apps/web/app/components/plate-ui/mark-toolbar-button.tsx
deleted file mode 100644
index bb19a5e3..00000000
--- a/apps/web/app/components/plate-ui/mark-toolbar-button.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { withRef } from '@udecode/cn';
-import {
- useMarkToolbarButton,
- useMarkToolbarButtonState,
-} from '@udecode/plate-common/react';
-
-import { ToolbarButton } from './toolbar';
-
-export const MarkToolbarButton = withRef<
- typeof ToolbarButton,
- {
- nodeType: string;
- clear?: string[] | string;
- }
->(({ clear, nodeType, ...rest }, ref) => {
- const state = useMarkToolbarButtonState({ clear, nodeType });
- const { props } = useMarkToolbarButton(state);
-
- return <ToolbarButton ref={ref} {...props} {...rest} />;
-});
diff --git a/apps/web/app/components/plate-ui/media-embed-element.tsx b/apps/web/app/components/plate-ui/media-embed-element.tsx
deleted file mode 100644
index 83310a35..00000000
--- a/apps/web/app/components/plate-ui/media-embed-element.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-"use client";
-
-import React from "react";
-import LiteYouTubeEmbed from "react-lite-youtube-embed";
-import * as ReactTweet from "react-tweet";
-
-const { useTweet } = ReactTweet;
-
-import { CustomTwitterComp } from "../twitter/render-tweet";
-import { Caption, CaptionTextarea } from "./caption";
-import { MediaPopover } from "./media-popover";
-import { PlateElement } from "./plate-element";
-import { Resizable, ResizeHandle, mediaResizeHandleVariants } from "./resizable";
-
-import { cn, withRef } from "@udecode/cn";
-import { withHOC } from "@udecode/plate-common/react";
-import { parseTwitterUrl, parseVideoUrl } from "@udecode/plate-media";
-import { MediaEmbedPlugin, useMediaState } from "@udecode/plate-media/react";
-import { ResizableProvider, useResizableStore } from "@udecode/plate-resizable";
-
-export const MediaEmbedElement = withHOC(
- ResizableProvider,
- withRef<typeof PlateElement>(({ children, className, ...props }, ref) => {
- const {
- align = "center",
- embed,
- focused,
- isTweet,
- isVideo,
- isYoutube,
- readOnly,
- selected,
- } = useMediaState({
- urlParsers: [parseTwitterUrl, parseVideoUrl],
- });
- const width = useResizableStore().get.width();
- const provider = embed?.provider;
- const { data, error } = useTweet(embed!.id!);
-
- return (
- <MediaPopover plugin={MediaEmbedPlugin}>
- <PlateElement ref={ref} className={cn("relative py-2.5", className)} {...props}>
- <figure className="group relative m-0 w-full" contentEditable={false}>
- <Resizable
- align={align}
- options={{
- align,
- maxWidth: isTweet ? 550 : "100%",
- minWidth: isTweet ? 300 : 100,
- }}
- >
- <ResizeHandle
- className={mediaResizeHandleVariants({ direction: "left" })}
- options={{ direction: "left" }}
- />
-
- {isVideo ? (
- isYoutube ? (
- <LiteYouTubeEmbed
- id={embed!.id!}
- title="youtube"
- wrapperClass={cn(
- "rounded-sm",
- focused && selected && "ring-2 ring-ring ring-offset-2",
- "relative block cursor-pointer bg-black bg-cover bg-center [contain:content]",
- "[&.lyt-activated]:before:absolute [&.lyt-activated]:before:top-0 [&.lyt-activated]:before:h-[60px] [&.lyt-activated]:before:w-full [&.lyt-activated]:before:bg-top [&.lyt-activated]:before:bg-repeat-x [&.lyt-activated]:before:pb-[50px] [&.lyt-activated]:before:[transition:all_0.2s_cubic-bezier(0,_0,_0.2,_1)]",
- "[&.lyt-activated]:before:bg-[url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==)]",
- 'after:block after:pb-[var(--aspect-ratio)] after:content-[""]',
- "[&_>_iframe]:absolute [&_>_iframe]:left-0 [&_>_iframe]:top-0 [&_>_iframe]:size-full",
- "[&_>_.lty-playbtn]:z-[1] [&_>_.lty-playbtn]:h-[46px] [&_>_.lty-playbtn]:w-[70px] [&_>_.lty-playbtn]:rounded-[14%] [&_>_.lty-playbtn]:bg-[#212121] [&_>_.lty-playbtn]:opacity-80 [&_>_.lty-playbtn]:[transition:all_0.2s_cubic-bezier(0,_0,_0.2,_1)]",
- "[&:hover_>_.lty-playbtn]:bg-[red] [&:hover_>_.lty-playbtn]:opacity-100",
- '[&_>_.lty-playbtn]:before:border-y-[11px] [&_>_.lty-playbtn]:before:border-l-[19px] [&_>_.lty-playbtn]:before:border-r-0 [&_>_.lty-playbtn]:before:border-[transparent_transparent_transparent_#fff] [&_>_.lty-playbtn]:before:content-[""]',
- "[&_>_.lty-playbtn]:absolute [&_>_.lty-playbtn]:left-1/2 [&_>_.lty-playbtn]:top-1/2 [&_>_.lty-playbtn]:[transform:translate3d(-50%,-50%,0)]",
- "[&_>_.lty-playbtn]:before:absolute [&_>_.lty-playbtn]:before:left-1/2 [&_>_.lty-playbtn]:before:top-1/2 [&_>_.lty-playbtn]:before:[transform:translate3d(-50%,-50%,0)]",
- "[&.lyt-activated]:cursor-[unset]",
- "[&.lyt-activated]:before:pointer-events-none [&.lyt-activated]:before:opacity-0",
- "[&.lyt-activated_>_.lty-playbtn]:pointer-events-none [&.lyt-activated_>_.lty-playbtn]:!opacity-0",
- )}
- />
- ) : (
- <div
- className={cn(
- provider === "vimeo" && "pb-[75%]",
- provider === "youku" && "pb-[56.25%]",
- provider === "dailymotion" && "pb-[56.0417%]",
- provider === "coub" && "pb-[51.25%]",
- )}
- >
- <iframe
- className={cn(
- "absolute left-0 top-0 size-full rounded-sm",
- isVideo && "border-0",
- focused && selected && "ring-2 ring-ring ring-offset-2",
- )}
- title="embed"
- src={embed!.url}
- allowFullScreen
- />
- </div>
- )
- ) : null}
-
- {isTweet && (
- <div
- className={cn(
- "[&_.react-tweet-theme]:my-0",
- !readOnly &&
- selected &&
- "[&_.react-tweet-theme]:ring-2 [&_.react-tweet-theme]:ring-ring [&_.react-tweet-theme]:ring-offset-2",
- )}
- >
- {data ? <CustomTwitterComp tweet={data} /> : <div>Error parsing tweet</div>}
- </div>
- )}
-
- <ResizeHandle
- className={mediaResizeHandleVariants({ direction: "right" })}
- options={{ direction: "right" }}
- />
- </Resizable>
-
- <Caption style={{ width }} align={align}>
- <CaptionTextarea placeholder="Write a caption..." />
- </Caption>
- </figure>
-
- {children}
- </PlateElement>
- </MediaPopover>
- );
- }),
-);
diff --git a/apps/web/app/components/plate-ui/media-popover.tsx b/apps/web/app/components/plate-ui/media-popover.tsx
deleted file mode 100644
index e8c9c052..00000000
--- a/apps/web/app/components/plate-ui/media-popover.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-'use client';
-
-import React, { useEffect } from 'react';
-
-import {
- type WithRequiredKey,
- isSelectionExpanded,
-} from '@udecode/plate-common';
-import {
- useEditorSelector,
- useElement,
- useRemoveNodeButton,
-} from '@udecode/plate-common/react';
-import {
- FloatingMedia as FloatingMediaPrimitive,
- floatingMediaActions,
- useFloatingMediaSelectors,
-} from '@udecode/plate-media/react';
-import { Link, Trash2Icon } from 'lucide-react';
-import { useReadOnly, useSelected } from 'slate-react';
-
-import { Button, buttonVariants } from './button';
-import { CaptionButton } from './caption';
-import { inputVariants } from './input';
-import { Popover, PopoverAnchor, PopoverContent } from './popover';
-import { Separator } from './separator';
-
-export interface MediaPopoverProps {
- children: React.ReactNode;
- plugin: WithRequiredKey;
-}
-
-export function MediaPopover({ children, plugin }: MediaPopoverProps) {
- const readOnly = useReadOnly();
- const selected = useSelected();
-
- const selectionCollapsed = useEditorSelector(
- (editor) => !isSelectionExpanded(editor),
- []
- );
- const isOpen = !readOnly && selected && selectionCollapsed;
- const isEditing = useFloatingMediaSelectors().isEditing();
-
- useEffect(() => {
- if (!isOpen && isEditing) {
- floatingMediaActions.isEditing(false);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isOpen]);
-
- const element = useElement();
- const { props: buttonProps } = useRemoveNodeButton({ element });
-
- if (readOnly) return <>{children}</>;
-
- return (
- <Popover open={isOpen} modal={false}>
- <PopoverAnchor>{children}</PopoverAnchor>
-
- <PopoverContent
- className="w-auto p-1"
- onOpenAutoFocus={(e) => e.preventDefault()}
- >
- {isEditing ? (
- <div className="flex w-[330px] flex-col">
- <div className="flex items-center">
- <div className="flex items-center pl-2 pr-1 text-muted-foreground">
- <Link className="size-4" />
- </div>
-
- <FloatingMediaPrimitive.UrlInput
- className={inputVariants({ h: 'sm', variant: 'ghost' })}
- placeholder="Paste the embed link..."
- options={{ plugin }}
- />
- </div>
- </div>
- ) : (
- <div className="box-content flex items-center">
- <FloatingMediaPrimitive.EditButton
- className={buttonVariants({ size: 'sm', variant: 'ghost' })}
- >
- Edit link
- </FloatingMediaPrimitive.EditButton>
-
- <CaptionButton variant="ghost">Caption</CaptionButton>
-
- <Separator orientation="vertical" className="mx-1 h-6" />
-
- <Button size="icon" variant="ghost" {...buttonProps}>
- <Trash2Icon />
- </Button>
- </div>
- )}
- </PopoverContent>
- </Popover>
- );
-}
diff --git a/apps/web/app/components/plate-ui/media-toolbar-button.tsx b/apps/web/app/components/plate-ui/media-toolbar-button.tsx
deleted file mode 100644
index af5a051d..00000000
--- a/apps/web/app/components/plate-ui/media-toolbar-button.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { withRef } from '@udecode/cn';
-import {
- type ImagePlugin,
- type MediaEmbedPlugin,
- useMediaToolbarButton,
-} from '@udecode/plate-media/react';
-import { ImageIcon } from 'lucide-react';
-
-import { ToolbarButton } from './toolbar';
-
-export const MediaToolbarButton = withRef<
- typeof ToolbarButton,
- {
- nodeType?: typeof ImagePlugin.key | typeof MediaEmbedPlugin.key;
- }
->(({ nodeType, ...rest }, ref) => {
- const { props } = useMediaToolbarButton({ nodeType });
-
- return (
- <ToolbarButton ref={ref} {...props} {...rest}>
- <ImageIcon />
- </ToolbarButton>
- );
-});
diff --git a/apps/web/app/components/plate-ui/mention-element.tsx b/apps/web/app/components/plate-ui/mention-element.tsx
deleted file mode 100644
index 3456861c..00000000
--- a/apps/web/app/components/plate-ui/mention-element.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { TMentionElement } from '@udecode/plate-mention';
-
-import { cn, withRef } from '@udecode/cn';
-import { IS_APPLE, getHandler } from '@udecode/plate-common';
-import { useElement } from '@udecode/plate-common/react';
-import { useFocused, useSelected } from 'slate-react';
-
-import { useMounted } from '~/lib/hooks/use-mounted';
-
-import { PlateElement } from './plate-element';
-
-export const MentionElement = withRef<
- typeof PlateElement,
- {
- prefix?: string;
- renderLabel?: (mentionable: TMentionElement) => string;
- onClick?: (mentionNode: any) => void;
- }
->(({ children, className, prefix, renderLabel, onClick, ...props }, ref) => {
- const element = useElement<TMentionElement>();
- const selected = useSelected();
- const focused = useFocused();
- const mounted = useMounted();
-
- return (
- <PlateElement
- ref={ref}
- className={cn(
- 'inline-block cursor-pointer rounded-md bg-muted px-1.5 py-0.5 align-baseline text-sm font-medium',
- selected && focused && 'ring-2 ring-ring',
- element.children[0].bold === true && 'font-bold',
- element.children[0].italic === true && 'italic',
- element.children[0].underline === true && 'underline',
- className
- )}
- onClick={getHandler(onClick, element)}
- data-slate-value={element.value}
- contentEditable={false}
- draggable
- {...props}
- >
- {mounted && IS_APPLE ? (
- // Mac OS IME https://github.com/ianstormtaylor/slate/issues/3490
- <React.Fragment>
- {children}
- {prefix}
- {renderLabel ? renderLabel(element) : element.value}
- </React.Fragment>
- ) : (
- // Others like Android https://github.com/ianstormtaylor/slate/pull/5360
- <React.Fragment>
- {prefix}
- {renderLabel ? renderLabel(element) : element.value}
- {children}
- </React.Fragment>
- )}
- </PlateElement>
- );
-});
diff --git a/apps/web/app/components/plate-ui/mention-input-element.tsx b/apps/web/app/components/plate-ui/mention-input-element.tsx
deleted file mode 100644
index e9bb671d..00000000
--- a/apps/web/app/components/plate-ui/mention-input-element.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-'use client';
-
-import React, { useState } from 'react';
-
-import { cn, withRef } from '@udecode/cn';
-import { getMentionOnSelectItem } from '@udecode/plate-mention';
-
-import {
- InlineCombobox,
- InlineComboboxContent,
- InlineComboboxEmpty,
- InlineComboboxGroup,
- InlineComboboxInput,
- InlineComboboxItem,
-} from './inline-combobox';
-import { PlateElement } from './plate-element';
-
-const onSelectItem = getMentionOnSelectItem();
-
-export const MentionInputElement = withRef<typeof PlateElement>(
- ({ className, ...props }, ref) => {
- const { children, editor, element } = props;
- const [search, setSearch] = useState('');
-
- return (
- <PlateElement
- ref={ref}
- as="span"
- data-slate-value={element.value}
- {...props}
- >
- <InlineCombobox
- value={search}
- element={element}
- setValue={setSearch}
- showTrigger={false}
- trigger="@"
- >
- <span
- className={cn(
- 'inline-block rounded-md bg-muted px-1.5 py-0.5 align-baseline text-sm ring-ring focus-within:ring-2',
- className
- )}
- >
- <InlineComboboxInput />
- </span>
-
- <InlineComboboxContent className="my-1.5">
- <InlineComboboxEmpty>No results</InlineComboboxEmpty>
-
- <InlineComboboxGroup>
- {MENTIONABLES.map((item) => (
- <InlineComboboxItem
- key={item.key}
- value={item.text}
- onClick={() => onSelectItem(editor, item, search)}
- >
- {item.text}
- </InlineComboboxItem>
- ))}
- </InlineComboboxGroup>
- </InlineComboboxContent>
- </InlineCombobox>
-
- {children}
- </PlateElement>
- );
- }
-);
-
-export const MENTIONABLES = [
- { key: '0', text: 'Aayla Secura' },
- { key: '1', text: 'Adi Gallia' },
- {
- key: '2',
- text: 'Admiral Dodd Rancit',
- },
- {
- key: '3',
- text: 'Admiral Firmus Piett',
- },
- {
- key: '4',
- text: 'Admiral Gial Ackbar',
- },
- { key: '5', text: 'Admiral Ozzel' },
- { key: '6', text: 'Admiral Raddus' },
- {
- key: '7',
- text: 'Admiral Terrinald Screed',
- },
- { key: '8', text: 'Admiral Trench' },
- {
- key: '9',
- text: 'Admiral U.O. Statura',
- },
- { key: '10', text: 'Agen Kolar' },
- { key: '11', text: 'Agent Kallus' },
- {
- key: '12',
- text: 'Aiolin and Morit Astarte',
- },
- { key: '13', text: 'Aks Moe' },
- { key: '14', text: 'Almec' },
- { key: '15', text: 'Alton Kastle' },
- { key: '16', text: 'Amee' },
- { key: '17', text: 'AP-5' },
- { key: '18', text: 'Armitage Hux' },
- { key: '19', text: 'Artoo' },
- { key: '20', text: 'Arvel Crynyd' },
- { key: '21', text: 'Asajj Ventress' },
- { key: '22', text: 'Aurra Sing' },
- { key: '23', text: 'AZI-3' },
- { key: '24', text: 'Bala-Tik' },
- { key: '25', text: 'Barada' },
- { key: '26', text: 'Bargwill Tomder' },
- { key: '27', text: 'Baron Papanoida' },
- { key: '28', text: 'Barriss Offee' },
- { key: '29', text: 'Baze Malbus' },
- { key: '30', text: 'Bazine Netal' },
- { key: '31', text: 'BB-8' },
- { key: '32', text: 'BB-9E' },
- { key: '33', text: 'Ben Quadinaros' },
- { key: '34', text: 'Berch Teller' },
- { key: '35', text: 'Beru Lars' },
- { key: '36', text: 'Bib Fortuna' },
- {
- key: '37',
- text: 'Biggs Darklighter',
- },
- { key: '38', text: 'Black Krrsantan' },
- { key: '39', text: 'Bo-Katan Kryze' },
- { key: '40', text: 'Boba Fett' },
- { key: '41', text: 'Bobbajo' },
- { key: '42', text: 'Bodhi Rook' },
- { key: '43', text: 'Borvo the Hutt' },
- { key: '44', text: 'Boss Nass' },
- { key: '45', text: 'Bossk' },
- {
- key: '46',
- text: 'Breha Antilles-Organa',
- },
- { key: '47', text: 'Bren Derlin' },
- { key: '48', text: 'Brendol Hux' },
- { key: '49', text: 'BT-1' },
-];
diff --git a/apps/web/app/components/plate-ui/mode-dropdown-menu.tsx b/apps/web/app/components/plate-ui/mode-dropdown-menu.tsx
deleted file mode 100644
index dcac579d..00000000
--- a/apps/web/app/components/plate-ui/mode-dropdown-menu.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
-
-import {
- focusEditor,
- useEditorReadOnly,
- useEditorRef,
- usePlateStore,
-} from '@udecode/plate-common/react';
-import { Eye, Pen } from 'lucide-react';
-
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuTrigger,
- useOpenState,
-} from './dropdown-menu';
-import { ToolbarButton } from './toolbar';
-
-export function ModeDropdownMenu(props: DropdownMenuProps) {
- const editor = useEditorRef();
- const setReadOnly = usePlateStore().set.readOnly();
- const readOnly = useEditorReadOnly();
- const openState = useOpenState();
-
- let value = 'editing';
-
- if (readOnly) value = 'viewing';
-
- const item: any = {
- editing: (
- <>
- <Pen />
- <span className="hidden lg:inline">Editing</span>
- </>
- ),
- viewing: (
- <>
- <Eye />
- <span className="hidden lg:inline">Viewing</span>
- </>
- ),
- };
-
- return (
- <DropdownMenu modal={false} {...openState} {...props}>
- <DropdownMenuTrigger asChild>
- <ToolbarButton
- pressed={openState.open}
- tooltip="Editing mode"
- isDropdown
- >
- {item[value]}
- </ToolbarButton>
- </DropdownMenuTrigger>
-
- <DropdownMenuContent className="min-w-[180px]" align="start">
- <DropdownMenuRadioGroup
- value={value}
- onValueChange={(newValue) => {
- if (newValue !== 'viewing') {
- setReadOnly(false);
- }
- if (newValue === 'viewing') {
- setReadOnly(true);
-
- return;
- }
- if (newValue === 'editing') {
- focusEditor(editor);
-
- return;
- }
- }}
- >
- <DropdownMenuRadioItem value="editing">
- {item.editing}
- </DropdownMenuRadioItem>
-
- <DropdownMenuRadioItem value="viewing">
- {item.viewing}
- </DropdownMenuRadioItem>
- </DropdownMenuRadioGroup>
- </DropdownMenuContent>
- </DropdownMenu>
- );
-}
diff --git a/apps/web/app/components/plate-ui/more-dropdown-menu.tsx b/apps/web/app/components/plate-ui/more-dropdown-menu.tsx
deleted file mode 100644
index 2407b02e..00000000
--- a/apps/web/app/components/plate-ui/more-dropdown-menu.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-'use client';
-import React from 'react';
-
-import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
-
-import {
- SubscriptPlugin,
- SuperscriptPlugin,
-} from '@udecode/plate-basic-marks/react';
-import { collapseSelection } from '@udecode/plate-common';
-import { focusEditor, useEditorRef } from '@udecode/plate-common/react';
-import { HighlightPlugin } from '@udecode/plate-highlight/react';
-import { KbdPlugin } from '@udecode/plate-kbd/react';
-import {
- HighlighterIcon,
- KeyboardIcon,
- MoreHorizontalIcon,
- SubscriptIcon,
- SuperscriptIcon,
-} from 'lucide-react';
-
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuTrigger,
- useOpenState,
-} from './dropdown-menu';
-import { ToolbarButton } from './toolbar';
-
-export function MoreDropdownMenu(props: DropdownMenuProps) {
- const editor = useEditorRef();
- const openState = useOpenState();
-
- return (
- <DropdownMenu modal={false} {...openState} {...props}>
- <DropdownMenuTrigger asChild>
- <ToolbarButton pressed={openState.open} tooltip="Insert">
- <MoreHorizontalIcon />
- </ToolbarButton>
- </DropdownMenuTrigger>
-
- <DropdownMenuContent
- className="ignore-click-outside/toolbar flex max-h-[500px] min-w-[180px] flex-col overflow-y-auto"
- align="start"
- >
- <DropdownMenuGroup>
- <DropdownMenuItem
- onSelect={() => {
- editor.tf.toggle.mark({ key: HighlightPlugin.key });
- collapseSelection(editor, { edge: 'end' });
- focusEditor(editor);
- }}
- >
- <HighlighterIcon />
- Highlight
- </DropdownMenuItem>
-
- <DropdownMenuItem
- onSelect={() => {
- editor.tf.toggle.mark({ key: KbdPlugin.key });
- collapseSelection(editor, { edge: 'end' });
- focusEditor(editor);
- }}
- >
- <KeyboardIcon />
- Keyboard input
- </DropdownMenuItem>
-
- <DropdownMenuItem
- onSelect={() => {
- editor.tf.toggle.mark({
- key: SuperscriptPlugin.key,
- clear: [SubscriptPlugin.key, SuperscriptPlugin.key],
- });
- focusEditor(editor);
- }}
- >
- <SuperscriptIcon />
- Superscript
- {/* (⌘+,) */}
- </DropdownMenuItem>
- <DropdownMenuItem
- onSelect={() => {
- editor.tf.toggle.mark({
- key: SubscriptPlugin.key,
- clear: [SuperscriptPlugin.key, SubscriptPlugin.key],
- });
- focusEditor(editor);
- }}
- >
- <SubscriptIcon />
- Subscript
- {/* (⌘+.) */}
- </DropdownMenuItem>
- </DropdownMenuGroup>
- </DropdownMenuContent>
- </DropdownMenu>
- );
-}
diff --git a/apps/web/app/components/plate-ui/outdent-toolbar-button.tsx b/apps/web/app/components/plate-ui/outdent-toolbar-button.tsx
deleted file mode 100644
index 535b3a4b..00000000
--- a/apps/web/app/components/plate-ui/outdent-toolbar-button.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { withRef } from '@udecode/cn';
-import { useOutdentButton } from '@udecode/plate-indent/react';
-import { Outdent } from 'lucide-react';
-
-import { ToolbarButton } from './toolbar';
-
-export const OutdentToolbarButton = withRef<typeof ToolbarButton>(
- (rest, ref) => {
- const { props } = useOutdentButton();
-
- return (
- <ToolbarButton ref={ref} tooltip="Outdent" {...props} {...rest}>
- <Outdent />
- </ToolbarButton>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/paragraph-element.tsx b/apps/web/app/components/plate-ui/paragraph-element.tsx
deleted file mode 100644
index a9388efb..00000000
--- a/apps/web/app/components/plate-ui/paragraph-element.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn } from '@udecode/cn';
-import { withRef } from '@udecode/plate-common/react';
-
-import { PlateElement } from './plate-element';
-
-export const ParagraphElement = withRef<typeof PlateElement>(
- ({ children, className, ...props }, ref) => {
- return (
- <PlateElement
- ref={ref}
- className={cn('m-0 px-0 py-1', className)}
- {...props}
- >
- {children}
- </PlateElement>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/placeholder.tsx b/apps/web/app/components/plate-ui/placeholder.tsx
deleted file mode 100644
index e55ccf04..00000000
--- a/apps/web/app/components/plate-ui/placeholder.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn } from '@udecode/cn';
-import { ParagraphPlugin } from '@udecode/plate-common/react';
-import {
- type PlaceholderProps,
- createNodeHOC,
- createNodesHOC,
- usePlaceholderState,
-} from '@udecode/plate-common/react';
-import { HEADING_KEYS } from '@udecode/plate-heading';
-
-export const Placeholder = (props: PlaceholderProps) => {
- const { children, nodeProps, placeholder } = props;
-
- const { enabled } = usePlaceholderState(props);
-
- return React.Children.map(children, (child) => {
- return React.cloneElement(child, {
- className: child.props.className,
- nodeProps: {
- ...nodeProps,
- className: cn(
- enabled &&
- 'before:absolute before:cursor-text before:opacity-30 before:content-[attr(placeholder)]'
- ),
- placeholder,
- },
- });
- });
-};
-
-export const withPlaceholder = createNodeHOC(Placeholder);
-
-export const withPlaceholdersPrimitive = createNodesHOC(Placeholder);
-
-export const withPlaceholders = (components: any) =>
- withPlaceholdersPrimitive(components, [
- {
- key: ParagraphPlugin.key,
- hideOnBlur: true,
- placeholder: 'Type a paragraph',
- query: {
- maxLevel: 1,
- },
- },
- {
- key: HEADING_KEYS.h1,
- hideOnBlur: false,
- placeholder: 'Untitled',
- },
- ]);
diff --git a/apps/web/app/components/plate-ui/plate-element.tsx b/apps/web/app/components/plate-ui/plate-element.tsx
deleted file mode 100644
index d3db18f8..00000000
--- a/apps/web/app/components/plate-ui/plate-element.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { PlateElementProps } from '@udecode/plate-common/react';
-
-import { cn } from '@udecode/cn';
-import { PlateElement as PlateElementPrimitive } from '@udecode/plate-common/react';
-
-import { BlockSelection } from './block-selection';
-
-export const PlateElement = React.forwardRef<HTMLDivElement, PlateElementProps>(
- ({ children, className, ...props }: PlateElementProps, ref) => {
- return (
- <PlateElementPrimitive
- ref={ref}
- className={cn('relative', className)}
- {...props}
- >
- {children}
-
- {className?.includes('slate-selectable') && <BlockSelection />}
- </PlateElementPrimitive>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/popover.tsx b/apps/web/app/components/plate-ui/popover.tsx
deleted file mode 100644
index b19a6f9d..00000000
--- a/apps/web/app/components/plate-ui/popover.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-'use client';
-
-import * as React from 'react';
-
-import * as PopoverPrimitive from '@radix-ui/react-popover';
-import { cn, withRef } from '@udecode/cn';
-import { cva } from 'class-variance-authority';
-
-export const Popover = PopoverPrimitive.Root;
-
-export const PopoverTrigger = PopoverPrimitive.Trigger;
-
-export const PopoverAnchor = PopoverPrimitive.Anchor;
-
-export const popoverVariants = cva(
- 'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 print:hidden'
-);
-
-export const PopoverContent = withRef<typeof PopoverPrimitive.Content>(
- ({ align = 'center', className, sideOffset = 4, ...props }, ref) => (
- <PopoverPrimitive.Portal>
- <PopoverPrimitive.Content
- ref={ref}
- className={cn(popoverVariants(), className)}
- align={align}
- sideOffset={sideOffset}
- {...props}
- />
- </PopoverPrimitive.Portal>
- )
-);
diff --git a/apps/web/app/components/plate-ui/resizable.tsx b/apps/web/app/components/plate-ui/resizable.tsx
deleted file mode 100644
index f78842a2..00000000
--- a/apps/web/app/components/plate-ui/resizable.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn, withRef, withVariants } from '@udecode/cn';
-import {
- Resizable as ResizablePrimitive,
- ResizeHandle as ResizeHandlePrimitive,
-} from '@udecode/plate-resizable';
-import { cva } from 'class-variance-authority';
-
-export const mediaResizeHandleVariants = cva(
- cn(
- 'top-0 flex w-6 select-none flex-col justify-center',
- "after:flex after:h-16 after:w-[3px] after:rounded-[6px] after:bg-ring after:opacity-0 after:content-['_'] group-hover:after:opacity-100"
- ),
- {
- variants: {
- direction: {
- left: '-left-3 -ml-3 pl-3',
- right: '-right-3 -mr-3 items-end pr-3',
- },
- },
- }
-);
-
-const resizeHandleVariants = cva(cn('absolute z-40'), {
- variants: {
- direction: {
- bottom: 'w-full cursor-row-resize',
- left: 'h-full cursor-col-resize',
- right: 'h-full cursor-col-resize',
- top: 'w-full cursor-row-resize',
- },
- },
-});
-
-const ResizeHandleVariants = withVariants(
- ResizeHandlePrimitive,
- resizeHandleVariants,
- ['direction']
-);
-
-export const ResizeHandle = withRef<typeof ResizeHandlePrimitive>(
- (props, ref) => (
- <ResizeHandleVariants
- ref={ref}
- direction={props.options?.direction}
- {...props}
- />
- )
-);
-
-const resizableVariants = cva('', {
- variants: {
- align: {
- center: 'mx-auto',
- left: 'mr-auto',
- right: 'ml-auto',
- },
- },
-});
-
-export const Resizable = withVariants(ResizablePrimitive, resizableVariants, [
- 'align',
-]);
diff --git a/apps/web/app/components/plate-ui/separator.tsx b/apps/web/app/components/plate-ui/separator.tsx
deleted file mode 100644
index 779eeaad..00000000
--- a/apps/web/app/components/plate-ui/separator.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-'use client';
-
-import * as SeparatorPrimitive from '@radix-ui/react-separator';
-import { withProps, withVariants } from '@udecode/cn';
-import { cva } from 'class-variance-authority';
-
-const separatorVariants = cva('shrink-0 bg-border', {
- defaultVariants: {
- orientation: 'horizontal',
- },
- variants: {
- orientation: {
- horizontal: 'h-px w-full',
- vertical: 'h-full w-px',
- },
- },
-});
-
-export const Separator = withVariants(
- withProps(SeparatorPrimitive.Root, {
- decorative: true,
- orientation: 'horizontal',
- }),
- separatorVariants
-);
diff --git a/apps/web/app/components/plate-ui/slash-input-element.tsx b/apps/web/app/components/plate-ui/slash-input-element.tsx
deleted file mode 100644
index 4c9d1a3d..00000000
--- a/apps/web/app/components/plate-ui/slash-input-element.tsx
+++ /dev/null
@@ -1,242 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { withRef } from '@udecode/cn';
-import { AIChatPlugin } from '@udecode/plate-ai/react';
-import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
-import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
-import { type PlateEditor, ParagraphPlugin } from '@udecode/plate-common/react';
-import { DatePlugin } from '@udecode/plate-date/react';
-import { HEADING_KEYS } from '@udecode/plate-heading';
-import { TocPlugin } from '@udecode/plate-heading/react';
-import { INDENT_LIST_KEYS, ListStyleType } from '@udecode/plate-indent-list';
-import { TablePlugin } from '@udecode/plate-table/react';
-import { TogglePlugin } from '@udecode/plate-toggle/react';
-import {
- CalendarIcon,
- ChevronRightIcon,
- Code2,
- Columns3Icon,
- Heading1Icon,
- Heading2Icon,
- Heading3Icon,
- ListIcon,
- ListOrdered,
- PilcrowIcon,
- Quote,
- SparklesIcon,
- Square,
- Table,
- TableOfContentsIcon,
-} from 'lucide-react';
-
-import {
- insertBlock,
- insertInlineElement,
-} from '~/components/editor/transforms';
-
-import {
- InlineCombobox,
- InlineComboboxContent,
- InlineComboboxEmpty,
- InlineComboboxGroup,
- InlineComboboxGroupLabel,
- InlineComboboxInput,
- InlineComboboxItem,
-} from './inline-combobox';
-import { PlateElement } from './plate-element';
-
-type Group = {
- group: string;
- items: Item[];
-};
-
-interface Item {
- icon: React.ReactNode;
-
- onSelect: (editor: PlateEditor, value: string) => void;
-
- value: string;
- className?: string;
- focusEditor?: boolean;
- keywords?: string[];
- label?: string;
-}
-
-const groups: Group[] = [
- {
- group: 'AI',
- items: [
- {
- focusEditor: false,
- icon: <SparklesIcon />,
- value: 'AI',
- onSelect: (editor) => {
- editor.getApi(AIChatPlugin).aiChat.show();
- },
- },
- ],
- },
- {
- group: 'Basic blocks',
- items: [
- {
- icon: <PilcrowIcon />,
- keywords: ['paragraph'],
- label: 'Text',
- value: ParagraphPlugin.key,
- },
- {
- icon: <Heading1Icon />,
- keywords: ['title', 'h1'],
- label: 'Heading 1',
- value: HEADING_KEYS.h1,
- },
- {
- icon: <Heading2Icon />,
- keywords: ['subtitle', 'h2'],
- label: 'Heading 2',
- value: HEADING_KEYS.h2,
- },
- {
- icon: <Heading3Icon />,
- keywords: ['subtitle', 'h3'],
- label: 'Heading 3',
- value: HEADING_KEYS.h3,
- },
- {
- icon: <ListIcon />,
- keywords: ['unordered', 'ul', '-'],
- label: 'Bulleted list',
- value: ListStyleType.Disc,
- },
- {
- icon: <ListOrdered />,
- keywords: ['ordered', 'ol', '1'],
- label: 'Numbered list',
- value: ListStyleType.Decimal,
- },
- {
- icon: <Square />,
- keywords: ['checklist', 'task', 'checkbox', '[]'],
- label: 'To-do list',
- value: INDENT_LIST_KEYS.todo,
- },
- {
- icon: <ChevronRightIcon />,
- keywords: ['collapsible', 'expandable'],
- label: 'Toggle',
- value: TogglePlugin.key,
- },
- {
- icon: <Code2 />,
- keywords: ['```'],
- label: 'Code Block',
- value: CodeBlockPlugin.key,
- },
- {
- icon: <Table />,
- label: 'Table',
- value: TablePlugin.key,
- },
- {
- icon: <Quote />,
- keywords: ['citation', 'blockquote', 'quote', '>'],
- label: 'Blockquote',
- value: BlockquotePlugin.key,
- },
- ].map((item) => ({
- ...item,
- onSelect: (editor, value) => {
- insertBlock(editor, value);
- },
- })),
- },
- {
- group: 'Advanced blocks',
- items: [
- {
- icon: <TableOfContentsIcon />,
- keywords: ['toc'],
- label: 'Table of contents',
- value: TocPlugin.key,
- },
- {
- icon: <Columns3Icon />,
- label: '3 columns',
- value: 'action_three_columns',
- },
- ].map((item) => ({
- ...item,
- onSelect: (editor, value) => {
- insertBlock(editor, value);
- },
- })),
- },
- {
- group: 'Inline',
- items: [
- {
- focusEditor: true,
- icon: <CalendarIcon />,
- keywords: ['time'],
- label: 'Date',
- value: DatePlugin.key,
- },
- ].map((item) => ({
- ...item,
- onSelect: (editor, value) => {
- insertInlineElement(editor, value);
- },
- })),
- },
-];
-
-export const SlashInputElement = withRef<typeof PlateElement>(
- ({ className, ...props }, ref) => {
- const { children, editor, element } = props;
-
- return (
- <PlateElement
- ref={ref}
- as="span"
- data-slate-value={element.value}
- {...props}
- >
- <InlineCombobox element={element} trigger="/">
- <InlineComboboxInput />
-
- <InlineComboboxContent>
- <InlineComboboxEmpty>No results</InlineComboboxEmpty>
-
- {groups.map(({ group, items }) => (
- <InlineComboboxGroup key={group}>
- <InlineComboboxGroupLabel>{group}</InlineComboboxGroupLabel>
-
- {items.map(
- ({ focusEditor, icon, keywords, label, value, onSelect }) => (
- <InlineComboboxItem
- key={value}
- value={value}
- onClick={() => onSelect(editor, value)}
- label={label}
- focusEditor={focusEditor}
- group={group}
- keywords={keywords}
- >
- <div className="mr-2 text-muted-foreground">{icon}</div>
- {label ?? value}
- </InlineComboboxItem>
- )
- )}
- </InlineComboboxGroup>
- ))}
- </InlineComboboxContent>
- </InlineCombobox>
-
- {children}
- </PlateElement>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/table-cell-element.tsx b/apps/web/app/components/plate-ui/table-cell-element.tsx
deleted file mode 100644
index b4f247d3..00000000
--- a/apps/web/app/components/plate-ui/table-cell-element.tsx
+++ /dev/null
@@ -1,152 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn, withProps, withRef } from '@udecode/cn';
-import { useElement } from '@udecode/plate-common/react';
-import { useBlockSelected } from '@udecode/plate-selection/react';
-import {
- TableRowPlugin,
- useTableCellElement,
- useTableCellElementResizable,
- useTableCellElementResizableState,
- useTableCellElementState,
-} from '@udecode/plate-table/react';
-
-import { blockSelectionVariants } from './block-selection';
-import { PlateElement } from './plate-element';
-import { ResizeHandle } from './resizable';
-
-export const TableCellElement = withRef<
- typeof PlateElement,
- {
- hideBorder?: boolean;
- isHeader?: boolean;
- }
->(({ children, className, hideBorder, isHeader, style, ...props }, ref) => {
- const { element } = props;
-
- const rowElement = useElement(TableRowPlugin.key);
- const isSelectingRow = useBlockSelected(rowElement.id as string);
-
- const {
- borders,
- colIndex,
- colSpan,
- hovered,
- hoveredLeft,
- isSelectingCell,
- readOnly,
- rowIndex,
- rowSize,
- selected,
- } = useTableCellElementState();
- const { props: cellProps } = useTableCellElement({ element: props.element });
- const resizableState = useTableCellElementResizableState({
- colIndex,
- colSpan,
- rowIndex,
- });
-
- const { bottomProps, hiddenLeft, leftProps, rightProps } =
- useTableCellElementResizable(resizableState);
-
- return (
- <PlateElement
- ref={ref}
- as={isHeader ? 'th' : 'td'}
- className={cn(
- 'relative h-full overflow-visible border-none bg-background p-0',
- hideBorder && 'before:border-none',
- element.background ? 'bg-[--cellBackground]' : 'bg-background',
- !hideBorder &&
- cn(
- isHeader && 'text-left [&_>_*]:m-0',
- 'before:size-full',
- selected && 'before:z-10 before:bg-muted',
- "before:absolute before:box-border before:select-none before:content-['']",
- borders &&
- cn(
- borders.bottom?.size &&
- `before:border-b before:border-b-border`,
- borders.right?.size && `before:border-r before:border-r-border`,
- borders.left?.size && `before:border-l before:border-l-border`,
- borders.top?.size && `before:border-t before:border-t-border`
- )
- ),
- className
- )}
- {...cellProps}
- {...props}
- style={
- {
- '--cellBackground': element.background,
- ...style,
- } as React.CSSProperties
- }
- >
- <div
- className="relative z-20 box-border h-full px-3 py-2"
- style={{
- minHeight: rowSize,
- }}
- >
- {children}
- </div>
-
- {!isSelectingCell && (
- <div
- className="group absolute top-0 size-full select-none"
- contentEditable={false}
- suppressContentEditableWarning={true}
- >
- {!readOnly && (
- <>
- <ResizeHandle
- {...rightProps}
- className="-top-3 right-[-5px] w-[10px]"
- />
- <ResizeHandle
- {...bottomProps}
- className="bottom-[-5px] h-[10px]"
- />
- {!hiddenLeft && (
- <ResizeHandle
- {...leftProps}
- className="-top-3 left-[-5px] w-[10px]"
- />
- )}
-
- {hovered && (
- <div
- className={cn(
- 'absolute -top-3 z-30 h-[calc(100%_+_12px)] w-1 bg-ring',
- 'right-[-1.5px]'
- )}
- />
- )}
- {hoveredLeft && (
- <div
- className={cn(
- 'absolute -top-3 z-30 h-[calc(100%_+_12px)] w-1 bg-ring',
- 'left-[-1.5px]'
- )}
- />
- )}
- </>
- )}
- </div>
- )}
-
- {isSelectingRow && (
- <div className={blockSelectionVariants()} contentEditable={false} />
- )}
- </PlateElement>
- );
-});
-
-TableCellElement.displayName = 'TableCellElement';
-
-export const TableCellHeaderElement = withProps(TableCellElement, {
- isHeader: true,
-});
diff --git a/apps/web/app/components/plate-ui/table-dropdown-menu.tsx b/apps/web/app/components/plate-ui/table-dropdown-menu.tsx
deleted file mode 100644
index 5f28689b..00000000
--- a/apps/web/app/components/plate-ui/table-dropdown-menu.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
-
-import { someNode } from '@udecode/plate-common';
-import {
- focusEditor,
- useEditorPlugin,
- useEditorSelector,
-} from '@udecode/plate-common/react';
-import { deleteTable, insertTableRow } from '@udecode/plate-table';
-import {
- TablePlugin,
- deleteColumn,
- deleteRow,
- insertTable,
-} from '@udecode/plate-table/react';
-import {
- Minus,
- Plus,
- RectangleHorizontal,
- RectangleVertical,
- Table,
- Trash,
-} from 'lucide-react';
-
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuTrigger,
- useOpenState,
-} from './dropdown-menu';
-import { ToolbarButton } from './toolbar';
-
-export function TableDropdownMenu(props: DropdownMenuProps) {
- const tableSelected = useEditorSelector(
- (editor) => someNode(editor, { match: { type: TablePlugin.key } }),
- []
- );
-
- const { editor, tf } = useEditorPlugin(TablePlugin);
-
- const openState = useOpenState();
-
- return (
- <DropdownMenu modal={false} {...openState} {...props}>
- <DropdownMenuTrigger asChild>
- <ToolbarButton pressed={openState.open} tooltip="Table" isDropdown>
- <Table />
- </ToolbarButton>
- </DropdownMenuTrigger>
-
- <DropdownMenuContent
- className="flex w-[180px] min-w-0 flex-col"
- align="start"
- >
- <DropdownMenuGroup>
- <DropdownMenuSub>
- <DropdownMenuSubTrigger>
- <Table />
- <span>Table</span>
- </DropdownMenuSubTrigger>
- <DropdownMenuSubContent>
- <DropdownMenuItem
- className="min-w-[180px]"
- onSelect={() => {
- insertTable(editor, {}, { select: true });
- focusEditor(editor);
- }}
- >
- <Plus />
- Insert table
- </DropdownMenuItem>
- <DropdownMenuItem
- className="min-w-[180px]"
- disabled={!tableSelected}
- onSelect={() => {
- deleteTable(editor);
- focusEditor(editor);
- }}
- >
- <Trash />
- Delete table
- </DropdownMenuItem>
- </DropdownMenuSubContent>
- </DropdownMenuSub>
-
- <DropdownMenuSub>
- <DropdownMenuSubTrigger disabled={!tableSelected}>
- <RectangleVertical />
- <span>Column</span>
- </DropdownMenuSubTrigger>
- <DropdownMenuSubContent>
- <DropdownMenuItem
- className="min-w-[180px]"
- disabled={!tableSelected}
- onSelect={() => {
- tf.insert.tableColumn();
- focusEditor(editor);
- }}
- >
- <Plus />
- Insert column after
- </DropdownMenuItem>
- <DropdownMenuItem
- className="min-w-[180px]"
- disabled={!tableSelected}
- onSelect={() => {
- deleteColumn(editor);
- focusEditor(editor);
- }}
- >
- <Minus />
- Delete column
- </DropdownMenuItem>
- </DropdownMenuSubContent>
- </DropdownMenuSub>
-
- <DropdownMenuSub>
- <DropdownMenuSubTrigger disabled={!tableSelected}>
- <RectangleHorizontal />
- <span>Row</span>
- </DropdownMenuSubTrigger>
- <DropdownMenuSubContent>
- <DropdownMenuItem
- className="min-w-[180px]"
- disabled={!tableSelected}
- onSelect={() => {
- insertTableRow(editor);
- focusEditor(editor);
- }}
- >
- <Plus />
- Insert row after
- </DropdownMenuItem>
- <DropdownMenuItem
- className="min-w-[180px]"
- disabled={!tableSelected}
- onSelect={() => {
- deleteRow(editor);
- focusEditor(editor);
- }}
- >
- <Minus />
- Delete row
- </DropdownMenuItem>
- </DropdownMenuSubContent>
- </DropdownMenuSub>
- </DropdownMenuGroup>
- </DropdownMenuContent>
- </DropdownMenu>
- );
-}
diff --git a/apps/web/app/components/plate-ui/table-element.tsx b/apps/web/app/components/plate-ui/table-element.tsx
deleted file mode 100644
index 14bacd94..00000000
--- a/apps/web/app/components/plate-ui/table-element.tsx
+++ /dev/null
@@ -1,330 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
-import type { TTableElement } from '@udecode/plate-table';
-
-import { PopoverAnchor } from '@radix-ui/react-popover';
-import { cn, withRef } from '@udecode/cn';
-import { isSelectionExpanded } from '@udecode/plate-common';
-import {
- useEditorRef,
- useEditorSelector,
- useElement,
- useRemoveNodeButton,
- withHOC,
-} from '@udecode/plate-common/react';
-import {
- TableProvider,
- mergeTableCells,
- unmergeTableCells,
- useTableBordersDropdownMenuContentState,
- useTableElement,
- useTableElementState,
- useTableMergeState,
-} from '@udecode/plate-table/react';
-import { type LucideProps, Combine, Trash2Icon, Ungroup } from 'lucide-react';
-import { useReadOnly, useSelected } from 'slate-react';
-
-import { Button } from './button';
-import {
- DropdownMenu,
- DropdownMenuCheckboxItem,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuPortal,
- DropdownMenuTrigger,
-} from './dropdown-menu';
-import { PlateElement } from './plate-element';
-import { Popover, PopoverContent, popoverVariants } from './popover';
-
-export const TableBordersDropdownMenuContent = withRef<
- typeof DropdownMenuPrimitive.Content
->((props, ref) => {
- const {
- getOnSelectTableBorder,
- hasBottomBorder,
- hasLeftBorder,
- hasNoBorders,
- hasOuterBorders,
- hasRightBorder,
- hasTopBorder,
- } = useTableBordersDropdownMenuContentState();
-
- return (
- <DropdownMenuContent
- ref={ref}
- className={cn('min-w-[220px]')}
- align="start"
- side="right"
- sideOffset={0}
- {...props}
- >
- <DropdownMenuGroup>
- <DropdownMenuCheckboxItem
- checked={hasBottomBorder}
- onCheckedChange={getOnSelectTableBorder('bottom')}
- >
- <BorderBottom />
- <div>Bottom Border</div>
- </DropdownMenuCheckboxItem>
- <DropdownMenuCheckboxItem
- checked={hasTopBorder}
- onCheckedChange={getOnSelectTableBorder('top')}
- >
- <BorderTop />
- <div>Top Border</div>
- </DropdownMenuCheckboxItem>
- <DropdownMenuCheckboxItem
- checked={hasLeftBorder}
- onCheckedChange={getOnSelectTableBorder('left')}
- >
- <BorderLeft />
- <div>Left Border</div>
- </DropdownMenuCheckboxItem>
- <DropdownMenuCheckboxItem
- checked={hasRightBorder}
- onCheckedChange={getOnSelectTableBorder('right')}
- >
- <BorderRight />
- <div>Right Border</div>
- </DropdownMenuCheckboxItem>
- </DropdownMenuGroup>
-
- <DropdownMenuGroup>
- <DropdownMenuCheckboxItem
- checked={hasNoBorders}
- onCheckedChange={getOnSelectTableBorder('none')}
- >
- <BorderNone />
- <div>No Border</div>
- </DropdownMenuCheckboxItem>
- <DropdownMenuCheckboxItem
- checked={hasOuterBorders}
- onCheckedChange={getOnSelectTableBorder('outer')}
- >
- <BorderAll />
- <div>Outside Borders</div>
- </DropdownMenuCheckboxItem>
- </DropdownMenuGroup>
- </DropdownMenuContent>
- );
-});
-
-export const TableFloatingToolbar = withRef<typeof PopoverContent>(
- ({ children, ...props }, ref) => {
- const element = useElement<TTableElement>();
- const { props: buttonProps } = useRemoveNodeButton({ element });
-
- const selectionCollapsed = useEditorSelector(
- (editor) => !isSelectionExpanded(editor),
- []
- );
-
- const readOnly = useReadOnly();
- const selected = useSelected();
- const editor = useEditorRef();
-
- const collapsed = !readOnly && selected && selectionCollapsed;
- const open = !readOnly && selected;
-
- const { canMerge, canUnmerge } = useTableMergeState();
-
- const mergeContent = canMerge && (
- <Button
- variant="ghost"
- onClick={() => mergeTableCells(editor)}
- contentEditable={false}
- isMenu
- >
- <Combine />
- Merge
- </Button>
- );
-
- const unmergeButton = canUnmerge && (
- <Button
- variant="ghost"
- onClick={() => unmergeTableCells(editor)}
- contentEditable={false}
- isMenu
- >
- <Ungroup />
- Unmerge
- </Button>
- );
-
- const bordersContent = collapsed && (
- <>
- <DropdownMenu modal={false}>
- <DropdownMenuTrigger asChild>
- <Button variant="ghost" isMenu>
- <BorderAll />
- Borders
- </Button>
- </DropdownMenuTrigger>
-
- <DropdownMenuPortal>
- <TableBordersDropdownMenuContent />
- </DropdownMenuPortal>
- </DropdownMenu>
-
- <Button variant="ghost" contentEditable={false} isMenu {...buttonProps}>
- <Trash2Icon />
- Delete
- </Button>
- </>
- );
-
- return (
- <Popover open={open} modal={false}>
- <PopoverAnchor asChild>{children}</PopoverAnchor>
- {(canMerge || canUnmerge || collapsed) && (
- <PopoverContent
- ref={ref}
- className={cn(popoverVariants(), 'flex w-[220px] flex-col p-1')}
- onOpenAutoFocus={(e) => e.preventDefault()}
- {...props}
- >
- {unmergeButton}
- {mergeContent}
- {bordersContent}
- </PopoverContent>
- )}
- </Popover>
- );
- }
-);
-
-export const TableElement = withHOC(
- TableProvider,
- withRef<typeof PlateElement>(({ children, className, ...props }, ref) => {
- const { colSizes, isSelectingCell, marginLeft, minColumnWidth } =
- useTableElementState();
- const { colGroupProps, props: tableProps } = useTableElement();
-
- return (
- <TableFloatingToolbar>
- <PlateElement
- className={cn('overflow-x-auto', className)}
- style={{ paddingLeft: marginLeft }}
- {...props}
- >
- <table
- ref={ref}
- className={cn(
- 'my-4 ml-px mr-0 table h-px w-[calc(100%-6px)] table-fixed border-collapse',
- isSelectingCell && '[&_*::selection]:bg-none'
- )}
- {...tableProps}
- >
- <colgroup {...colGroupProps}>
- {colSizes.map((width, index) => (
- <col
- key={index}
- style={{
- minWidth: minColumnWidth,
- width: width || undefined,
- }}
- />
- ))}
- </colgroup>
-
- <tbody className="min-w-full">{children}</tbody>
- </table>
- </PlateElement>
- </TableFloatingToolbar>
- );
- })
-);
-
-const BorderAll = (props: LucideProps) => (
- <svg
- fill="currentColor"
- focusable="false"
- height="48"
- role="img"
- viewBox="0 0 24 24"
- width="48"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <path d="M3 6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6zm10 13h5a1 1 0 0 0 1-1v-5h-6v6zm-2-6H5v5a1 1 0 0 0 1 1h5v-6zm2-2h6V6a1 1 0 0 0-1-1h-5v6zm-2-6H6a1 1 0 0 0-1 1v5h6V5z" />
- </svg>
-);
-
-const BorderBottom = (props: LucideProps) => (
- <svg
- fill="currentColor"
- focusable="false"
- height="48"
- role="img"
- viewBox="0 0 24 24"
- width="48"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <path d="M13 5a1 1 0 1 0 0-2h-2a1 1 0 1 0 0 2h2zm-8 6a1 1 0 1 0-2 0v2a1 1 0 1 0 2 0v-2zm-2 7a1 1 0 1 1 2 0 1 1 0 0 0 1 1h12a1 1 0 0 0 1-1 1 1 0 1 1 2 0 3 3 0 0 1-3 3H6a3 3 0 0 1-3-3zm17-8a1 1 0 0 0-1 1v2a1 1 0 1 0 2 0v-2a1 1 0 0 0-1-1zM7 4a1 1 0 0 0-1-1 3 3 0 0 0-3 3 1 1 0 0 0 2 0 1 1 0 0 1 1-1 1 1 0 0 0 1-1zm11-1a1 1 0 1 0 0 2 1 1 0 0 1 1 1 1 1 0 1 0 2 0 3 3 0 0 0-3-3z" />
- </svg>
-);
-
-const BorderLeft = (props: LucideProps) => (
- <svg
- fill="currentColor"
- focusable="false"
- height="48"
- role="img"
- viewBox="0 0 24 24"
- width="48"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <path d="M6 21a1 1 0 1 0 0-2 1 1 0 0 1-1-1V6a1 1 0 0 1 1-1 1 1 0 0 0 0-2 3 3 0 0 0-3 3v12a3 3 0 0 0 3 3zm7-16a1 1 0 1 0 0-2h-2a1 1 0 1 0 0 2h2zm6 6a1 1 0 1 1 2 0v2a1 1 0 1 1-2 0v-2zm-5 9a1 1 0 0 1-1 1h-2a1 1 0 1 1 0-2h2a1 1 0 0 1 1 1zm4-17a1 1 0 1 0 0 2 1 1 0 0 1 1 1 1 1 0 1 0 2 0 3 3 0 0 0-3-3zm-1 17a1 1 0 0 0 1 1 3 3 0 0 0 3-3 1 1 0 1 0-2 0 1 1 0 0 1-1 1 1 1 0 0 0-1 1z" />
- </svg>
-);
-
-const BorderNone = (props: LucideProps) => (
- <svg
- fill="currentColor"
- focusable="false"
- height="48"
- role="img"
- viewBox="0 0 24 24"
- width="48"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <path d="M14 4a1 1 0 0 1-1 1h-2a1 1 0 1 1 0-2h2a1 1 0 0 1 1 1zm-9 7a1 1 0 1 0-2 0v2a1 1 0 1 0 2 0v-2zm14 0a1 1 0 1 1 2 0v2a1 1 0 1 1-2 0v-2zm-6 10a1 1 0 1 0 0-2h-2a1 1 0 1 0 0 2h2zM7 4a1 1 0 0 0-1-1 3 3 0 0 0-3 3 1 1 0 0 0 2 0 1 1 0 0 1 1-1 1 1 0 0 0 1-1zm11-1a1 1 0 1 0 0 2 1 1 0 0 1 1 1 1 1 0 1 0 2 0 3 3 0 0 0-3-3zM7 20a1 1 0 0 1-1 1 3 3 0 0 1-3-3 1 1 0 1 1 2 0 1 1 0 0 0 1 1 1 1 0 0 1 1 1zm11 1a1 1 0 1 1 0-2 1 1 0 0 0 1-1 1 1 0 1 1 2 0 3 3 0 0 1-3 3z" />
- </svg>
-);
-
-const BorderRight = (props: LucideProps) => (
- <svg
- fill="currentColor"
- focusable="false"
- height="48"
- role="img"
- viewBox="0 0 24 24"
- width="48"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <path d="M13 5a1 1 0 1 0 0-2h-2a1 1 0 1 0 0 2h2zm-8 6a1 1 0 1 0-2 0v2a1 1 0 1 0 2 0v-2zm9 9a1 1 0 0 1-1 1h-2a1 1 0 1 1 0-2h2a1 1 0 0 1 1 1zM6 3a1 1 0 0 1 0 2 1 1 0 0 0-1 1 1 1 0 0 1-2 0 3 3 0 0 1 3-3zm1 17a1 1 0 0 1-1 1 3 3 0 0 1-3-3 1 1 0 1 1 2 0 1 1 0 0 0 1 1 1 1 0 0 1 1 1zm11 1a1 1 0 1 1 0-2 1 1 0 0 0 1-1V6a1 1 0 0 0-1-1 1 1 0 1 1 0-2 3 3 0 0 1 3 3v12a3 3 0 0 1-3 3z" />
- </svg>
-);
-
-const BorderTop = (props: LucideProps) => (
- <svg
- fill="currentColor"
- focusable="false"
- height="48"
- role="img"
- viewBox="0 0 24 24"
- width="48"
- xmlns="http://www.w3.org/2000/svg"
- {...props}
- >
- <path d="M3 6a1 1 0 0 0 2 0 1 1 0 0 1 1-1h12a1 1 0 0 1 1 1 1 1 0 1 0 2 0 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3zm2 5a1 1 0 1 0-2 0v2a1 1 0 1 0 2 0v-2zm14 0a1 1 0 1 1 2 0v2a1 1 0 1 1-2 0v-2zm-5 9a1 1 0 0 1-1 1h-2a1 1 0 1 1 0-2h2a1 1 0 0 1 1 1zm-8 1a1 1 0 1 0 0-2 1 1 0 0 1-1-1 1 1 0 1 0-2 0 3 3 0 0 0 3 3zm11-1a1 1 0 0 0 1 1 3 3 0 0 0 3-3 1 1 0 1 0-2 0 1 1 0 0 1-1 1 1 1 0 0 0-1 1z" />
- </svg>
-);
diff --git a/apps/web/app/components/plate-ui/table-row-element.tsx b/apps/web/app/components/plate-ui/table-row-element.tsx
deleted file mode 100644
index d437383b..00000000
--- a/apps/web/app/components/plate-ui/table-row-element.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { cn, withRef } from '@udecode/cn';
-import { PlateElement } from '@udecode/plate-common/react';
-
-export const TableRowElement = withRef<
- typeof PlateElement,
- {
- hideBorder?: boolean;
- }
->(({ children, hideBorder, ...props }, ref) => {
- return (
- <PlateElement
- ref={ref}
- as="tr"
- className={cn('h-full', hideBorder && 'border-none')}
- {...props}
- >
- {children}
- </PlateElement>
- );
-});
diff --git a/apps/web/app/components/plate-ui/toc-element.tsx b/apps/web/app/components/plate-ui/toc-element.tsx
deleted file mode 100644
index 844b2bed..00000000
--- a/apps/web/app/components/plate-ui/toc-element.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-'use client';
-
-import { cn, withRef } from '@udecode/cn';
-import {
- useTocElement,
- useTocElementState,
-} from '@udecode/plate-heading/react';
-import { cva } from 'class-variance-authority';
-
-import { Button } from './button';
-import { PlateElement } from './plate-element';
-
-const headingItemVariants = cva(
- 'block h-auto w-full cursor-pointer truncate rounded-none px-0.5 py-1.5 text-left font-medium text-muted-foreground underline decoration-[0.5px] underline-offset-4 hover:bg-accent hover:text-muted-foreground',
- {
- variants: {
- depth: {
- 1: 'pl-0.5',
- 2: 'pl-[26px]',
- 3: 'pl-[50px]',
- },
- },
- }
-);
-
-export const TocElement = withRef<typeof PlateElement>(
- ({ children, className, ...props }, ref) => {
- const state = useTocElementState();
-
- const { props: btnProps } = useTocElement(state);
-
- const { headingList } = state;
-
- return (
- <PlateElement
- ref={ref}
- className={cn('relative mb-1 p-0', className)}
- {...props}
- >
- <nav contentEditable={false}>
- {headingList.length > 0 ? (
- headingList.map((item) => (
- <Button
- key={item.id}
- variant="ghost"
- className={cn(
- headingItemVariants({ depth: item.depth as any })
- )}
- onClick={(e) => btnProps.onClick(e, item, 'smooth')}
- aria-current
- >
- {item.title}
- </Button>
- ))
- ) : (
- <div className="text-sm text-gray-500">
- Create a heading to display the table of contents.
- </div>
- )}
- </nav>
- {children}
- </PlateElement>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/toggle-element.tsx b/apps/web/app/components/plate-ui/toggle-element.tsx
deleted file mode 100644
index 42366f3b..00000000
--- a/apps/web/app/components/plate-ui/toggle-element.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-'use client';
-
-import { cn, withRef } from '@udecode/cn';
-import { useElement } from '@udecode/plate-common/react';
-import {
- useToggleButton,
- useToggleButtonState,
-} from '@udecode/plate-toggle/react';
-import { ChevronRight } from 'lucide-react';
-
-import { Button } from './button';
-import { PlateElement } from './plate-element';
-
-export const ToggleElement = withRef<typeof PlateElement>(
- ({ children, className, ...props }, ref) => {
- const element = useElement();
- const state = useToggleButtonState(element.id as string);
- const { buttonProps, open } = useToggleButton(state);
-
- return (
- <PlateElement
- ref={ref}
- className={cn('relative pl-6', className)}
- {...props}
- >
- <Button
- size="icon"
- variant="ghost"
- className="absolute -left-0.5 top-0 size-6 cursor-pointer select-none items-center justify-center rounded-md p-px text-muted-foreground transition-colors hover:bg-accent [&_svg]:size-4"
- contentEditable={false}
- {...buttonProps}
- >
- <ChevronRight
- className={cn(
- 'transition-transform duration-75',
- open ? 'rotate-90' : 'rotate-0'
- )}
- />
- </Button>
- {children}
- </PlateElement>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/toggle-toolbar-button.tsx b/apps/web/app/components/plate-ui/toggle-toolbar-button.tsx
deleted file mode 100644
index 985a05f6..00000000
--- a/apps/web/app/components/plate-ui/toggle-toolbar-button.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { withRef } from '@udecode/cn';
-import {
- useToggleToolbarButton,
- useToggleToolbarButtonState,
-} from '@udecode/plate-toggle/react';
-import { ChevronRightIcon } from 'lucide-react';
-
-import { ToolbarButton } from './toolbar';
-
-export const ToggleToolbarButton = withRef<typeof ToolbarButton>(
- (rest, ref) => {
- const state = useToggleToolbarButtonState();
- const { props } = useToggleToolbarButton(state);
-
- return (
- <ToolbarButton ref={ref} tooltip="Toggle" {...props} {...rest}>
- <ChevronRightIcon />
- </ToolbarButton>
- );
- }
-);
diff --git a/apps/web/app/components/plate-ui/toolbar.tsx b/apps/web/app/components/plate-ui/toolbar.tsx
deleted file mode 100644
index 9720c362..00000000
--- a/apps/web/app/components/plate-ui/toolbar.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-'use client';
-
-import * as React from 'react';
-
-import * as ToolbarPrimitive from '@radix-ui/react-toolbar';
-import { cn, withCn, withRef, withVariants } from '@udecode/cn';
-import { type VariantProps, cva } from 'class-variance-authority';
-import { ChevronDown } from 'lucide-react';
-
-import { Separator } from './separator';
-import { withTooltip } from './tooltip';
-
-export const Toolbar = withCn(
- ToolbarPrimitive.Root,
- 'relative flex select-none items-center'
-);
-
-export const ToolbarToggleGroup = withCn(
- ToolbarPrimitive.ToolbarToggleGroup,
- 'flex items-center'
-);
-
-export const ToolbarLink = withCn(
- ToolbarPrimitive.Link,
- 'font-medium underline underline-offset-4'
-);
-
-export const ToolbarSeparator = withCn(
- ToolbarPrimitive.Separator,
- 'mx-2 my-1 w-px shrink-0 bg-border'
-);
-
-const toolbarButtonVariants = cva(
- cn(
- 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium text-foreground ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg:not([data-icon])]:size-4'
- ),
- {
- defaultVariants: {
- size: 'sm',
- variant: 'default',
- },
- variants: {
- size: {
- default: 'h-10 px-3',
- lg: 'h-11 px-5',
- sm: 'h-7 px-2',
- },
- variant: {
- default:
- 'bg-transparent hover:bg-muted hover:text-muted-foreground aria-checked:bg-accent aria-checked:text-accent-foreground',
- outline:
- 'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',
- },
- },
- }
-);
-
-const ToolbarButton = withTooltip(
- // eslint-disable-next-line react/display-name
- React.forwardRef<
- React.ElementRef<typeof ToolbarToggleItem>,
- {
- isDropdown?: boolean;
- pressed?: boolean;
- } & Omit<
- React.ComponentPropsWithoutRef<typeof ToolbarToggleItem>,
- 'asChild' | 'value'
- > &
- VariantProps<typeof toolbarButtonVariants>
- >(
- (
- { children, className, isDropdown, pressed, size, variant, ...props },
- ref
- ) => {
- return typeof pressed === 'boolean' ? (
- <ToolbarToggleGroup
- disabled={props.disabled}
- value="single"
- type="single"
- >
- <ToolbarToggleItem
- ref={ref}
- className={cn(
- toolbarButtonVariants({
- size,
- variant,
- }),
- isDropdown && 'justify-between gap-1 pr-1',
- className
- )}
- value={pressed ? 'single' : ''}
- {...props}
- >
- {isDropdown ? (
- <>
- <div className="flex flex-1 items-center gap-2 whitespace-nowrap">
- {children}
- </div>
- <div>
- <ChevronDown
- className="size-3.5 text-muted-foreground"
- data-icon
- />
- </div>
- </>
- ) : (
- children
- )}
- </ToolbarToggleItem>
- </ToolbarToggleGroup>
- ) : (
- <ToolbarPrimitive.Button
- ref={ref}
- className={cn(
- toolbarButtonVariants({
- size,
- variant,
- }),
- isDropdown && 'pr-1',
- className
- )}
- {...props}
- >
- {children}
- </ToolbarPrimitive.Button>
- );
- }
- )
-);
-ToolbarButton.displayName = 'ToolbarButton';
-
-export { ToolbarButton };
-
-export const ToolbarToggleItem = withVariants(
- ToolbarPrimitive.ToggleItem,
- toolbarButtonVariants,
- ['variant', 'size']
-);
-
-export const ToolbarGroup = withRef<'div'>(({ children, className }, ref) => {
- return (
- <div
- ref={ref}
- className={cn(
- 'group/toolbar-group',
- 'relative hidden has-[button]:flex',
- className
- )}
- >
- <div className="flex items-center">{children}</div>
-
- <div className="mx-1.5 py-0.5 group-last/toolbar-group:!hidden">
- <Separator orientation="vertical" />
- </div>
- </div>
- );
-});
diff --git a/apps/web/app/components/plate-ui/tooltip.tsx b/apps/web/app/components/plate-ui/tooltip.tsx
deleted file mode 100644
index ca6e82ab..00000000
--- a/apps/web/app/components/plate-ui/tooltip.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import * as TooltipPrimitive from '@radix-ui/react-tooltip';
-import { withCn, withProps } from '@udecode/cn';
-
-export const TooltipProvider = withProps(TooltipPrimitive.Provider, {
- delayDuration: 0,
- disableHoverableContent: true,
- skipDelayDuration: 0,
-});
-
-export const Tooltip = TooltipPrimitive.Root;
-
-export const TooltipTrigger = TooltipPrimitive.Trigger;
-
-export const TooltipPortal = TooltipPrimitive.Portal;
-
-export const TooltipContent = withCn(
- withProps(TooltipPrimitive.Content, {
- sideOffset: 4,
- }),
- 'z-50 overflow-hidden rounded-md bg-black px-3 py-1.5 text-sm font-semibold text-white shadow-md'
-);
-
-export function withTooltip<
- T extends React.ComponentType<any> | keyof HTMLElementTagNameMap,
->(Component: T) {
- return React.forwardRef<
- React.ElementRef<T>,
- {
- tooltipContentProps?: Omit<
- React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>,
- 'children'
- >;
- tooltipProps?: Omit<
- React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Root>,
- 'children'
- >;
- tooltip?: React.ReactNode;
- } & React.ComponentPropsWithoutRef<T> &
- Omit<TooltipPrimitive.TooltipProviderProps, 'children'>
- >(function ExtendComponent(
- {
- delayDuration = 0,
- disableHoverableContent = true,
- skipDelayDuration = 0,
- tooltip,
- tooltipContentProps,
- tooltipProps,
- ...props
- },
- ref
- ) {
- const [mounted, setMounted] = React.useState(false);
-
- React.useEffect(() => {
- setMounted(true);
- }, []);
-
- const component = <Component ref={ref} {...(props as any)} />;
-
- if (tooltip && mounted) {
- return (
- <TooltipProvider
- delayDuration={delayDuration}
- disableHoverableContent={disableHoverableContent}
- skipDelayDuration={skipDelayDuration}
- >
- <Tooltip {...tooltipProps}>
- <TooltipTrigger asChild>{component}</TooltipTrigger>
-
- <TooltipPortal>
- <TooltipContent {...tooltipContentProps}>
- {tooltip}
- </TooltipContent>
- </TooltipPortal>
- </Tooltip>
- </TooltipProvider>
- );
- }
-
- return component;
- });
-}
diff --git a/apps/web/app/components/plate-ui/turn-into-dropdown-menu.tsx b/apps/web/app/components/plate-ui/turn-into-dropdown-menu.tsx
deleted file mode 100644
index a81dd09b..00000000
--- a/apps/web/app/components/plate-ui/turn-into-dropdown-menu.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
-
-import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
-import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
-import {
- ParagraphPlugin,
- focusEditor,
- useEditorRef,
- useSelectionFragmentProp,
-} from '@udecode/plate-common/react';
-import { HEADING_KEYS } from '@udecode/plate-heading';
-import { INDENT_LIST_KEYS, ListStyleType } from '@udecode/plate-indent-list';
-import { TogglePlugin } from '@udecode/plate-toggle/react';
-import {
- ChevronRightIcon,
- Columns3Icon,
- FileCodeIcon,
- Heading1Icon,
- Heading2Icon,
- Heading3Icon,
- ListIcon,
- ListOrderedIcon,
- PilcrowIcon,
- QuoteIcon,
- SquareIcon,
-} from 'lucide-react';
-
-import {
- getBlockType,
- setBlockType,
-} from '~/components/editor/transforms';
-
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuTrigger,
- useOpenState,
-} from './dropdown-menu';
-import { ToolbarButton } from './toolbar';
-
-const turnIntoItems = [
- {
- icon: <PilcrowIcon />,
- keywords: ['paragraph'],
- label: 'Text',
- value: ParagraphPlugin.key,
- },
- {
- icon: <Heading1Icon />,
- keywords: ['title', 'h1'],
- label: 'Heading 1',
- value: HEADING_KEYS.h1,
- },
- {
- icon: <Heading2Icon />,
- keywords: ['subtitle', 'h2'],
- label: 'Heading 2',
- value: HEADING_KEYS.h2,
- },
- {
- icon: <Heading3Icon />,
- keywords: ['subtitle', 'h3'],
- label: 'Heading 3',
- value: HEADING_KEYS.h3,
- },
- {
- icon: <ListIcon />,
- keywords: ['unordered', 'ul', '-'],
- label: 'Bulleted list',
- value: ListStyleType.Disc,
- },
- {
- icon: <ListOrderedIcon />,
- keywords: ['ordered', 'ol', '1'],
- label: 'Numbered list',
- value: ListStyleType.Decimal,
- },
- {
- icon: <SquareIcon />,
- keywords: ['checklist', 'task', 'checkbox', '[]'],
- label: 'To-do list',
- value: INDENT_LIST_KEYS.todo,
- },
- {
- icon: <ChevronRightIcon />,
- keywords: ['collapsible', 'expandable'],
- label: 'Toggle list',
- value: TogglePlugin.key,
- },
- {
- icon: <FileCodeIcon />,
- keywords: ['```'],
- label: 'Code',
- value: CodeBlockPlugin.key,
- },
- {
- icon: <QuoteIcon />,
- keywords: ['citation', 'blockquote', '>'],
- label: 'Quote',
- value: BlockquotePlugin.key,
- },
- {
- icon: <Columns3Icon />,
- label: '3 columns',
- value: 'action_three_columns',
- },
-];
-
-export function TurnIntoDropdownMenu(props: DropdownMenuProps) {
- const editor = useEditorRef();
- const openState = useOpenState();
-
- const value = useSelectionFragmentProp({
- defaultValue: ParagraphPlugin.key,
- getProp: (node) => getBlockType(node as any),
- });
- const selectedItem = React.useMemo(
- () =>
- turnIntoItems.find(
- (item) => item.value === (value ?? ParagraphPlugin.key)
- ) ?? turnIntoItems[0],
- [value]
- );
-
- return (
- <DropdownMenu modal={false} {...openState} {...props}>
- <DropdownMenuTrigger asChild>
- <ToolbarButton pressed={openState.open} tooltip="Turn into" isDropdown>
- {selectedItem.label}
- </ToolbarButton>
- </DropdownMenuTrigger>
-
- <DropdownMenuContent
- className="ignore-click-outside/toolbar min-w-0"
- align="start"
- >
- <DropdownMenuRadioGroup
- value={value}
- onValueChange={(type) => {
- setBlockType(editor, type);
- focusEditor(editor);
- }}
- label="Turn into"
- >
- {turnIntoItems.map(({ icon, label, value: itemValue }) => (
- <DropdownMenuRadioItem
- key={itemValue}
- className="min-w-[180px]"
- value={itemValue}
- >
- {icon}
- {label}
- </DropdownMenuRadioItem>
- ))}
- </DropdownMenuRadioGroup>
- </DropdownMenuContent>
- </DropdownMenu>
- );
-}
diff --git a/apps/web/app/components/plate-ui/with-draggables.tsx b/apps/web/app/components/plate-ui/with-draggables.tsx
deleted file mode 100644
index 85f455a7..00000000
--- a/apps/web/app/components/plate-ui/with-draggables.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-'use client';
-
-import type { FC } from 'react';
-
-import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
-import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
-import {
- ParagraphPlugin,
- createNodesWithHOC,
-} from '@udecode/plate-common/react';
-import {
- type WithDraggableOptions,
- withDraggable as withDraggablePrimitive,
-} from '@udecode/plate-dnd';
-import { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react';
-import { HEADING_KEYS } from '@udecode/plate-heading';
-import { ColumnPlugin } from '@udecode/plate-layout/react';
-import {
- ImagePlugin,
- MediaEmbedPlugin,
- PlaceholderPlugin,
-} from '@udecode/plate-media/react';
-import { TablePlugin } from '@udecode/plate-table/react';
-import { TogglePlugin } from '@udecode/plate-toggle/react';
-
-import { type DraggableProps, Draggable } from './draggable';
-
-export const withDraggable = (
- Component: FC,
- options?: WithDraggableOptions<
- Partial<Omit<DraggableProps, 'children' | 'editor' | 'element'>>
- >
-) =>
- withDraggablePrimitive<DraggableProps>(Draggable, Component, options as any);
-
-export const withDraggablesPrimitive = createNodesWithHOC(withDraggable);
-
-export const withDraggables = (components: any) => {
- return withDraggablesPrimitive(components, [
- {
- keys: [ParagraphPlugin.key, 'ul', 'ol'],
- level: 0,
- },
- {
- key: HEADING_KEYS.h1,
- draggableProps: {
- className:
- '[&_.slate-blockToolbarWrapper]:h-[1.3em] [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-1 [&_.slate-gutterLeft]:text-[1.875em]',
- },
- },
- {
- key: HEADING_KEYS.h2,
- draggableProps: {
- className:
- '[&_.slate-blockToolbarWrapper]:h-[1.3em] [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-1 [&_.slate-gutterLeft]:text-[1.5em]',
- },
- },
- {
- key: HEADING_KEYS.h3,
- draggableProps: {
- className:
- '[&_.slate-blockToolbarWrapper]:h-[1.3em] [&_.slate-gutterLeft]:pt-[2px] [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-1 [&_.slate-gutterLeft]:text-[1.25em]',
- },
- },
- {
- keys: [HEADING_KEYS.h4, HEADING_KEYS.h5],
- draggableProps: {
- className:
- '[&_.slate-blockToolbarWrapper]:h-[1.3em] [&_.slate-gutterLeft]:pt-[3px] [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0 [&_.slate-gutterLeft]:text-[1.1em]',
- },
- },
- {
- keys: [ParagraphPlugin.key],
- draggableProps: {
- className:
- '[&_.slate-gutterLeft]:pt-[3px] [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0',
- },
- },
- {
- keys: [HEADING_KEYS.h6, 'ul', 'ol'],
- draggableProps: {
- className: '[&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0',
- },
- },
- {
- key: BlockquotePlugin.key,
- draggableProps: {
- className: '[&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0',
- },
- },
- {
- key: CodeBlockPlugin.key,
- draggableProps: {
- className:
- '[&_.slate-gutterLeft]:pt-6 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0',
- },
- },
- {
- key: ImagePlugin.key,
- draggableProps: {
- className:
- '[&_.slate-gutterLeft]:pt-0 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0',
- },
- },
- {
- key: MediaEmbedPlugin.key,
- draggableProps: {
- className:
- '[&_.slate-gutterLeft]:pt-0 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0',
- },
- },
- {
- key: ExcalidrawPlugin.key,
- draggableProps: {
- className:
- '[&_.slate-gutterLeft]:pt-0 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0',
- },
- },
- {
- key: TogglePlugin.key,
- draggableProps: {
- className:
- '[&_.slate-gutterLeft]:pt-0 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0',
- },
- },
- {
- key: ColumnPlugin.key,
- draggableProps: {
- className:
- '[&_.slate-gutterLeft]:pt-0 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0',
- },
- },
- {
- key: PlaceholderPlugin.key,
- draggableProps: {
- className:
- '[&_.slate-gutterLeft]:pt-3 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0',
- },
- },
- {
- key: TablePlugin.key,
- draggableProps: {
- className:
- '[&_.slate-gutterLeft]:pt-3 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0',
- },
- },
- ]);
-};
diff --git a/apps/web/app/components/skeletons/HistoriesSkeleton.tsx b/apps/web/app/components/skeletons/HistoriesSkeleton.tsx
deleted file mode 100644
index 335bf350..00000000
--- a/apps/web/app/components/skeletons/HistoriesSkeleton.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from "react";
-import { cn } from "~/lib/utils";
-
-const HistoriesSkeleton: React.FC = () => {
- return (
- <div className="mt-12 max-w-lg hidden md:block">
- <div className="flex items-center gap-2 text-sm text-neutral-500 mb-2">
- <div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
- <div className="w-24 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
- </div>
- <div className="space-y-2">
- {Array.from({ length: 3 }).map((_, index) => (
- <div
- key={index}
- className="flex items-center gap-2 p-2 rounded-lg bg-gray-200 dark:bg-gray-700 animate-pulse"
- >
- <div className="w-4 h-4 bg-gray-300 dark:bg-gray-600 rounded"></div>
- <div className="flex-1 h-4 bg-gray-300 dark:bg-gray-600 rounded"></div>
- </div>
- ))}
- </div>
- </div>
- );
-};
-
-export default HistoriesSkeleton; \ No newline at end of file
diff --git a/apps/web/app/components/skeletons/SuggestionsSkeleton.tsx b/apps/web/app/components/skeletons/SuggestionsSkeleton.tsx
deleted file mode 100644
index e310c781..00000000
--- a/apps/web/app/components/skeletons/SuggestionsSkeleton.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from "react";
-import { cn } from "~/lib/utils";
-
-const SuggestionsSkeleton: React.FC = () => {
- return (
- <div className={cn("flex flex-wrap gap-2 mt-4")}>
- {Array.from({ length: 3 }).map((_, index) => (
- <div
- key={index}
- className="w-1/3 h-8 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"
- ></div>
- ))}
- </div>
- );
-};
-
-export default SuggestionsSkeleton; \ No newline at end of file
diff --git a/apps/web/app/components/theme-button.tsx b/apps/web/app/components/theme-button.tsx
deleted file mode 100644
index eccb88f1..00000000
--- a/apps/web/app/components/theme-button.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Theme, useTheme } from "~/lib/theme-provider";
-
-function ThemeButton() {
- const [theme, setTheme] = useTheme();
- return (
- <button
- onClick={() => setTheme(theme === Theme.LIGHT ? Theme.DARK : Theme.LIGHT)}
- className="rounded-md bg-secondary px-4 py-2 text-secondary-foreground transition-colors hover:bg-secondary/80"
- >
- {theme === Theme.LIGHT ? "Dark" : "Light"} Mode
- </button>
- );
-}
-
-export default ThemeButton;
diff --git a/apps/web/app/components/twitter/avatar-img.tsx b/apps/web/app/components/twitter/avatar-img.tsx
deleted file mode 100644
index b101b78f..00000000
--- a/apps/web/app/components/twitter/avatar-img.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-type AvatarImgProps = {
- src: string;
- alt: string;
- width: number;
- height: number;
-};
-
-// eslint-disable-next-line jsx-a11y/alt-text -- The alt text is part of `...props`
-export const AvatarImg = (props: AvatarImgProps) => <img {...props} />;
diff --git a/apps/web/app/components/twitter/icons/icons.module.css b/apps/web/app/components/twitter/icons/icons.module.css
deleted file mode 100644
index ff9c07f8..00000000
--- a/apps/web/app/components/twitter/icons/icons.module.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.verified {
- margin-left: 0.125rem;
- max-width: 20px;
- max-height: 20px;
- height: 1.25em;
- fill: currentColor;
- user-select: none;
- vertical-align: text-bottom;
-}
diff --git a/apps/web/app/components/twitter/icons/index.ts b/apps/web/app/components/twitter/icons/index.ts
deleted file mode 100644
index f90ec616..00000000
--- a/apps/web/app/components/twitter/icons/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export * from "./verified";
-export * from "./verified-business";
-export * from "./verified-government";
diff --git a/apps/web/app/components/twitter/icons/verified-business.tsx b/apps/web/app/components/twitter/icons/verified-business.tsx
deleted file mode 100644
index 3e2235f8..00000000
--- a/apps/web/app/components/twitter/icons/verified-business.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import s from "./icons.module.css";
-
-export const VerifiedBusiness = () => (
- <svg viewBox="0 0 22 22" aria-label="Verified account" role="img" className={s.verified}>
- <g>
- <linearGradient
- gradientUnits="userSpaceOnUse"
- id="0-a"
- x1="4.411"
- x2="18.083"
- y1="2.495"
- y2="21.508"
- >
- <stop offset="0" stopColor="#f4e72a"></stop>
- <stop offset=".539" stopColor="#cd8105"></stop>
- <stop offset=".68" stopColor="#cb7b00"></stop>
- <stop offset="1" stopColor="#f4ec26"></stop>
- <stop offset="1" stopColor="#f4e72a"></stop>
- </linearGradient>
- <linearGradient
- gradientUnits="userSpaceOnUse"
- id="0-b"
- x1="5.355"
- x2="16.361"
- y1="3.395"
- y2="19.133"
- >
- <stop offset="0" stopColor="#f9e87f"></stop>
- <stop offset=".406" stopColor="#e2b719"></stop>
- <stop offset=".989" stopColor="#e2b719"></stop>
- </linearGradient>
- <g clipRule="evenodd" fillRule="evenodd">
- <path
- d="M13.324 3.848L11 1.6 8.676 3.848l-3.201-.453-.559 3.184L2.06 8.095 3.48 11l-1.42 2.904 2.856 1.516.559 3.184 3.201-.452L11 20.4l2.324-2.248 3.201.452.559-3.184 2.856-1.516L18.52 11l1.42-2.905-2.856-1.516-.559-3.184zm-7.09 7.575l3.428 3.428 5.683-6.206-1.347-1.247-4.4 4.795-2.072-2.072z"
- fill="url(#0-a)"
- ></path>
- <path
- d="M13.101 4.533L11 2.5 8.899 4.533l-2.895-.41-.505 2.88-2.583 1.37L4.2 11l-1.284 2.627 2.583 1.37.505 2.88 2.895-.41L11 19.5l2.101-2.033 2.895.41.505-2.88 2.583-1.37L17.8 11l1.284-2.627-2.583-1.37-.505-2.88zm-6.868 6.89l3.429 3.428 5.683-6.206-1.347-1.247-4.4 4.795-2.072-2.072z"
- fill="url(#0-b)"
- ></path>
- <path
- d="M6.233 11.423l3.429 3.428 5.65-6.17.038-.033-.005 1.398-5.683 6.206-3.429-3.429-.003-1.405.005.003z"
- fill="#d18800"
- ></path>
- </g>
- </g>
- </svg>
-);
diff --git a/apps/web/app/components/twitter/icons/verified-government.tsx b/apps/web/app/components/twitter/icons/verified-government.tsx
deleted file mode 100644
index cb40fdc3..00000000
--- a/apps/web/app/components/twitter/icons/verified-government.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import s from "./icons.module.css";
-
-export const VerifiedGovernment = () => (
- <svg viewBox="0 0 22 22" aria-label="Verified account" role="img" className={s.verified}>
- <g>
- <path
- clipRule="evenodd"
- d="M12.05 2.056c-.568-.608-1.532-.608-2.1 0l-1.393 1.49c-.284.303-.685.47-1.1.455L5.42 3.932c-.832-.028-1.514.654-1.486 1.486l.069 2.039c.014.415-.152.816-.456 1.1l-1.49 1.392c-.608.568-.608 1.533 0 2.101l1.49 1.393c.304.284.47.684.456 1.1l-.07 2.038c-.027.832.655 1.514 1.487 1.486l2.038-.069c.415-.014.816.152 1.1.455l1.392 1.49c.569.609 1.533.609 2.102 0l1.393-1.49c.283-.303.684-.47 1.099-.455l2.038.069c.832.028 1.515-.654 1.486-1.486L18 14.542c-.015-.415.152-.815.455-1.099l1.49-1.393c.608-.568.608-1.533 0-2.101l-1.49-1.393c-.303-.283-.47-.684-.455-1.1l.068-2.038c.029-.832-.654-1.514-1.486-1.486l-2.038.07c-.415.013-.816-.153-1.1-.456zm-5.817 9.367l3.429 3.428 5.683-6.206-1.347-1.247-4.4 4.795-2.072-2.072z"
- fillRule="evenodd"
- ></path>
- </g>
- </svg>
-);
diff --git a/apps/web/app/components/twitter/icons/verified.tsx b/apps/web/app/components/twitter/icons/verified.tsx
deleted file mode 100644
index a139e716..00000000
--- a/apps/web/app/components/twitter/icons/verified.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import s from "./icons.module.css";
-
-export const Verified = () => (
- <svg viewBox="0 0 24 24" aria-label="Verified account" role="img" className={s.verified}>
- <g>
- <path d="M22.25 12c0-1.43-.88-2.67-2.19-3.34.46-1.39.2-2.9-.81-3.91s-2.52-1.27-3.91-.81c-.66-1.31-1.91-2.19-3.34-2.19s-2.67.88-3.33 2.19c-1.4-.46-2.91-.2-3.92.81s-1.26 2.52-.8 3.91c-1.31.67-2.2 1.91-2.2 3.34s.89 2.67 2.2 3.34c-.46 1.39-.21 2.9.8 3.91s2.52 1.26 3.91.81c.67 1.31 1.91 2.19 3.34 2.19s2.68-.88 3.34-2.19c1.39.45 2.9.2 3.91-.81s1.27-2.52.81-3.91c1.31-.67 2.19-1.91 2.19-3.34zm-11.71 4.2L6.8 12.46l1.41-1.42 2.26 2.26 4.8-5.23 1.47 1.36-6.2 6.77z"></path>
- </g>
- </svg>
-);
diff --git a/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-body.module.css b/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-body.module.css
deleted file mode 100644
index e31a45c3..00000000
--- a/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-body.module.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.root {
- font-size: var(--tweet-quoted-body-font-size);
- font-weight: var(--tweet-quoted-body-font-weight);
- line-height: var(--tweet-quoted-body-line-height);
- margin: var(--tweet-quoted-body-margin);
- overflow-wrap: break-word;
- white-space: pre-wrap;
- padding: 0 0.75rem;
-}
diff --git a/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-body.tsx b/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-body.tsx
deleted file mode 100644
index 8fa82e8c..00000000
--- a/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-body.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { EnrichedQuotedTweet } from "react-tweet";
-
-import s from "./quoted-tweet-body.module.css";
-
-type Props = { tweet: EnrichedQuotedTweet };
-
-export const QuotedTweetBody = ({ tweet }: Props) => (
- <p className={s.root} lang={tweet.lang} dir="auto">
- {tweet.entities.map((item, i) => (
- <span key={i} dangerouslySetInnerHTML={{ __html: item.text }} />
- ))}
- </p>
-);
diff --git a/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-container.module.css b/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-container.module.css
deleted file mode 100644
index 06e702ae..00000000
--- a/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-container.module.css
+++ /dev/null
@@ -1,19 +0,0 @@
-.root {
- width: 100%;
- overflow: hidden;
- border: var(--tweet-border);
- border-radius: 12px;
- margin: var(--tweet-quoted-container-margin);
- transition-property: background-color, box-shadow;
- transition-duration: 0.2s;
- cursor: pointer;
-}
-
-.root:hover {
- background-color: var(--tweet-quoted-bg-color-hover);
-}
-
-.article {
- position: relative;
- box-sizing: inherit;
-}
diff --git a/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-container.tsx b/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-container.tsx
deleted file mode 100644
index 0233952b..00000000
--- a/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-container.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { ReactNode } from "react";
-import type { EnrichedQuotedTweet } from "react-tweet";
-
-import s from "./quoted-tweet-container.module.css";
-
-type Props = { tweet: EnrichedQuotedTweet; children: ReactNode };
-
-export const QuotedTweetContainer = ({ tweet, children }: Props) => (
- <div
- className={s.root}
- onClick={(e) => {
- e.preventDefault();
- window.open(tweet.url, "_blank");
- }}
- >
- <article className={s.article}>{children}</article>
- </div>
-);
diff --git a/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-header.module.css b/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-header.module.css
deleted file mode 100644
index 7b939bd2..00000000
--- a/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-header.module.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.header {
- display: flex;
- padding: 0.75rem 0.75rem 0 0.75rem;
- line-height: var(--tweet-header-line-height);
- font-size: var(--tweet-header-font-size);
- white-space: nowrap;
- overflow-wrap: break-word;
- overflow: hidden;
-}
-
-.avatar {
- position: relative;
- height: 20px;
- width: 20px;
-}
-
-.avatarSquare {
- border-radius: 4px;
-}
-
-.author {
- display: flex;
- margin: 0 0.5rem;
-}
-
-.authorText {
- font-weight: 700;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
-}
-
-.username {
- color: var(--tweet-font-color-secondary);
- text-decoration: none;
- text-overflow: ellipsis;
- margin-left: 0.125rem;
-}
diff --git a/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-header.tsx b/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-header.tsx
deleted file mode 100644
index 46966de2..00000000
--- a/apps/web/app/components/twitter/quoted-tweet/quoted-tweet-header.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import type { EnrichedQuotedTweet } from "react-tweet";
-
-import { AvatarImg } from "../avatar-img";
-import { VerifiedBadge } from "../verified-badge.js";
-import s from "./quoted-tweet-header.module.css";
-
-import clsx from "clsx";
-
-type Props = { tweet: EnrichedQuotedTweet };
-
-export const QuotedTweetHeader = ({ tweet }: Props) => {
- const { user } = tweet;
-
- return (
- <div className={s.header}>
- <a href={tweet.url} className={s.avatar} target="_blank" rel="noopener noreferrer">
- <div
- className={clsx(
- s.avatarOverflow,
- user.profile_image_shape === "Square" && s.avatarSquare,
- )}
- >
- <AvatarImg src={user.profile_image_url_https} alt={user.name} width={20} height={20} />
- </div>
- </a>
- <div className={s.author}>
- <div className={s.authorText}>
- <span title={user.name}>{user.name}</span>
- </div>
- <VerifiedBadge user={user} />
- <div className={s.username}>
- <span title={`@${user.screen_name}`}>@{user.screen_name}</span>
- </div>
- </div>
- </div>
- );
-};
diff --git a/apps/web/app/components/twitter/quoted-tweet/quoted-tweet.tsx b/apps/web/app/components/twitter/quoted-tweet/quoted-tweet.tsx
deleted file mode 100644
index 939f9e56..00000000
--- a/apps/web/app/components/twitter/quoted-tweet/quoted-tweet.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { EnrichedQuotedTweet } from "react-tweet";
-
-import { TweetMedia } from "../tweet-media";
-import { QuotedTweetBody } from "./quoted-tweet-body";
-import { QuotedTweetContainer } from "./quoted-tweet-container";
-import { QuotedTweetHeader } from "./quoted-tweet-header";
-
-type Props = { tweet: EnrichedQuotedTweet };
-
-export const QuotedTweet = ({ tweet }: Props) => (
- <QuotedTweetContainer tweet={tweet}>
- <QuotedTweetHeader tweet={tweet} />
- <QuotedTweetBody tweet={tweet} />
- {tweet.mediaDetails?.length ? <TweetMedia quoted tweet={tweet} /> : null}
- </QuotedTweetContainer>
-);
diff --git a/apps/web/app/components/twitter/render-tweet.tsx b/apps/web/app/components/twitter/render-tweet.tsx
deleted file mode 100644
index 50e68eff..00000000
--- a/apps/web/app/components/twitter/render-tweet.tsx
+++ /dev/null
@@ -1,198 +0,0 @@
-import * as ReactTweet from "react-tweet";
-import { ReactNode, memo, useCallback, useEffect, useMemo, useState } from "react";
-import { EnrichedTweet, TwitterComponents } from "react-tweet";
-import type { Tweet } from "react-tweet/api";
-import "react-tweet/theme.css";
-
-import { QuotedTweet } from "./quoted-tweet/quoted-tweet";
-import tweetBodyStyles from "./tweet-body.module.css";
-import tweetContainerStyles from "./tweet-container.module.css";
-import tweetHeaderStyles from "./tweet-header.module.css";
-import tweetInReplyToStyles from "./tweet-in-reply-to.module.css";
-import tweetLinkStyles from "./tweet-link.module.css";
-import { TweetMedia } from "./tweet-media";
-import { VerifiedBadge } from "./verified-badge";
-
-import clsx from "clsx";
-import { useTheme } from "~/lib/theme-provider";
-
-const { enrichTweet } = ReactTweet;
-
-type Props = {
- tweet: Tweet | { error: string };
- components?: TwitterComponents;
-};
-
-type AvatarImgProps = {
- src: string;
- alt: string;
- width: number;
- height: number;
-};
-
-const AvatarImg = memo((props: AvatarImgProps) => <img {...props} />);
-
-const TweetHeader = memo(
- ({ tweet, components }: { tweet: EnrichedTweet; components?: TwitterComponents }) => {
- const Img = components?.AvatarImg ?? AvatarImg;
- const { user } = tweet;
-
- const avatarClasses = useMemo(
- () =>
- clsx(
- tweetHeaderStyles.avatarOverflow,
- user.profile_image_shape === "Square" && tweetHeaderStyles.avatarSquare,
- ),
- [user.profile_image_shape],
- );
-
- return (
- <div className={tweetHeaderStyles.header}>
- <a
- href={tweet.url}
- className={tweetHeaderStyles.avatar}
- target="_blank"
- rel="noopener noreferrer"
- >
- <div className={avatarClasses}>
- <Img src={user.profile_image_url_https} alt={user.name} width={48} height={48} />
- </div>
- <div className={tweetHeaderStyles.avatarOverflow}>
- <div className={tweetHeaderStyles.avatarShadow}></div>
- </div>
- </a>
- <div className={tweetHeaderStyles.author}>
- <a
- href={tweet.url}
- className={tweetHeaderStyles.authorLink}
- target="_blank"
- rel="noopener noreferrer"
- >
- <div className={tweetHeaderStyles.authorLinkText}>
- <span title={user.name}>{user.name}</span>
- </div>
- <VerifiedBadge user={user} className={tweetHeaderStyles.authorVerified} />
- </a>
- <div className={tweetHeaderStyles.authorMeta}>
- <a
- href={tweet.url}
- className={tweetHeaderStyles.username}
- target="_blank"
- rel="noopener noreferrer"
- >
- <span title={`@${user.screen_name}`}>@{user.screen_name}</span>
- </a>
- </div>
- </div>
- </div>
- );
- },
-);
-
-export const TweetContainer = memo(
- ({ className, children }: { className?: string; children: ReactNode }) => {
- const containerClasses = useMemo(
- () => clsx("react-tweet-theme", tweetContainerStyles.root, className),
- [className],
- );
-
- return (
- <div className={containerClasses}>
- <article className={tweetContainerStyles.article}>{children}</article>
- </div>
- );
- },
-);
-
-export const TweetInReplyTo = memo(({ tweet }: { tweet: EnrichedTweet }) => (
- <a
- href={tweet.in_reply_to_url}
- className={tweetInReplyToStyles.root}
- target="_blank"
- rel="noopener noreferrer"
- >
- Replying to @{tweet.in_reply_to_screen_name}
- </a>
-));
-
-export const TweetBody = memo(({ tweet }: { tweet: EnrichedTweet }) => {
- const renderEntity = useCallback((item: any, i: number) => {
- switch (item.type) {
- case "hashtag":
- case "mention":
- case "url":
- case "symbol":
- return (
- <TweetLink key={i} href={item.href}>
- {item.text}
- </TweetLink>
- );
- case "media":
- return;
- default:
- return <span key={i} dangerouslySetInnerHTML={{ __html: item.text }} />;
- }
- }, []);
-
- return (
- <p className={tweetBodyStyles.root} lang={tweet.lang} dir="auto">
- {tweet.entities.map(renderEntity)}
- </p>
- );
-});
-
-export const TweetLink = memo(({ href, children }: { children: ReactNode; href: string }) => (
- <a
- href={href}
- className={tweetLinkStyles.root}
- target="_blank"
- rel="noopener noreferrer nofollow"
- >
- {children}
- </a>
-));
-
-export const CustomTwitterComp = memo(({ tweet: t, components }: Props) => {
- const [tweet, setTweet] = useState<EnrichedTweet | null>(null);
- const [theme] = useTheme();
-
- useEffect(() => {
- console.log("t", t);
- if ("error" in t) {
- setTweet(null);
- } else {
- try {
- const enrichedTweet = enrichTweet(t);
- setTweet(enrichedTweet);
- } catch (error) {
- console.error("Failed to enrich tweet:", error);
- setTweet(null);
- }
- }
- }, [t]);
-
- const tweetId = useMemo(
- () => (tweet && typeof tweet === "object" ? `tweet-${tweet?.id_str}-${theme}` : ""),
- [tweet?.id_str, theme],
- );
-
- if (typeof tweet === "undefined" || tweet === null) {
- return <ReactTweet.TweetSkeleton />;
- }
-
- if ("error" in t) {
- return <div>{"Failed to load tweet"}</div>;
- }
-
- return (
- <div className="" id={tweetId}>
- <TweetContainer className="!z-0 !m-0 w-full bg-transparent !p-0">
- <TweetHeader tweet={tweet} components={components} />
- {tweet.in_reply_to_status_id_str && <TweetInReplyTo tweet={tweet} />}
- <TweetBody tweet={tweet} />
- {tweet.mediaDetails?.length ? <TweetMedia tweet={tweet} components={components} /> : null}
- {tweet.quoted_tweet && <QuotedTweet tweet={tweet.quoted_tweet} />}
- </TweetContainer>
- </div>
- );
-});
diff --git a/apps/web/app/components/twitter/tweet-body.module.css b/apps/web/app/components/twitter/tweet-body.module.css
deleted file mode 100644
index 771f657c..00000000
--- a/apps/web/app/components/twitter/tweet-body.module.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.root {
- font-size: var(--tweet-body-font-size);
- font-weight: var(--tweet-body-font-weight);
- line-height: var(--tweet-body-line-height);
- margin: var(--tweet-body-margin);
- overflow-wrap: break-word;
- white-space: pre-wrap;
-}
diff --git a/apps/web/app/components/twitter/tweet-container.module.css b/apps/web/app/components/twitter/tweet-container.module.css
deleted file mode 100644
index 79cf90fb..00000000
--- a/apps/web/app/components/twitter/tweet-container.module.css
+++ /dev/null
@@ -1,25 +0,0 @@
-.root {
- width: 100%;
- min-width: 250px;
- max-width: 550px;
- overflow: hidden;
- /* Base font styles */
- color: var(--tweet-font-color);
- font-family: var(--tweet-font-family);
- font-weight: 400;
- box-sizing: border-box;
- border: var(--tweet-border);
- border-radius: 24px;
- margin: var(--tweet-container-margin);
- background-color: var(--tweet-bg-color);
- transition-property: background-color, box-shadow;
- transition-duration: 0.2s;
-}
-.root:hover {
- background-color: var(--tweet-bg-color-hover);
-}
-.article {
- position: relative;
- box-sizing: inherit;
- padding: 0.75rem 1rem;
-}
diff --git a/apps/web/app/components/twitter/tweet-header.module.css b/apps/web/app/components/twitter/tweet-header.module.css
deleted file mode 100644
index 38a053dd..00000000
--- a/apps/web/app/components/twitter/tweet-header.module.css
+++ /dev/null
@@ -1,96 +0,0 @@
-.header {
- display: flex;
- padding-bottom: 0.75rem;
- line-height: var(--tweet-header-line-height);
- font-size: var(--tweet-header-font-size);
- white-space: nowrap;
- overflow-wrap: break-word;
- overflow: hidden;
-}
-
-.avatar {
- position: relative;
- height: 48px;
- width: 48px;
-}
-.avatarOverflow {
- height: 100%;
- width: 100%;
- position: absolute;
- overflow: hidden;
- border-radius: 9999px;
-}
-.avatarSquare {
- border-radius: 4px;
-}
-.avatarShadow {
- height: 100%;
- width: 100%;
- transition-property: background-color;
- transition-duration: 0.2s;
- box-shadow: rgb(0 0 0 / 3%) 0px 0px 2px inset;
-}
-.avatarShadow:hover {
- background-color: rgba(26, 26, 26, 0.15);
-}
-
-.author {
- max-width: calc(100% - 84px);
- display: flex;
- flex-direction: column;
- justify-content: center;
- margin: 0 0.5rem;
-}
-.authorLink {
- text-decoration: none;
- color: inherit;
- display: flex;
- align-items: center;
-}
-.authorLink:hover {
- text-decoration-line: underline;
-}
-.authorVerified {
- display: inline-flex;
-}
-.authorLinkText {
- font-weight: 700;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
-}
-
-.authorMeta {
- display: flex;
-}
-.authorFollow {
- display: flex;
-}
-.username {
- color: var(--tweet-font-color-secondary);
- text-decoration: none;
- text-overflow: ellipsis;
-}
-.follow {
- color: var(--tweet-color-blue-secondary);
- text-decoration: none;
- font-weight: 700;
-}
-.follow:hover {
- text-decoration-line: underline;
-}
-.separator {
- padding: 0 0.25rem;
-}
-
-.brand {
- margin-inline-start: auto;
-}
-
-.twitterIcon {
- width: 23.75px;
- height: 23.75px;
- color: var(--tweet-twitter-icon-color);
- fill: currentColor;
- user-select: none;
-}
diff --git a/apps/web/app/components/twitter/tweet-in-reply-to.module.css b/apps/web/app/components/twitter/tweet-in-reply-to.module.css
deleted file mode 100644
index 672c97cd..00000000
--- a/apps/web/app/components/twitter/tweet-in-reply-to.module.css
+++ /dev/null
@@ -1,13 +0,0 @@
-.root {
- text-decoration: none;
- color: var(--tweet-font-color-secondary);
- font-size: 0.9375rem;
- line-height: 1.25rem;
- margin-bottom: 0.25rem;
- overflow-wrap: break-word;
- white-space: pre-wrap;
-}
-.root:hover {
- text-decoration-thickness: 1px;
- text-decoration-line: underline;
-}
diff --git a/apps/web/app/components/twitter/tweet-link.module.css b/apps/web/app/components/twitter/tweet-link.module.css
deleted file mode 100644
index 241e2b95..00000000
--- a/apps/web/app/components/twitter/tweet-link.module.css
+++ /dev/null
@@ -1,10 +0,0 @@
-.root {
- font-weight: inherit;
- color: var(--tweet-color-blue-secondary);
- text-decoration: none;
- cursor: pointer;
-}
-.root:hover {
- text-decoration-thickness: 1px;
- text-decoration-line: underline;
-}
diff --git a/apps/web/app/components/twitter/tweet-media-video.module.css b/apps/web/app/components/twitter/tweet-media-video.module.css
deleted file mode 100644
index 4314593f..00000000
--- a/apps/web/app/components/twitter/tweet-media-video.module.css
+++ /dev/null
@@ -1,70 +0,0 @@
-.anchor {
- display: flex;
- align-items: center;
- color: white;
- padding: 0 1rem;
- border: 1px solid transparent;
- border-radius: 9999px;
- font-weight: 700;
- transition: background-color 0.2s;
- cursor: pointer;
- user-select: none;
- outline-style: none;
- text-decoration: none;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.videoButton {
- position: relative;
- height: 67px;
- width: 67px;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: var(--tweet-color-blue-primary);
- transition-property: background-color;
- transition-duration: 0.2s;
- border: 4px solid #fff;
- border-radius: 9999px;
- cursor: pointer;
-}
-.videoButton:hover,
-.videoButton:focus-visible {
- background-color: var(--tweet-color-blue-primary-hover);
-}
-.videoButtonIcon {
- margin-left: 3px;
- width: calc(50% + 4px);
- height: calc(50% + 4px);
- max-width: 100%;
- color: #fff;
- fill: currentColor;
- user-select: none;
-}
-.watchOnTwitter {
- position: absolute;
- top: 12px;
- right: 8px;
-}
-.watchOnTwitter > a {
- min-width: 2rem;
- min-height: 2rem;
- font-size: 0.875rem;
- line-height: 1rem;
- backdrop-filter: blur(4px);
- background-color: rgba(15, 20, 25, 0.75);
-}
-.watchOnTwitter > a:hover {
- background-color: rgba(39, 44, 48, 0.75);
-}
-.viewReplies {
- position: relative;
- min-height: 2rem;
- background-color: var(--tweet-color-blue-primary);
- border-color: var(--tweet-color-blue-primary);
- font-size: 0.9375rem;
- line-height: 1.25rem;
-}
-.viewReplies:hover {
- background-color: var(--tweet-color-blue-primary-hover);
-}
diff --git a/apps/web/app/components/twitter/tweet-media-video.tsx b/apps/web/app/components/twitter/tweet-media-video.tsx
deleted file mode 100644
index a1601b9b..00000000
--- a/apps/web/app/components/twitter/tweet-media-video.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { EnrichedQuotedTweet, type EnrichedTweet, getMediaUrl, getMp4Video } from "react-tweet";
-import type { MediaAnimatedGif, MediaVideo } from "react-tweet/api";
-
-import s from "./tweet-media-video.module.css";
-import mediaStyles from "./tweet-media.module.css";
-
-import clsx from "clsx";
-
-type Props = {
- tweet: EnrichedTweet | EnrichedQuotedTweet;
- media: MediaAnimatedGif | MediaVideo;
-};
-
-export const TweetMediaVideo = ({ tweet, media }: Props) => {
- const [playButton, setPlayButton] = useState(true);
- const [isPlaying, setIsPlaying] = useState(false);
- const [ended, setEnded] = useState(false);
- const mp4Video = getMp4Video(media);
- let timeout = 0;
-
- return (
- <>
- <video
- className={mediaStyles.image}
- poster={getMediaUrl(media, "small")}
- controls={!playButton}
- muted
- preload="none"
- tabIndex={playButton ? -1 : 0}
- onPlay={() => {
- if (timeout) window.clearTimeout(timeout);
- if (!isPlaying) setIsPlaying(true);
- if (ended) setEnded(false);
- }}
- onPause={() => {
- // When the video is seeked (moved to a different timestamp), it will pause for a moment
- // before resuming. We don't want to show the message in that case so we wait a bit.
- if (timeout) window.clearTimeout(timeout);
- timeout = window.setTimeout(() => {
- if (isPlaying) setIsPlaying(false);
- timeout = 0;
- }, 100);
- }}
- onEnded={() => {
- setEnded(true);
- }}
- >
- <source src={mp4Video.url} type={mp4Video.content_type} />
- </video>
-
- {playButton && (
- <button
- type="button"
- className={s.videoButton}
- aria-label="View video on X"
- onClick={(e) => {
- const video = e.currentTarget.previousSibling as HTMLMediaElement;
-
- e.preventDefault();
- setPlayButton(false);
- setIsPlaying(true);
- video.play();
- video.focus();
- }}
- >
- <svg viewBox="0 0 24 24" className={s.videoButtonIcon} aria-hidden="true">
- <g>
- <path d="M21 12L4 2v20l17-10z"></path>
- </g>
- </svg>
- </button>
- )}
-
- {!isPlaying && !ended && (
- <div className={s.watchOnTwitter}>
- <a href={tweet.url} className={s.anchor} target="_blank" rel="noopener noreferrer">
- {playButton ? "Watch on X" : "Continue watching on X"}
- </a>
- </div>
- )}
-
- {ended && (
- <a
- href={tweet.url}
- className={clsx(s.anchor, s.viewReplies)}
- target="_blank"
- rel="noopener noreferrer"
- >
- View replies
- </a>
- )}
- </>
- );
-};
diff --git a/apps/web/app/components/twitter/tweet-media.module.css b/apps/web/app/components/twitter/tweet-media.module.css
deleted file mode 100644
index 375461e6..00000000
--- a/apps/web/app/components/twitter/tweet-media.module.css
+++ /dev/null
@@ -1,53 +0,0 @@
-.root {
- margin-top: 0.75rem;
- overflow: hidden;
- position: relative;
-}
-.rounded {
- border: var(--tweet-border);
- border-radius: 12px;
-}
-.mediaWrapper {
- display: grid;
- grid-auto-rows: 1fr;
- gap: 2px;
- height: 100%;
- width: 100%;
-}
-.grid2Columns {
- grid-template-columns: repeat(2, 1fr);
-}
-.grid3 > a:first-child {
- grid-row: span 2;
-}
-.grid2x2 {
- grid-template-rows: repeat(2, 1fr);
-}
-.mediaContainer {
- position: relative;
- height: 100%;
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-.mediaLink {
- text-decoration: none;
- outline-style: none;
-}
-.skeleton {
- padding-bottom: 56.25%;
- width: 100%;
- display: block;
-}
-.image {
- position: absolute;
- top: 0px;
- left: 0px;
- bottom: 0px;
- height: 100%;
- width: 100%;
- margin: 0;
- object-fit: cover;
- object-position: center;
-}
diff --git a/apps/web/app/components/twitter/tweet-media.tsx b/apps/web/app/components/twitter/tweet-media.tsx
deleted file mode 100644
index fe087b66..00000000
--- a/apps/web/app/components/twitter/tweet-media.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import * as ReactTweet from "react-tweet";
-import { Fragment } from "react";
-import { type EnrichedQuotedTweet, type EnrichedTweet , TwitterComponents } from "react-tweet";
-import { MediaDetails } from "react-tweet/api";
-
-import s from "./tweet-media.module.css";
-
-import clsx from "clsx";
-
-const { getMediaUrl, TweetMediaVideo } = ReactTweet;
-
-type MediaImgProps = {
- src: string;
- alt: string;
- className?: string;
- draggable?: boolean;
-};
-
-// eslint-disable-next-line jsx-a11y/alt-text -- The alt text is part of `...props`
-export const MediaImg = (props: MediaImgProps) => <img {...props} />;
-
-const getSkeletonStyle = (media: MediaDetails, itemCount: number) => {
- let paddingBottom = 56.25; // default of 16x9
-
- // if we only have 1 item, show at original ratio
- if (itemCount === 1)
- paddingBottom = (100 / media.original_info.width) * media.original_info.height;
-
- // if we have 2 items, double the default to be 16x9 total
- if (itemCount === 2) paddingBottom = paddingBottom * 2;
-
- return {
- width: media.type === "photo" ? undefined : "unset",
- paddingBottom: `${paddingBottom}%`,
- };
-};
-
-type Props = {
- tweet: EnrichedTweet | EnrichedQuotedTweet;
- components?: TwitterComponents;
- quoted?: boolean;
-};
-
-export const TweetMedia = ({ tweet, components, quoted }: Props) => {
- const length = tweet.mediaDetails?.length ?? 0;
- const Img = components?.MediaImg ?? MediaImg;
-
- return (
- <div className={clsx(s.root, !quoted && s.rounded)}>
- <div
- className={clsx(
- s.mediaWrapper,
- length > 1 && s.grid2Columns,
- length === 3 && s.grid3,
- length > 4 && s.grid2x2,
- )}
- >
- {tweet.mediaDetails?.map((media) => (
- <Fragment key={media.media_url_https}>
- {media.type === "photo" ? (
- <a
- key={media.media_url_https}
- href={tweet.url}
- className={clsx(s.mediaContainer, s.mediaLink)}
- target="_blank"
- rel="noopener noreferrer"
- >
- <div className={s.skeleton} style={getSkeletonStyle(media, length)} />
- <Img
- src={getMediaUrl(media, "small")}
- alt={media.ext_alt_text || "Image"}
- className={s.image}
- draggable
- />
- </a>
- ) : (
- <div key={media.media_url_https} className={s.mediaContainer}>
- <div className={s.skeleton} style={getSkeletonStyle(media, length)} />
- <TweetMediaVideo tweet={tweet} media={media} />
- </div>
- )}
- </Fragment>
- ))}
- </div>
- </div>
- );
-};
diff --git a/apps/web/app/components/twitter/verified-badge.module.css b/apps/web/app/components/twitter/verified-badge.module.css
deleted file mode 100644
index 55537b1c..00000000
--- a/apps/web/app/components/twitter/verified-badge.module.css
+++ /dev/null
@@ -1,10 +0,0 @@
-.verifiedOld {
- color: var(--tweet-verified-old-color);
-}
-.verifiedBlue {
- color: var(--tweet-verified-blue-color);
-}
-.verifiedGovernment {
- /* color: var(--tweet-verified-government-color); */
- color: rgb(130, 154, 171);
-}
diff --git a/apps/web/app/components/twitter/verified-badge.tsx b/apps/web/app/components/twitter/verified-badge.tsx
deleted file mode 100644
index a9dabbe4..00000000
--- a/apps/web/app/components/twitter/verified-badge.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { Verified, VerifiedBusiness, VerifiedGovernment } from "./icons/index";
-import s from "./verified-badge.module.css";
-
-import clsx from "clsx";
-
-type Props = {
- user: any;
- className?: string;
-};
-
-export const VerifiedBadge = ({ user, className }: Props) => {
- const verified = user.verified || user.is_blue_verified || user.verified_type;
- let icon = <Verified />;
- let iconClassName: string | null = s.verifiedBlue ?? null;
-
- if (verified) {
- if (!user.is_blue_verified) {
- iconClassName = s.verifiedOld!;
- }
- switch (user.verified_type) {
- case "Government":
- icon = <VerifiedGovernment />;
- iconClassName = s.verifiedGovernment!;
- break;
- case "Business":
- icon = <VerifiedBusiness />;
- iconClassName = null;
- break;
- }
- }
-
- return verified ? <div className={clsx(className, iconClassName)}>{icon}</div> : null;
-};
diff --git a/apps/web/app/components/ui/Loader.tsx b/apps/web/app/components/ui/Loader.tsx
deleted file mode 100644
index bbac6b0b..00000000
--- a/apps/web/app/components/ui/Loader.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-
-function Loader() {
- return (
- <div className="loader">
- {/* Example Loader: Spinner */}
- <svg className="animate-spin h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
- <path
- className="opacity-75"
- fill="currentColor"
- d="M4 12a8 8 0 018-8v8H4z"
- ></path>
- </svg>
- </div>
- );
-}
-
-export default Loader;
diff --git a/apps/web/app/components/ui/avatar.tsx b/apps/web/app/components/ui/avatar.tsx
deleted file mode 100644
index 706f1778..00000000
--- a/apps/web/app/components/ui/avatar.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import * as React from "react"
-import * as AvatarPrimitive from "@radix-ui/react-avatar"
-
-import { cn } from "~/lib/utils"
-
-const Avatar = React.forwardRef<
- React.ElementRef<typeof AvatarPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
->(({ className, ...props }, ref) => (
- <AvatarPrimitive.Root
- ref={ref}
- className={cn(
- "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
- className
- )}
- {...props}
- />
-))
-Avatar.displayName = AvatarPrimitive.Root.displayName
-
-const AvatarImage = React.forwardRef<
- React.ElementRef<typeof AvatarPrimitive.Image>,
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
->(({ className, ...props }, ref) => (
- <AvatarPrimitive.Image
- ref={ref}
- className={cn("aspect-square h-full w-full", className)}
- {...props}
- />
-))
-AvatarImage.displayName = AvatarPrimitive.Image.displayName
-
-const AvatarFallback = React.forwardRef<
- React.ElementRef<typeof AvatarPrimitive.Fallback>,
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
->(({ className, ...props }, ref) => (
- <AvatarPrimitive.Fallback
- ref={ref}
- className={cn(
- "flex h-full w-full items-center justify-center rounded-full bg-muted",
- className
- )}
- {...props}
- />
-))
-AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
-
-export { Avatar, AvatarImage, AvatarFallback }
diff --git a/apps/web/app/components/ui/badge.tsx b/apps/web/app/components/ui/badge.tsx
deleted file mode 100644
index 5e2b7aca..00000000
--- a/apps/web/app/components/ui/badge.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "~/lib/utils"
-
-const badgeVariants = cva(
- "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
- {
- variants: {
- variant: {
- default:
- "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
- secondary:
- "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
- destructive:
- "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
- outline: "text-foreground",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-)
-
-export interface BadgeProps
- extends React.HTMLAttributes<HTMLDivElement>,
- VariantProps<typeof badgeVariants> {}
-
-function Badge({ className, variant, ...props }: BadgeProps) {
- return (
- <div className={cn(badgeVariants({ variant }), className)} {...props} />
- )
-}
-
-export { Badge, badgeVariants }
diff --git a/apps/web/app/components/ui/button.tsx b/apps/web/app/components/ui/button.tsx
deleted file mode 100644
index 1425c6b6..00000000
--- a/apps/web/app/components/ui/button.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import * as React from "react";
-
-import { Slot } from "@radix-ui/react-slot";
-import { type VariantProps, cva } from "class-variance-authority";
-import { cn } from "~/lib/utils";
-
-const buttonVariants = cva(
- "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
- {
- variants: {
- variant: {
- default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
- destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
- outline:
- "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
- secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
- ghost: "hover:bg-accent hover:text-accent-foreground",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default: "h-9 px-4 py-2",
- sm: "h-8 rounded-md px-3 text-xs",
- lg: "h-10 rounded-md px-8",
- icon: "h-9 w-9",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- },
-);
-
-export interface ButtonProps
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
- VariantProps<typeof buttonVariants> {
- asChild?: boolean;
-}
-
-const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
- ({ className, variant, size, asChild = false, onClick, ...props }, ref) => {
- const Comp = asChild ? Slot : "button";
-
- // Memoize the onClick handler to prevent unnecessary re-renders
- const memoizedOnClick = React.useCallback(onClick ?? (() => {}), [onClick]);
-
- return (
- <Comp
- className={cn(buttonVariants({ variant, size, className }))}
- ref={ref}
- onClick={memoizedOnClick}
- {...props}
- />
- );
- },
-);
-Button.displayName = "Button";
-
-export { Button, buttonVariants };
diff --git a/apps/web/app/components/ui/card.tsx b/apps/web/app/components/ui/card.tsx
deleted file mode 100644
index 874ce09d..00000000
--- a/apps/web/app/components/ui/card.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import * as React from "react"
-
-import { cn } from "~/lib/utils"
-
-const Card = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn(
- "rounded-xl border bg-card text-card-foreground shadow",
- className
- )}
- {...props}
- />
-))
-Card.displayName = "Card"
-
-const CardHeader = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn("flex flex-col space-y-1.5 p-6", className)}
- {...props}
- />
-))
-CardHeader.displayName = "CardHeader"
-
-const CardTitle = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn("font-semibold leading-none tracking-tight", className)}
- {...props}
- />
-))
-CardTitle.displayName = "CardTitle"
-
-const CardDescription = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn("text-sm text-muted-foreground", className)}
- {...props}
- />
-))
-CardDescription.displayName = "CardDescription"
-
-const CardContent = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
- <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
-))
-CardContent.displayName = "CardContent"
-
-const CardFooter = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn("flex items-center p-6 pt-0", className)}
- {...props}
- />
-))
-CardFooter.displayName = "CardFooter"
-
-export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/apps/web/app/components/ui/checkbox.tsx b/apps/web/app/components/ui/checkbox.tsx
deleted file mode 100644
index 25d39143..00000000
--- a/apps/web/app/components/ui/checkbox.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-"use client";
-
-import * as React from "react";
-
-import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
-import { Check } from "lucide-react";
-import { cn } from "~/lib/utils";
-
-const Checkbox = React.forwardRef<
- React.ElementRef<typeof CheckboxPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
->(({ className, ...props }, ref) => (
- <CheckboxPrimitive.Root
- ref={ref}
- className={cn(
- "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
- className,
- )}
- {...props}
- >
- <CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
- <Check className="h-4 w-4" />
- </CheckboxPrimitive.Indicator>
- </CheckboxPrimitive.Root>
-));
-Checkbox.displayName = CheckboxPrimitive.Root.displayName;
-
-export { Checkbox };
diff --git a/apps/web/app/components/ui/combobox.tsx b/apps/web/app/components/ui/combobox.tsx
deleted file mode 100644
index 94762db4..00000000
--- a/apps/web/app/components/ui/combobox.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-"use client";
-
-import * as React from "react";
-
-import { Check, ChevronsUpDown, Plus } from "lucide-react";
-import { Button } from "~/components/ui/button";
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
-} from "~/components/ui/command";
-import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
-import { cn } from "~/lib/utils";
-
-export function Combobox({
- options,
- value,
- setValue,
- placeholder,
- createNew,
- className,
-}: {
- options: {
- value: string;
- label: string;
- }[];
- value: string;
- setValue: (value: string) => void;
- placeholder?: string;
- createNew?: {
- createAction: (query: string) => Promise<void>;
- createLabel: string;
- };
- className?: string;
-}) {
- const [open, setOpen] = React.useState(false);
- const [query, setQuery] = React.useState("");
-
- const handleCreate = async () => {
- if (createNew) {
- await createNew.createAction(query);
- setOpen(false);
- setQuery("");
- }
- };
-
- const handleSelect = React.useCallback(
- (currentValue: string) => {
- setValue(currentValue === value ? "" : currentValue);
- setOpen(false);
- },
- [value, setValue],
- );
-
- return (
- <Popover open={open} onOpenChange={setOpen}>
- <PopoverTrigger asChild>
- <Button
- variant="ghost"
- role="combobox"
- aria-expanded={open}
- className={cn("w-[200px] justify-between", className)}
- >
- {value ? options.find((option) => option.value === value)?.label : placeholder}
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-[200px] p-0">
- <Command>
- <CommandInput placeholder={placeholder} onValueChange={setQuery} />
- <CommandList>
- <CommandEmpty>
- {createNew && (
- <CommandItem onSelect={handleCreate}>
- <Plus className="mr-2 h-4 w-4" />
- {createNew.createLabel.replace("{query}", query)}
- </CommandItem>
- )}
- {!createNew && "No items found."}
- </CommandEmpty>
- <CommandGroup>
- {options.map((option) => (
- <CommandItem key={option.value} value={option.value} onSelect={handleSelect}>
- <Check
- className={cn(
- "mr-2 h-4 w-4",
- value === option.value ? "opacity-100" : "opacity-0",
- )}
- />
- {option.label}
- </CommandItem>
- ))}
- </CommandGroup>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- );
-}
diff --git a/apps/web/app/components/ui/command.tsx b/apps/web/app/components/ui/command.tsx
deleted file mode 100644
index 113a5545..00000000
--- a/apps/web/app/components/ui/command.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import * as React from "react";
-
-import { type DialogProps } from "@radix-ui/react-dialog";
-import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
-import { Command as CommandPrimitive } from "cmdk";
-import { Dialog, DialogContent } from "~/components/ui/dialog";
-import { cn } from "~/lib/utils";
-
-const Command = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive>
->(({ className, ...props }, ref) => (
- <CommandPrimitive
- ref={ref}
- className={cn(
- "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
- className,
- )}
- {...props}
- />
-));
-Command.displayName = CommandPrimitive.displayName;
-
-interface CommandDialogProps extends DialogProps {}
-
-const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
- return (
- <Dialog {...props}>
- <DialogContent className="overflow-hidden p-0">
- <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
- {children}
- </Command>
- </DialogContent>
- </Dialog>
- );
-};
-
-const CommandInput = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Input>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
->(({ className, ...props }, ref) => (
- <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
- <MagnifyingGlassIcon className="mr-2 h-4 w-4 shrink-0 opacity-50" />
- <CommandPrimitive.Input
- ref={ref}
- className={cn(
- "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
- className,
- )}
- {...props}
- />
- </div>
-));
-
-CommandInput.displayName = CommandPrimitive.Input.displayName;
-
-const CommandList = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.List>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
->(({ className, ...props }, ref) => (
- <CommandPrimitive.List
- ref={ref}
- className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
- {...props}
- />
-));
-
-CommandList.displayName = CommandPrimitive.List.displayName;
-
-const CommandEmpty = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Empty>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
->((props, ref) => (
- <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
-));
-
-CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
-
-const CommandGroup = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Group>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
->(({ className, ...props }, ref) => (
- <CommandPrimitive.Group
- ref={ref}
- className={cn(
- "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
- className,
- )}
- {...props}
- />
-));
-
-CommandGroup.displayName = CommandPrimitive.Group.displayName;
-
-const CommandSeparator = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Separator>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
->(({ className, ...props }, ref) => (
- <CommandPrimitive.Separator
- ref={ref}
- className={cn("-mx-1 h-px bg-border", className)}
- {...props}
- />
-));
-CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
-
-const CommandItem = React.forwardRef<
- React.ElementRef<typeof CommandPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
->(({ className, ...props }, ref) => (
- <CommandPrimitive.Item
- ref={ref}
- className={cn(
- "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
- className,
- )}
- {...props}
- />
-));
-
-CommandItem.displayName = CommandPrimitive.Item.displayName;
-
-const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
- return (
- <span
- className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
- {...props}
- />
- );
-};
-CommandShortcut.displayName = "CommandShortcut";
-
-export {
- Command,
- CommandDialog,
- CommandInput,
- CommandList,
- CommandEmpty,
- CommandGroup,
- CommandItem,
- CommandShortcut,
- CommandSeparator,
-};
diff --git a/apps/web/app/components/ui/credenza.tsx b/apps/web/app/components/ui/credenza.tsx
deleted file mode 100644
index 5923e8f1..00000000
--- a/apps/web/app/components/ui/credenza.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-"use client";
-
-import * as React from "react";
-
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "~/components/ui/dialog";
-import {
- Drawer,
- DrawerClose,
- DrawerContent,
- DrawerDescription,
- DrawerFooter,
- DrawerHeader,
- DrawerTitle,
- DrawerTrigger,
-} from "~/components/ui/drawer";
-import { useMediaQuery } from "~/lib/hooks/use-media-query";
-import { cn } from "~/lib/utils";
-
-interface BaseProps {
- children: React.ReactNode;
-}
-
-interface RootCredenzaProps extends BaseProps {
- open?: boolean;
- onOpenChange?: (open: boolean) => void;
-}
-
-interface CredenzaProps extends BaseProps {
- className?: string;
- asChild?: true;
-}
-
-const desktop = "(min-width: 768px)";
-
-const Credenza = ({ children, ...props }: RootCredenzaProps) => {
- const isDesktop = useMediaQuery(desktop);
- const Credenza = isDesktop ? Dialog : Drawer;
-
- return <Credenza {...props}>{children}</Credenza>;
-};
-
-const CredenzaTrigger = ({ className, children, ...props }: CredenzaProps) => {
- const isDesktop = useMediaQuery(desktop);
- const CredenzaTrigger = isDesktop ? DialogTrigger : DrawerTrigger;
-
- return (
- <CredenzaTrigger className={className} {...props}>
- {children}
- </CredenzaTrigger>
- );
-};
-
-const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
- const isDesktop = useMediaQuery(desktop);
- const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
-
- return (
- <CredenzaClose className={className} {...props}>
- {children}
- </CredenzaClose>
- );
-};
-
-const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
- const isDesktop = useMediaQuery(desktop);
- const CredenzaContent = isDesktop ? DialogContent : DrawerContent;
-
- return (
- <CredenzaContent className={className} {...props}>
- {children}
- </CredenzaContent>
- );
-};
-
-const CredenzaDescription = ({ className, children, ...props }: CredenzaProps) => {
- const isDesktop = useMediaQuery(desktop);
- const CredenzaDescription = isDesktop ? DialogDescription : DrawerDescription;
-
- return (
- <CredenzaDescription className={className} {...props}>
- {children}
- </CredenzaDescription>
- );
-};
-
-const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
- const isDesktop = useMediaQuery(desktop);
- const CredenzaHeader = isDesktop ? DialogHeader : DrawerHeader;
-
- return (
- <CredenzaHeader className={className} {...props}>
- {children}
- </CredenzaHeader>
- );
-};
-
-const CredenzaTitle = ({ className, children, ...props }: CredenzaProps) => {
- const isDesktop = useMediaQuery(desktop);
- const CredenzaTitle = isDesktop ? DialogTitle : DrawerTitle;
-
- return (
- <CredenzaTitle className={className} {...props}>
- {children}
- </CredenzaTitle>
- );
-};
-
-const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
- return (
- <div className={cn("px-4 md:px-0", className)} {...props}>
- {children}
- </div>
- );
-};
-
-const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
- const isDesktop = useMediaQuery(desktop);
- const CredenzaFooter = isDesktop ? DialogFooter : DrawerFooter;
-
- return (
- <CredenzaFooter className={className} {...props}>
- {children}
- </CredenzaFooter>
- );
-};
-
-export {
- Credenza,
- CredenzaTrigger,
- CredenzaClose,
- CredenzaContent,
- CredenzaDescription,
- CredenzaHeader,
- CredenzaTitle,
- CredenzaBody,
- CredenzaFooter,
-};
diff --git a/apps/web/app/components/ui/dialog.tsx b/apps/web/app/components/ui/dialog.tsx
deleted file mode 100644
index 3e7b6e56..00000000
--- a/apps/web/app/components/ui/dialog.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import * as React from "react";
-
-import * as DialogPrimitive from "@radix-ui/react-dialog";
-import { Cross2Icon } from "@radix-ui/react-icons";
-import { cn } from "~/lib/utils";
-
-const Dialog = DialogPrimitive.Root;
-
-const DialogTrigger = DialogPrimitive.Trigger;
-
-const DialogPortal = DialogPrimitive.Portal;
-
-const DialogClose = DialogPrimitive.Close;
-
-const DialogOverlay = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Overlay>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
->(({ className, ...props }, ref) => (
- <DialogPrimitive.Overlay
- ref={ref}
- className={cn(
- "fixed inset-0 z-50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
- className,
- )}
- {...props}
- />
-));
-DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
-
-const DialogContent = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
->(({ className, children, ...props }, ref) => (
- <DialogPortal>
- <DialogOverlay />
- <DialogPrimitive.Content
- ref={ref}
- className={cn(
- "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
- className,
- )}
- {...props}
- >
- {children}
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
- <Cross2Icon className="h-4 w-4" />
- <span className="sr-only">Close</span>
- </DialogPrimitive.Close>
- </DialogPrimitive.Content>
- </DialogPortal>
-));
-DialogContent.displayName = DialogPrimitive.Content.displayName;
-
-const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
- <div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
-);
-DialogHeader.displayName = "DialogHeader";
-
-const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
- {...props}
- />
-);
-DialogFooter.displayName = "DialogFooter";
-
-const DialogTitle = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Title>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
->(({ className, ...props }, ref) => (
- <DialogPrimitive.Title
- ref={ref}
- className={cn("text-lg font-semibold leading-none tracking-tight", className)}
- {...props}
- />
-));
-DialogTitle.displayName = DialogPrimitive.Title.displayName;
-
-const DialogDescription = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Description>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
->(({ className, ...props }, ref) => (
- <DialogPrimitive.Description
- ref={ref}
- className={cn("text-sm text-muted-foreground", className)}
- {...props}
- />
-));
-DialogDescription.displayName = DialogPrimitive.Description.displayName;
-
-export {
- Dialog,
- DialogPortal,
- DialogOverlay,
- DialogTrigger,
- DialogClose,
- DialogContent,
- DialogHeader,
- DialogFooter,
- DialogTitle,
- DialogDescription,
-};
diff --git a/apps/web/app/components/ui/drawer.tsx b/apps/web/app/components/ui/drawer.tsx
deleted file mode 100644
index 6825edf8..00000000
--- a/apps/web/app/components/ui/drawer.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import * as React from "react"
-import { Drawer as DrawerPrimitive } from "vaul"
-
-import { cn } from "~/lib/utils"
-
-const Drawer = ({
- shouldScaleBackground = true,
- ...props
-}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
- <DrawerPrimitive.Root
- shouldScaleBackground={shouldScaleBackground}
- {...props}
- />
-)
-Drawer.displayName = "Drawer"
-
-const DrawerTrigger = DrawerPrimitive.Trigger
-
-const DrawerPortal = DrawerPrimitive.Portal
-
-const DrawerClose = DrawerPrimitive.Close
-
-const DrawerOverlay = React.forwardRef<
- React.ElementRef<typeof DrawerPrimitive.Overlay>,
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
->(({ className, ...props }, ref) => (
- <DrawerPrimitive.Overlay
- ref={ref}
- className={cn("fixed inset-0 z-50 backdrop-blur-sm", className)}
- {...props}
- />
-))
-DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
-
-const DrawerContent = React.forwardRef<
- React.ElementRef<typeof DrawerPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
->(({ className, children, ...props }, ref) => (
- <DrawerPortal>
- <DrawerOverlay />
- <DrawerPrimitive.Content
- ref={ref}
- className={cn(
- "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
- className
- )}
- {...props}
- >
- <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
- {children}
- </DrawerPrimitive.Content>
- </DrawerPortal>
-))
-DrawerContent.displayName = "DrawerContent"
-
-const DrawerHeader = ({
- className,
- ...props
-}: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
- {...props}
- />
-)
-DrawerHeader.displayName = "DrawerHeader"
-
-const DrawerFooter = ({
- className,
- ...props
-}: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn("mt-auto flex flex-col gap-2 p-4", className)}
- {...props}
- />
-)
-DrawerFooter.displayName = "DrawerFooter"
-
-const DrawerTitle = React.forwardRef<
- React.ElementRef<typeof DrawerPrimitive.Title>,
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
->(({ className, ...props }, ref) => (
- <DrawerPrimitive.Title
- ref={ref}
- className={cn(
- "text-lg font-semibold leading-none tracking-tight",
- className
- )}
- {...props}
- />
-))
-DrawerTitle.displayName = DrawerPrimitive.Title.displayName
-
-const DrawerDescription = React.forwardRef<
- React.ElementRef<typeof DrawerPrimitive.Description>,
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
->(({ className, ...props }, ref) => (
- <DrawerPrimitive.Description
- ref={ref}
- className={cn("text-sm text-muted-foreground", className)}
- {...props}
- />
-))
-DrawerDescription.displayName = DrawerPrimitive.Description.displayName
-
-export {
- Drawer,
- DrawerPortal,
- DrawerOverlay,
- DrawerTrigger,
- DrawerClose,
- DrawerContent,
- DrawerHeader,
- DrawerFooter,
- DrawerTitle,
- DrawerDescription,
-}
diff --git a/apps/web/app/components/ui/dropdown-menu.tsx b/apps/web/app/components/ui/dropdown-menu.tsx
deleted file mode 100644
index 4c9ce104..00000000
--- a/apps/web/app/components/ui/dropdown-menu.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
-import * as React from "react"
-import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
-import {
- CheckIcon,
- ChevronRightIcon,
- DotFilledIcon,
-} from "@radix-ui/react-icons"
-
-import { cn } from "~/lib/utils"
-
-const DropdownMenu = DropdownMenuPrimitive.Root
-
-const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
-
-const DropdownMenuGroup = DropdownMenuPrimitive.Group
-
-const DropdownMenuPortal = DropdownMenuPrimitive.Portal
-
-const DropdownMenuSub = DropdownMenuPrimitive.Sub
-
-const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
-
-const DropdownMenuSubTrigger = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
- inset?: boolean
- }
->(({ className, inset, children, ...props }, ref) => (
- <DropdownMenuPrimitive.SubTrigger
- ref={ref}
- className={cn(
- "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
- inset && "pl-8",
- className
- )}
- {...props}
- >
- {children}
- <ChevronRightIcon className="ml-auto h-4 w-4" />
- </DropdownMenuPrimitive.SubTrigger>
-))
-DropdownMenuSubTrigger.displayName =
- DropdownMenuPrimitive.SubTrigger.displayName
-
-const DropdownMenuSubContent = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
->(({ className, ...props }, ref) => (
- <DropdownMenuPrimitive.SubContent
- ref={ref}
- className={cn(
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- className
- )}
- {...props}
- />
-))
-DropdownMenuSubContent.displayName =
- DropdownMenuPrimitive.SubContent.displayName
-
-const DropdownMenuContent = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
->(({ className, sideOffset = 4, ...props }, ref) => (
- <DropdownMenuPrimitive.Portal>
- <DropdownMenuPrimitive.Content
- ref={ref}
- sideOffset={sideOffset}
- className={cn(
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- className
- )}
- {...props}
- />
- </DropdownMenuPrimitive.Portal>
-))
-DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
-
-const DropdownMenuItem = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
- inset?: boolean
- }
->(({ className, inset, ...props }, ref) => (
- <DropdownMenuPrimitive.Item
- ref={ref}
- className={cn(
- "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- inset && "pl-8",
- className
- )}
- {...props}
- />
-))
-DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
-
-const DropdownMenuCheckboxItem = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
->(({ className, children, checked, ...props }, ref) => (
- <DropdownMenuPrimitive.CheckboxItem
- ref={ref}
- className={cn(
- "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className
- )}
- checked={checked}
- {...props}
- >
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
- <DropdownMenuPrimitive.ItemIndicator>
- <CheckIcon className="h-4 w-4" />
- </DropdownMenuPrimitive.ItemIndicator>
- </span>
- {children}
- </DropdownMenuPrimitive.CheckboxItem>
-))
-DropdownMenuCheckboxItem.displayName =
- DropdownMenuPrimitive.CheckboxItem.displayName
-
-const DropdownMenuRadioItem = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
->(({ className, children, ...props }, ref) => (
- <DropdownMenuPrimitive.RadioItem
- ref={ref}
- className={cn(
- "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className
- )}
- {...props}
- >
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
- <DropdownMenuPrimitive.ItemIndicator>
- <DotFilledIcon className="h-4 w-4 fill-current" />
- </DropdownMenuPrimitive.ItemIndicator>
- </span>
- {children}
- </DropdownMenuPrimitive.RadioItem>
-))
-DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
-
-const DropdownMenuLabel = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Label>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
- inset?: boolean
- }
->(({ className, inset, ...props }, ref) => (
- <DropdownMenuPrimitive.Label
- ref={ref}
- className={cn(
- "px-2 py-1.5 text-sm font-semibold",
- inset && "pl-8",
- className
- )}
- {...props}
- />
-))
-DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
-
-const DropdownMenuSeparator = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
->(({ className, ...props }, ref) => (
- <DropdownMenuPrimitive.Separator
- ref={ref}
- className={cn("-mx-1 my-1 h-px bg-muted", className)}
- {...props}
- />
-))
-DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
-
-const DropdownMenuShortcut = ({
- className,
- ...props
-}: React.HTMLAttributes<HTMLSpanElement>) => {
- return (
- <span
- className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
- {...props}
- />
- )
-}
-DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
-
-export {
- DropdownMenu,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuCheckboxItem,
- DropdownMenuRadioItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuShortcut,
- DropdownMenuGroup,
- DropdownMenuPortal,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuRadioGroup,
-}
diff --git a/apps/web/app/components/ui/input.tsx b/apps/web/app/components/ui/input.tsx
deleted file mode 100644
index 601b3d24..00000000
--- a/apps/web/app/components/ui/input.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import * as React from "react"
-
-import { cn } from "~/lib/utils"
-
-const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
- ({ className, type, ...props }, ref) => {
- return (
- <input
- type={type}
- className={cn(
- "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
- className
- )}
- ref={ref}
- {...props}
- />
- )
- }
-)
-Input.displayName = "Input"
-
-export { Input }
diff --git a/apps/web/app/components/ui/label.tsx b/apps/web/app/components/ui/label.tsx
deleted file mode 100644
index dbefb85d..00000000
--- a/apps/web/app/components/ui/label.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as React from "react"
-import * as LabelPrimitive from "@radix-ui/react-label"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "~/lib/utils"
-
-const labelVariants = cva(
- "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
-)
-
-const Label = React.forwardRef<
- React.ElementRef<typeof LabelPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
- VariantProps<typeof labelVariants>
->(({ className, ...props }, ref) => (
- <LabelPrimitive.Root
- ref={ref}
- className={cn(labelVariants(), className)}
- {...props}
- />
-))
-Label.displayName = LabelPrimitive.Root.displayName
-
-export { Label }
diff --git a/apps/web/app/components/ui/popover.tsx b/apps/web/app/components/ui/popover.tsx
deleted file mode 100644
index 0852301d..00000000
--- a/apps/web/app/components/ui/popover.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import * as React from "react";
-
-import * as PopoverPrimitive from "@radix-ui/react-popover";
-import { cn } from "~/lib/utils";
-
-const Popover = PopoverPrimitive.Root;
-
-const PopoverTrigger = PopoverPrimitive.Trigger;
-
-const PopoverAnchor = PopoverPrimitive.Anchor;
-
-const PopoverContent = React.forwardRef<
- React.ElementRef<typeof PopoverPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
->(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
- <PopoverPrimitive.Portal>
- <PopoverPrimitive.Content
- ref={ref}
- align={align}
- sideOffset={sideOffset}
- className={cn(
- "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- className,
- )}
- {...props}
- />
- </PopoverPrimitive.Portal>
-));
-PopoverContent.displayName = PopoverPrimitive.Content.displayName;
-
-export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
diff --git a/apps/web/app/components/ui/select.tsx b/apps/web/app/components/ui/select.tsx
deleted file mode 100644
index 4cba950c..00000000
--- a/apps/web/app/components/ui/select.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-import * as React from "react";
-
-import * as SelectPrimitive from "@radix-ui/react-select";
-import { CaretSortIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from "@radix-ui/react-icons";
-import { cn } from "~/lib/utils";
-
-const Select = SelectPrimitive.Root;
-
-const SelectGroup = SelectPrimitive.Group;
-
-const SelectValue = SelectPrimitive.Value;
-
-const SelectTrigger = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Trigger>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
->(({ className, children, ...props }, ref) => (
- <SelectPrimitive.Trigger
- ref={ref}
- className={cn(
- "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
- className,
- )}
- {...props}
- >
- {children}
- <SelectPrimitive.Icon asChild>
- <CaretSortIcon className="h-4 w-4 opacity-50" />
- </SelectPrimitive.Icon>
- </SelectPrimitive.Trigger>
-));
-SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
-
-const SelectScrollUpButton = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
->(({ className, ...props }, ref) => (
- <SelectPrimitive.ScrollUpButton
- ref={ref}
- className={cn("flex cursor-default items-center justify-center py-1", className)}
- {...props}
- >
- <ChevronUpIcon />
- </SelectPrimitive.ScrollUpButton>
-));
-SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
-
-const SelectScrollDownButton = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
->(({ className, ...props }, ref) => (
- <SelectPrimitive.ScrollDownButton
- ref={ref}
- className={cn("flex cursor-default items-center justify-center py-1", className)}
- {...props}
- >
- <ChevronDownIcon />
- </SelectPrimitive.ScrollDownButton>
-));
-SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
-
-const SelectContent = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
->(({ className, children, position = "popper", ...props }, ref) => (
- <SelectPrimitive.Portal>
- <SelectPrimitive.Content
- ref={ref}
- className={cn(
- "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- position === "popper" &&
- "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
- className,
- )}
- position={position}
- {...props}
- >
- <SelectScrollUpButton />
- <SelectPrimitive.Viewport
- className={cn(
- "p-1",
- position === "popper" &&
- "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
- )}
- >
- {children}
- </SelectPrimitive.Viewport>
- <SelectScrollDownButton />
- </SelectPrimitive.Content>
- </SelectPrimitive.Portal>
-));
-SelectContent.displayName = SelectPrimitive.Content.displayName;
-
-const SelectLabel = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Label>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
->(({ className, ...props }, ref) => (
- <SelectPrimitive.Label
- ref={ref}
- className={cn("px-2 py-1.5 text-sm font-semibold", className)}
- {...props}
- />
-));
-SelectLabel.displayName = SelectPrimitive.Label.displayName;
-
-const SelectItem = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
->(({ className, children, ...props }, ref) => (
- <SelectPrimitive.Item
- ref={ref}
- className={cn(
- "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className,
- )}
- {...props}
- >
- <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
- <SelectPrimitive.ItemIndicator>
- <CheckIcon className="h-4 w-4" />
- </SelectPrimitive.ItemIndicator>
- </span>
- <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
- </SelectPrimitive.Item>
-));
-SelectItem.displayName = SelectPrimitive.Item.displayName;
-
-const SelectSeparator = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Separator>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
->(({ className, ...props }, ref) => (
- <SelectPrimitive.Separator
- ref={ref}
- className={cn("-mx-1 my-1 h-px bg-muted", className)}
- {...props}
- />
-));
-SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
-
-export {
- Select,
- SelectGroup,
- SelectValue,
- SelectTrigger,
- SelectContent,
- SelectLabel,
- SelectItem,
- SelectSeparator,
- SelectScrollUpButton,
- SelectScrollDownButton,
-};
diff --git a/apps/web/app/components/ui/switch.tsx b/apps/web/app/components/ui/switch.tsx
deleted file mode 100644
index 87485a39..00000000
--- a/apps/web/app/components/ui/switch.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import * as React from "react"
-import * as SwitchPrimitives from "@radix-ui/react-switch"
-
-import { cn } from "~/lib/utils"
-
-const Switch = React.forwardRef<
- React.ElementRef<typeof SwitchPrimitives.Root>,
- React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
->(({ className, ...props }, ref) => (
- <SwitchPrimitives.Root
- className={cn(
- "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
- className
- )}
- {...props}
- ref={ref}
- >
- <SwitchPrimitives.Thumb
- className={cn(
- "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
- )}
- />
- </SwitchPrimitives.Root>
-))
-Switch.displayName = SwitchPrimitives.Root.displayName
-
-export { Switch }
diff --git a/apps/web/app/components/ui/tabs.tsx b/apps/web/app/components/ui/tabs.tsx
deleted file mode 100644
index e39e6060..00000000
--- a/apps/web/app/components/ui/tabs.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import * as React from "react"
-import * as TabsPrimitive from "@radix-ui/react-tabs"
-
-import { cn } from "~/lib/utils"
-
-const Tabs = TabsPrimitive.Root
-
-const TabsList = React.forwardRef<
- React.ElementRef<typeof TabsPrimitive.List>,
- React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
->(({ className, ...props }, ref) => (
- <TabsPrimitive.List
- ref={ref}
- className={cn(
- "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
- className
- )}
- {...props}
- />
-))
-TabsList.displayName = TabsPrimitive.List.displayName
-
-const TabsTrigger = React.forwardRef<
- React.ElementRef<typeof TabsPrimitive.Trigger>,
- React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
->(({ className, ...props }, ref) => (
- <TabsPrimitive.Trigger
- ref={ref}
- className={cn(
- "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
- className
- )}
- {...props}
- />
-))
-TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
-
-const TabsContent = React.forwardRef<
- React.ElementRef<typeof TabsPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
->(({ className, ...props }, ref) => (
- <TabsPrimitive.Content
- ref={ref}
- className={cn(
- "ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
- className
- )}
- {...props}
- />
-))
-TabsContent.displayName = TabsPrimitive.Content.displayName
-
-export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/apps/web/app/components/ui/textarea.tsx b/apps/web/app/components/ui/textarea.tsx
deleted file mode 100644
index e05f6bb1..00000000
--- a/apps/web/app/components/ui/textarea.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as React from "react";
-
-import { cn } from "~/lib/utils";
-
-export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
-
-const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
- ({ className, ...props }, ref) => {
- return (
- <textarea
- className={cn(
- "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
- className,
- )}
- ref={ref}
- {...props}
- />
- );
- },
-);
-Textarea.displayName = "Textarea";
-
-export { Textarea };
diff --git a/apps/web/app/config/integrations.tsx b/apps/web/app/config/integrations.tsx
deleted file mode 100644
index 0b55c143..00000000
--- a/apps/web/app/config/integrations.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
-import type { NavigateFunction } from "@remix-run/react";
-
-import { NotionIcon } from "../components/icons/IntegrationIcons";
-import { getChromeExtensionId } from "./util";
-
-export type IntegrationStatus = "idle" | "loading" | "success" | "error";
-
-export interface IntegrationProgress {
- progress: number;
- message: string;
-}
-
-export interface IntegrationConfig {
- id: string;
- name: string;
- description: string;
- icon?: React.ComponentType<{ className?: string }>;
- buttonClassName?: string;
- iconClassName?: string;
-
- requiresChromeExtension?: {
- extensionId: string;
- installUrl?: string;
- };
-
- getAuthUrl?: (env: Record<string, string>) => string;
- handleConnection?: (env: Record<string, string>, navigate: NavigateFunction) => void;
-
- importData?: {
- url?: string;
- method?: "GET" | "POST";
- withCredentials?: boolean;
- parseProgress?: (data: any) => IntegrationProgress;
- onSuccess?: (navigate: NavigateFunction) => void;
- onError?: (error: Error) => void;
- };
-}
-
-export const getIntegrations = (
- env: Record<string, string>,
-): Record<string, IntegrationConfig> => ({
- notion: {
- id: "notion",
- name: "Notion",
- description: "Import your Notion pages and databases",
- icon: NotionIcon,
- buttonClassName: "bg-neutral-800 hover:bg-neutral-700",
- iconClassName: "w-6 h-6",
-
- getAuthUrl: (env) => {
- const params = new URLSearchParams({
- client_id: env.NOTION_CLIENT_ID || "",
- redirect_uri:
- env.NODE_ENV === "development"
- ? "http://localhost:3000/auth/notion/callback"
- : "https://supermemory.ai/auth/notion/callback",
- response_type: "code",
- owner: "user",
- });
-
- return `https://api.notion.com/v1/oauth/authorize?${params.toString()}`;
- },
-
- importData: {
- url: `/backend/v1/integrations/notion/import`,
- withCredentials: true,
- parseProgress: (data) => ({
- progress: data.progress,
- message: `Importing from Notion: ${data.progress}%`,
- }),
- onSuccess: (navigate) => {
- setTimeout(() => navigate("/"), 1500);
- },
- },
- },
-
- xBookmarks: {
- id: "xBookmarks",
- name: "X Bookmarks",
- description: "Import your saved tweets and threads",
- iconClassName: "w-6 h-6",
- buttonClassName: "bg-blue-500 hover:bg-blue-600",
-
- requiresChromeExtension: {
- extensionId: getChromeExtensionId(),
- installUrl:
- "https://chrome.google.com/webstore/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc",
- },
-
- handleConnection: async (env, navigate) => {
- console.log("Sending message to extension");
- console.log(window.chrome?.runtime);
- await window.chrome?.runtime.sendMessage(
- getChromeExtensionId(),
- { action: "exportBookmarks" },
- (response: any) => {
- // Handle the response and show progress
- console.log("Response:", response);
- },
- );
- console.log("Message sent");
- },
-
- importData: {
- parseProgress: (data) => ({
- progress: data.progress,
- message: `Importing from X: ${data.progress}%`,
- }),
- onSuccess: (navigate) => {
- setTimeout(() => navigate("/bookmarks"), 1500);
- },
- },
-
- icon: (props) => (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="1em"
- height="1em"
- fill="none"
- viewBox="0 0 1200 1227"
- {...props}
- >
- <path
- fill="currentColor"
- d="M714.163 519.284 1160.89 0h-105.86L667.137 450.887 357.328 0H0l468.492 681.821L0 1226.37h105.866l409.625-476.152 327.181 476.152H1200L714.137 519.284h.026ZM569.165 687.828l-47.468-67.894-377.686-540.24h162.604l304.797 435.991 47.468 67.894 396.2 566.721H892.476L569.165 687.854v-.026Z"
- />
- </svg>
- ),
- },
-
- chromeBookmarks: {
- id: "chromeBookmarks",
- name: "Chrome Bookmarks",
- description: "Import your Chrome bookmarks",
- iconClassName: "w-6 h-6",
- buttonClassName: "bg-yellow-500 hover:bg-yellow-600",
-
- requiresChromeExtension: {
- extensionId: getChromeExtensionId(),
- installUrl:
- "https://chrome.google.com/webstore/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc",
- },
-
- handleConnection: (env, navigate) => {
- window.chrome?.runtime.sendMessage(
- getChromeExtensionId(),
- { action: "importBookmarks" },
- (response: any) => {
- console.log("Response:", response);
- },
- );
- },
-
- importData: {
- parseProgress: (data) => ({
- progress: data.progress,
- message: `Importing Chrome bookmarks: ${data.progress}%`,
- }),
- onSuccess: (navigate) => {
- setTimeout(() => navigate("/bookmarks"), 1500);
- },
- },
-
- icon: (props) => (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="1em"
- height="1em"
- viewBox="0 0 24 24"
- {...props}
- >
- <path
- fill="currentColor"
- d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"
- />
- <path
- fill="currentColor"
- d="M12 6c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z"
- />
- </svg>
- ),
- },
-
- iosShortcut: {
- id: "iosShortcut",
- name: "iOS Shortcut",
- description: "Import content using the iOS Shortcut",
- iconClassName: "w-6 h-6",
- buttonClassName: "bg-gray-500 hover:bg-gray-600",
-
- handleConnection: (env, navigate) => {
- window.location.href = "https://www.icloud.com/shortcuts/55f0695258cd46e4aad1aba8a2a7d14b";
- },
-
- icon: (props) => (
- <img
- src="https://imagedelivery.net/_Zs8NCbSWCQ8-iurXrWjBg/21434e3e-49ac-4a15-126d-175e95440300/public"
- alt="iOS Shortcut"
- {...props}
- />
- ),
- },
-});
diff --git a/apps/web/app/config/util.ts b/apps/web/app/config/util.ts
deleted file mode 100644
index 8e5e143e..00000000
--- a/apps/web/app/config/util.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// import {} from "@remix-run/cloudflare"
-
-// TODO: determine current context and return the correct extension id
-export const getChromeExtensionId = () => {
- // const context =
- // if (environment === "development") {
- // return "hepmifhfbiehpdlbeigpapkmjcaphfmn";
- // }
- // return "lcmcmhklcjciegoakjejefbacbgikncb";
- return "afpgkkipfdpeaflnpoaffkcankadgjfc";
-};
diff --git a/apps/web/app/entry.client.tsx b/apps/web/app/entry.client.tsx
deleted file mode 100644
index 2c8e46bd..00000000
--- a/apps/web/app/entry.client.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { StrictMode, startTransition, useEffect } from "react";
-import { hydrateRoot } from "react-dom/client";
-
-import { RemixBrowser } from "@remix-run/react";
-
-startTransition(() => {
- hydrateRoot(
- document,
- // <StrictMode>
- <>
- <RemixBrowser />
- </>,
- // </StrictMode>,
- );
-});
diff --git a/apps/web/app/entry.server.tsx b/apps/web/app/entry.server.tsx
deleted file mode 100644
index a47be3fc..00000000
--- a/apps/web/app/entry.server.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import type { AppLoadContext, EntryContext } from "@remix-run/cloudflare";
-import { RemixServer } from "@remix-run/react";
-import * as isbotModule from "isbot";
-import { renderToReadableStream } from "react-dom/server";
-
-export default async function handleRequest(
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- remixContext: EntryContext,
- loadContext: AppLoadContext
-) {
- const body = await renderToReadableStream(
- <RemixServer context={remixContext} url={request.url} />,
- {
- // If you wish to abort the rendering process, you can pass a signal here.
- // Please refer to the templates for example son how to configure this.
- // signal: controller.signal,
- onError(error: unknown) {
- // Log streaming rendering errors from inside the shell
- console.error(error);
- responseStatusCode = 500;
- },
- }
- );
-
- if (isBotRequest(request.headers.get("user-agent"))) {
- await body.allReady;
- }
-
- responseHeaders.set("Content-Type", "text/html");
- return new Response(body, {
- headers: responseHeaders,
- status: responseStatusCode,
- });
-}
-
-// We have some Remix apps in the wild already running with isbot@3 so we need
-// to maintain backwards compatibility even though we want new apps to use
-// isbot@4. That way, we can ship this as a minor Semver update to @remix-run/dev.
-function isBotRequest(userAgent: string | null) {
- if (!userAgent) {
- return false;
- }
-
- // isbot >= 3.8.0, >4
- if ("isbot" in isbotModule && typeof isbotModule.isbot === "function") {
- return isbotModule.isbot(userAgent);
- }
-
- // isbot < 3.8.0
- if ("default" in isbotModule && typeof isbotModule.default === "function") {
- return isbotModule.default(userAgent);
- }
-
- return false;
-}
diff --git a/apps/web/app/icon.png b/apps/web/app/icon.png
new file mode 100644
index 00000000..2f30042b
--- /dev/null
+++ b/apps/web/app/icon.png
Binary files differ
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
new file mode 100644
index 00000000..095763b3
--- /dev/null
+++ b/apps/web/app/layout.tsx
@@ -0,0 +1,69 @@
+import type { Metadata } from "next"
+import { JetBrains_Mono, Inter } from "next/font/google"
+import "../globals.css"
+import "@ui/globals.css"
+import { AuthProvider } from "@lib/auth-context"
+import { ErrorTrackingProvider } from "@lib/error-tracking"
+import { PostHogProvider } from "@lib/posthog"
+import { QueryProvider } from "@lib/query-client"
+import { AutumnProvider } from "autumn-js/react"
+import { Suspense } from "react"
+import { Toaster } from "sonner"
+import { TourProvider } from "@/components/tour"
+import { MobilePanelProvider } from "@/lib/mobile-panel-context"
+
+import { ViewModeProvider } from "@/lib/view-mode-context"
+
+const sans = Inter({
+ subsets: ["latin"],
+ variable: "--font-sans",
+})
+
+const mono = JetBrains_Mono({
+ subsets: ["latin"],
+ variable: "--font-mono",
+})
+
+export const metadata: Metadata = {
+ metadataBase: new URL("https://app.supermemory.ai"),
+ description: "Your memories, wherever you are",
+ title: "supermemory app",
+}
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode
+}>) {
+ return (
+ <html className="dark bg-sm-black" lang="en">
+ <body
+ className={`${sans.variable} ${mono.variable} antialiased bg-[#0f1419]`}
+ >
+ <AutumnProvider
+ backendUrl={
+ process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
+ }
+ includeCredentials={true}
+ >
+ <QueryProvider>
+ <AuthProvider>
+ <ViewModeProvider>
+ <MobilePanelProvider>
+ <PostHogProvider>
+ <ErrorTrackingProvider>
+ <TourProvider>
+ <Suspense>{children}</Suspense>
+ <Toaster richColors theme="dark" />
+ </TourProvider>
+ </ErrorTrackingProvider>
+ </PostHogProvider>
+ </MobilePanelProvider>
+ </ViewModeProvider>
+ </AuthProvider>
+ </QueryProvider>
+ </AutumnProvider>
+ </body>
+ </html>
+ )
+}
diff --git a/apps/web/app/lib/auth/authMiddleware.ts b/apps/web/app/lib/auth/authMiddleware.ts
deleted file mode 100644
index e4b59243..00000000
--- a/apps/web/app/lib/auth/authMiddleware.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { AppLoadContext, redirect } from "@remix-run/cloudflare";
-
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-import { database, eq } from "@supermemory/db";
-import { User, users } from "@supermemory/db/schema";
-
-export const verifyOrCreateUser = async (
- request: Request,
- context: AppLoadContext,
-): Promise<User | null> => {
- const session = await getSessionFromRequest(request, context);
- console.log(session);
- if (!session?.user?.id) {
- return null;
- }
-
- let user = await database(context.cloudflare.env.HYPERDRIVE.connectionString)
- .select()
- .from(users)
- .where(eq(users.uuid, session.user.id));
-
- if ((!user || user.length === 0) && session?.user?.id) {
- const newUser = await database(context.cloudflare.env.HYPERDRIVE.connectionString)
- .insert(users)
- .values({
- uuid: session.user?.id,
- email: session.user?.email,
- firstName: session.user?.firstName,
- lastName: session.user?.lastName,
- createdAt: new Date(),
- updatedAt: new Date(),
- emailVerified: false,
- profilePictureUrl: session.user?.profilePictureUrl ?? "",
- })
- .returning();
-
- user = newUser;
- }
-
- if (!user) {
- return null;
- }
-
- return user[0];
-};
diff --git a/apps/web/app/lib/constants/pastelColors.tsx b/apps/web/app/lib/constants/pastelColors.tsx
deleted file mode 100644
index 096d8aa2..00000000
--- a/apps/web/app/lib/constants/pastelColors.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-export const pastelColors = [
- "#FF634770",
- "#FFA50070",
- "#FFD70070",
- "#90EE9070",
- "#ADD8E670",
- "#9370DB70",
- "#FFB6C170",
-];
diff --git a/apps/web/app/lib/constants/typeIcons.tsx b/apps/web/app/lib/constants/typeIcons.tsx
deleted file mode 100644
index 6655650c..00000000
--- a/apps/web/app/lib/constants/typeIcons.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-export const memoryTypes = ["website", "document", "tweet", "note"] as const;
-
-export const typeIcons = {
- page: (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 16 16"
- fill="currentColor"
- className="size-4"
- >
- <path
- fillRule="evenodd"
- d="M3.757 4.5c.18.217.376.42.586.608.153-.61.354-1.175.596-1.678A5.53 5.53 0 0 0 3.757 4.5ZM8 1a6.994 6.994 0 0 0-7 7 7 7 0 1 0 7-7Zm0 1.5c-.476 0-1.091.386-1.633 1.427-.293.564-.531 1.267-.683 2.063A5.48 5.48 0 0 0 8 6.5a5.48 5.48 0 0 0 2.316-.51c-.152-.796-.39-1.499-.683-2.063C9.09 2.886 8.476 2.5 8 2.5Zm3.657 2.608a8.823 8.823 0 0 0-.596-1.678c.444.298.842.659 1.182 1.07-.18.217-.376.42-.586.608Zm-1.166 2.436A6.983 6.983 0 0 1 8 8a6.983 6.983 0 0 1-2.49-.456 10.703 10.703 0 0 0 .202 2.6c.72.231 1.49.356 2.288.356.798 0 1.568-.125 2.29-.356a10.705 10.705 0 0 0 .2-2.6Zm1.433 1.85a12.652 12.652 0 0 0 .018-2.609c.405-.276.78-.594 1.117-.947a5.48 5.48 0 0 1 .44 2.262 7.536 7.536 0 0 1-1.575 1.293Zm-2.172 2.435a9.046 9.046 0 0 1-3.504 0c.039.084.078.166.12.244C6.907 13.114 7.523 13.5 8 13.5s1.091-.386 1.633-1.427c.04-.078.08-.16.12-.244Zm1.31.74a8.5 8.5 0 0 0 .492-1.298c.457-.197.893-.43 1.307-.696a5.526 5.526 0 0 1-1.8 1.995Zm-6.123 0a8.507 8.507 0 0 1-.493-1.298 8.985 8.985 0 0 1-1.307-.696 5.526 5.526 0 0 0 1.8 1.995ZM2.5 8.1c.463.5.993.935 1.575 1.293a12.652 12.652 0 0 1-.018-2.608 7.037 7.037 0 0 1-1.117-.947 5.48 5.48 0 0 0-.44 2.262Z"
- clipRule="evenodd"
- />
- </svg>
- ),
- document: (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 16 16"
- fill="currentColor"
- className="size-4"
- >
- <path
- fillRule="evenodd"
- d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm1 5.75A.75.75 0 0 1 5.75 7h4.5a.75.75 0 0 1 0 1.5h-4.5A.75.75 0 0 1 5 7.75Zm0 3a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z"
- clipRule="evenodd"
- />
- </svg>
- ),
- tweet: (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 16 16"
- fill="currentColor"
- className="size-4"
- >
- <path
- fillRule="evenodd"
- d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm1 5.75A.75.75 0 0 1 5.75 7h4.5a.75.75 0 0 1 0 1.5h-4.5A.75.75 0 0 1 5 7.75Zm0 3a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z"
- clipRule="evenodd"
- />
- </svg>
- ),
- note: (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 16 16"
- fill="currentColor"
- className="size-4"
- >
- <path d="M13.488 2.513a1.75 1.75 0 0 0-2.475 0L6.75 6.774a2.75 2.75 0 0 0-.596.892l-.848 2.047a.75.75 0 0 0 .98.98l2.047-.848a2.75 2.75 0 0 0 .892-.596l4.261-4.262a1.75 1.75 0 0 0 0-2.474Z" />
- <path d="M4.75 3.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h6.5c.69 0 1.25-.56 1.25-1.25V9A.75.75 0 0 1 14 9v2.25A2.75 2.75 0 0 1 11.25 14h-6.5A2.75 2.75 0 0 1 2 11.25v-6.5A2.75 2.75 0 0 1 4.75 2H7a.75.75 0 0 1 0 1.5H4.75Z" />
- </svg>
- ),
-};
diff --git a/apps/web/app/lib/env.server.ts b/apps/web/app/lib/env.server.ts
deleted file mode 100644
index 314bc9db..00000000
--- a/apps/web/app/lib/env.server.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { zodEnv } from "./environment";
-
-import process from "node:process";
-import { z } from "zod";
-
-try {
- zodEnv.parse(process.env);
-} catch (err) {
- if (process.env.NODE_ENV === "production") {
- // do nothihng
- console.log("production");
- } else if (err instanceof z.ZodError) {
- const { fieldErrors } = err.flatten();
- const errorMessage = Object.entries(fieldErrors)
- .map(([field, errors]) => (errors ? `${field}: ${errors.join(", ")}` : field))
- .join("\n ");
- throw new Error(`Missing environment variables:\n ${errorMessage}`);
- }
-}
diff --git a/apps/web/app/lib/environment.ts b/apps/web/app/lib/environment.ts
deleted file mode 100644
index f8b69762..00000000
--- a/apps/web/app/lib/environment.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { z } from "zod";
-
-export const zodEnv = z.object({
- // Auth
- WORKOS_CLIENT_ID: z.string(),
- WORKOS_API_KEY: z.string(),
- WORKOS_REDIRECT_URI: z.string(),
- WORKOS_COOKIE_PASSWORD: z.string(),
-
- DATABASE_URL: z.string(),
-
- CLOUDFLARE_ACCOUNT_ID: z.string(),
- R2_ACCESS_KEY_ID: z.string(),
- R2_SECRET_ACCESS_KEY: z.string(),
-});
diff --git a/apps/web/app/lib/hooks/use-auto-scroll.ts b/apps/web/app/lib/hooks/use-auto-scroll.ts
deleted file mode 100644
index b54e99ea..00000000
--- a/apps/web/app/lib/hooks/use-auto-scroll.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-
-interface UseAutoScrollOptions {
- offset?: number;
- bottomThreshold?: number;
-}
-
-export function useAutoScroll({ offset = 250, bottomThreshold = 10 }: UseAutoScrollOptions = {}) {
- const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
- const scrollRef = useRef<HTMLDivElement>(null);
- const bottomRef = useRef<HTMLDivElement>(null);
-
- const scrollToBottom = (behavior: ScrollBehavior = "smooth") => {
- if (bottomRef.current && shouldAutoScroll) {
- const parent = scrollRef.current;
- if (!parent) return;
-
- // Calculate the scroll position accounting for the offset
- const targetScrollTop = bottomRef.current.offsetTop - parent.clientHeight + offset;
- parent.scrollTo({ top: targetScrollTop, behavior });
- }
- };
-
- const handleScroll = () => {
- if (!scrollRef.current || !bottomRef.current) return;
-
- const { scrollTop, clientHeight } = scrollRef.current;
- const targetScrollTop = bottomRef.current.offsetTop - clientHeight + offset;
-
- // Consider the user at the bottom if they're within the threshold of the target scroll position
- const isAtBottom = Math.abs(targetScrollTop - scrollTop) < bottomThreshold;
-
- if (shouldAutoScroll !== isAtBottom) {
- setShouldAutoScroll(isAtBottom);
- }
- };
-
- useEffect(() => {
- const scrollElement = scrollRef.current;
- if (!scrollElement) return;
-
- const handleTouchStart = () => {
- setShouldAutoScroll(false);
- };
-
- scrollElement.addEventListener("touchstart", handleTouchStart);
- scrollElement.addEventListener("scroll", handleScroll);
-
- return () => {
- scrollElement.removeEventListener("touchstart", handleTouchStart);
- scrollElement.removeEventListener("scroll", handleScroll);
- };
- }, []);
-
- return {
- scrollRef,
- bottomRef,
- shouldAutoScroll,
- scrollToBottom,
- setShouldAutoScroll
- };
-} \ No newline at end of file
diff --git a/apps/web/app/lib/hooks/use-chat-stream.ts b/apps/web/app/lib/hooks/use-chat-stream.ts
deleted file mode 100644
index 4cf25111..00000000
--- a/apps/web/app/lib/hooks/use-chat-stream.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { useEffect, useState } from "react";
-
-import { convertToUIMessages } from "@supermemory/shared";
-import { CoreMessage } from "ai";
-import { useChat } from "ai/react";
-import { CreateMessage } from "ai/react";
-import { toast } from "sonner";
-
-export const useChatStream = (initialMessages: CoreMessage[], initialThreadUuid?: string) => {
- const [threadUuid, setThreadUuid] = useState(initialThreadUuid || "");
- const { messages, input, setInput, append, isLoading, error, stop, handleSubmit } = useChat({
- initialMessages: convertToUIMessages(initialMessages),
- api: `/backend/v1/chat`,
- onResponse: (resp) => {
- const newThreadUuid = resp.headers.get("Supermemory-Thread-Uuid");
- if (newThreadUuid) {
- setThreadUuid(newThreadUuid);
- if (window.location.pathname !== `/chat/${newThreadUuid}`) {
- window.history.replaceState({}, "", `/chat/${newThreadUuid}`);
- }
- }
- },
- keepLastMessageOnError: true,
- onError: (e) => {
- console.error(e);
- },
- body: {
- threadId: threadUuid,
- },
- credentials: "include",
- });
-
- const sendMessage = async ({
- comingFromUseEffect = false,
- fileURLs,
- }: { comingFromUseEffect?: boolean; fileURLs?: string[] } = {}) => {
- if (input.trim().length === 0 && !comingFromUseEffect) {
- return;
- }
-
- const item = {
- role: "user",
- content: input,
- experimental_attachments: fileURLs?.map((url) => ({
- name: url,
- url,
- contentType: url.endsWith(".pdf") ? "application/pdf" : "image/jpeg",
- })),
- data:
- fileURLs &&
- JSON.parse(
- JSON.stringify({
- files:
- fileURLs?.map((url) => ({
- contentType: url.endsWith(".pdf") ? "application/pdf" : "image/jpeg",
- url: decodeURIComponent(url),
- name: url.split("/").pop(),
- })) ?? [],
- }),
- ),
- };
-
- console.log("item", item);
-
- append(item as unknown as CreateMessage);
- };
-
- useEffect(() => {
- if (error) {
- console.error(error);
- toast.error(`Error while starting chat: ${error}`);
- stop();
- }
- if (
- messages.length !== 0 &&
- !isLoading &&
- messages[messages.length - 1].role === "user" &&
- !error
- ) {
- sendMessage({ comingFromUseEffect: true });
- }
- }, []);
-
- return { threadUuid, chatMessages: messages, sendMessage, input, setInput, isLoading };
-};
diff --git a/apps/web/app/lib/hooks/use-debounce.ts b/apps/web/app/lib/hooks/use-debounce.ts
deleted file mode 100644
index fd60d32a..00000000
--- a/apps/web/app/lib/hooks/use-debounce.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import * as React from 'react';
-
-export const useDebounce = <T>(value: T, delay = 500) => {
- const [debouncedValue, setDebouncedValue] = React.useState(value);
-
- React.useEffect(() => {
- const handler: NodeJS.Timeout = setTimeout(() => {
- setDebouncedValue(value);
- }, delay);
-
- // Cancel the timeout if value changes (also on delay change or unmount)
- return () => {
- clearTimeout(handler);
- };
- }, [value, delay]);
-
- return debouncedValue;
-};
diff --git a/apps/web/app/lib/hooks/use-fetcher-with-promise.tsx b/apps/web/app/lib/hooks/use-fetcher-with-promise.tsx
deleted file mode 100644
index 8fa77a7d..00000000
--- a/apps/web/app/lib/hooks/use-fetcher-with-promise.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import type { SerializeFrom } from '@remix-run/cloudflare'
-import { useFetcher } from '@remix-run/react'
-import type { AppData } from '@remix-run/react/dist/data'
-import React from 'react'
-
-type FetcherData<T> = NonNullable<SerializeFrom<T>>
-type ResolveFunction<T> = (value: FetcherData<T>) => void
-
-export function useFetcherWithPromise<TData = AppData>(opts?: Parameters<typeof useFetcher>[0]) {
- const fetcher = useFetcher<TData>(opts)
- const resolveRef = React.useRef<ResolveFunction<TData>>()
- const promiseRef = React.useRef<Promise<FetcherData<TData>>>()
-
- if (!promiseRef.current) {
- promiseRef.current = new Promise<FetcherData<TData>>((resolve) => {
- resolveRef.current = resolve
- })
- }
-
- const resetResolver = React.useCallback(() => {
- promiseRef.current = new Promise((resolve) => {
- resolveRef.current = resolve
- })
- }, [promiseRef, resolveRef])
-
- const submit = React.useCallback(
- async (...args: Parameters<typeof fetcher.submit>) => {
- fetcher.submit(...args)
- return promiseRef.current
- },
- [fetcher, promiseRef]
- )
-
- React.useEffect(() => {
- if (fetcher.state === 'idle') {
- if (fetcher.data) {
- resolveRef.current?.(fetcher.data)
- }
- resetResolver()
- }
- }, [fetcher, resetResolver])
-
- return { ...fetcher, submit }
-} \ No newline at end of file
diff --git a/apps/web/app/lib/hooks/use-is-touch-device.ts b/apps/web/app/lib/hooks/use-is-touch-device.ts
deleted file mode 100644
index a1c1d41d..00000000
--- a/apps/web/app/lib/hooks/use-is-touch-device.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-export function useIsTouchDevice() {
- const [isTouchDevice, setIsTouchDevice] = useState(false);
-
- useEffect(() => {
- function onResize() {
- setIsTouchDevice(
- 'ontouchstart' in window ||
- navigator.maxTouchPoints > 0 ||
- navigator.maxTouchPoints > 0
- );
- }
-
- window.addEventListener('resize', onResize);
- onResize();
-
- return () => {
- window.removeEventListener('resize', onResize);
- };
- }, []);
-
- return isTouchDevice;
-}
diff --git a/apps/web/app/lib/hooks/use-keyboard.tsx b/apps/web/app/lib/hooks/use-keyboard.tsx
deleted file mode 100644
index c2225693..00000000
--- a/apps/web/app/lib/hooks/use-keyboard.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import React, { createContext, useCallback, useContext, useEffect } from "react";
-
-// Define types
-type ModifierKey = "command" | "ctrl" | "shift" | "alt" | "meta";
-type Key = ModifierKey | string;
-
-type Shortcut = {
- keys: Key[];
- callback: () => void;
- label?: string;
-};
-
-type KeyboardContextType = {
- registerShortcut: (shortcut: Shortcut) => void;
- unregisterShortcut: (keys: Key[]) => void;
-};
-
-// Create context
-const KeyboardContext = createContext<KeyboardContextType | null>(null);
-
-// Create provider component
-export function KeyboardProvider({ children }: { children: React.ReactNode }) {
- const shortcuts = new Map<string, Shortcut>();
-
- const registerShortcut = useCallback((shortcut: Shortcut) => {
- const key = shortcut.keys.sort().join("+");
- shortcuts.set(key, shortcut);
- }, []);
-
- const unregisterShortcut = useCallback((keys: Key[]) => {
- const key = keys.sort().join("+");
- shortcuts.delete(key);
- }, []);
-
- const handleKeyDown = useCallback((event: KeyboardEvent) => {
- // Don't trigger shortcuts when typing in input elements
- if (
- event.target instanceof HTMLInputElement ||
- event.target instanceof HTMLTextAreaElement ||
- event.target instanceof HTMLSelectElement ||
- (event.target as HTMLElement).isContentEditable
- ) {
- return;
- }
-
- const pressedKeys: Key[] = [];
-
- // Handle modifier keys
- if (event.metaKey) {
- pressedKeys.push("command"); // Always use "command" for metaKey
- } else if (event.ctrlKey) {
- pressedKeys.push("ctrl");
- }
- if (event.shiftKey) pressedKeys.push("shift");
- if (event.altKey) pressedKeys.push("alt");
-
- // Add the actual key if it's not a modifier
- if (
- !["Meta", "Control", "Shift", "Alt"].includes(event.key) &&
- event.key.length === 1
- ) {
- pressedKeys.push(event.key.toLowerCase());
- } else if (event.key.length > 1) {
- // Handle special keys like Enter, Escape, etc
- pressedKeys.push(event.key.toLowerCase());
- }
-
- const key = pressedKeys.sort().join("+");
- const shortcut = shortcuts.get(key);
-
- if (shortcut) {
- event.preventDefault();
- shortcut.callback();
- }
- }, []);
-
- useEffect(() => {
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, [handleKeyDown]);
-
- return (
- <KeyboardContext.Provider value={{ registerShortcut, unregisterShortcut }}>
- {children}
- </KeyboardContext.Provider>
- );
-}
-
-export function useKeyboardShortcut(keys: Key[], callback: () => void, label?: string) {
- const context = useContext(KeyboardContext);
-
- if (!context) {
- throw new Error("useKeyboardShortcut must be used within a KeyboardProvider");
- }
-
- useEffect(() => {
- const shortcut = { keys, callback, label };
- context.registerShortcut(shortcut);
- return () => context.unregisterShortcut(keys);
- }, [keys.join(","), callback, label, context]);
-} \ No newline at end of file
diff --git a/apps/web/app/lib/hooks/use-live-transcript.tsx b/apps/web/app/lib/hooks/use-live-transcript.tsx
deleted file mode 100644
index 05c3436a..00000000
--- a/apps/web/app/lib/hooks/use-live-transcript.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-
-import { useState, useEffect, useCallback } from "react";
-import { useQueue } from "@uidotdev/usehooks";
-import { LiveClient, LiveTranscriptionEvents, createClient } from "@deepgram/sdk";
-
-export function useLiveTranscript() {
-
- const { add, remove, first, size, queue } = useQueue<any>([]);
- const [apiKey, _] = useState<string | null>("");
- const [connection, setConnection] = useState<LiveClient | null>(null);
- const [isListening, setIsListening] = useState(false);
- const [isLoading, setIsLoading] = useState(true);
- const [isProcessing, setIsProcessing] = useState(false);
- const [micOpen, setMicOpen] = useState(false);
- const [microphone, setMicrophone] = useState<MediaRecorder | null>(null);
- const [userMedia, setUserMedia] = useState<MediaStream | null>(null);
- const [caption, setCaption] = useState<string>("");
- const [status, setStatus] = useState<string>("Not Connected");
- // Initialize Deepgram connection
- const initializeConnection = useCallback(() => {
- if (!apiKey) return null;
-
- const deepgram = createClient(apiKey);
- const connection = deepgram.listen.live({
- model: "nova-3",
- language: "en",
- smart_format: true,
- interim_results: true,
- punctuate: true,
- diarize: true,
- utterances: true,
- });
-
- connection.on(LiveTranscriptionEvents.Open, () => {
- setStatus("Connected");
- setIsListening(true);
- });
-
- connection.on(LiveTranscriptionEvents.Close, () => {
- setStatus("Not Connected");
- setIsListening(false);
- });
-
- connection.on(LiveTranscriptionEvents.Error, (error) => {
- console.error("Deepgram error:", error);
- setStatus("Error occurred");
- });
-
- connection.on(LiveTranscriptionEvents.Transcript, (data) => {
- const transcript = data.channel.alternatives[0].transcript;
- if (data.is_final) {
- if (transcript && transcript.trim() !== "") {
- setCaption((prev) => prev + " " + transcript);
- }
- }
- });
-
- return connection;
- }, [apiKey]);
-
- const toggleMicrophone = useCallback(async () => {
- if (microphone && userMedia) {
- setUserMedia(null);
- setMicrophone(null);
- microphone.stop();
- if (connection) {
- connection.finish();
- setConnection(null);
- }
- } else {
- const userMedia = await navigator.mediaDevices.getUserMedia({
- audio: true,
- });
-
- const microphone = new MediaRecorder(userMedia);
- microphone.start(250);
-
- microphone.onstart = () => {
- setMicOpen(true);
- // Create new connection when starting microphone
- const newConnection = initializeConnection();
- if (newConnection) {
- setConnection(newConnection);
- }
- };
-
- microphone.onstop = () => {
- setMicOpen(false);
- };
-
- microphone.ondataavailable = (e) => {
- add(e.data);
- };
-
- setUserMedia(userMedia);
- setMicrophone(microphone);
- }
- }, [add, microphone, userMedia, connection, initializeConnection]);
-
- useEffect(() => {
- setIsLoading(false);
- }, []);
-
- useEffect(() => {
- const processQueue = async () => {
- if (size > 0 && !isProcessing && isListening && connection) {
- setIsProcessing(true);
- try {
- const blob = first;
- if (blob) {
- connection.send(blob);
- }
- remove();
- } catch (error) {
- console.error("Error processing audio:", error);
- }
- setIsProcessing(false);
- }
- };
-
- const interval = setInterval(processQueue, 100);
- return () => clearInterval(interval);
- }, [connection, queue, remove, first, size, isProcessing, isListening]);
-
- return {
- toggleMicrophone,
- caption,
- status,
- isListening,
- isLoading,
- };
-} \ No newline at end of file
diff --git a/apps/web/app/lib/hooks/use-media-query.tsx b/apps/web/app/lib/hooks/use-media-query.tsx
deleted file mode 100644
index ca0eae29..00000000
--- a/apps/web/app/lib/hooks/use-media-query.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as React from "react";
-
-export function useMediaQuery(query: string) {
- const [value, setValue] = React.useState(false);
-
- React.useEffect(() => {
- function onChange(event: MediaQueryListEvent) {
- setValue(event.matches);
- }
-
- const result = matchMedia(query);
- result.addEventListener("change", onChange);
- setValue(result.matches);
-
- return () => result.removeEventListener("change", onChange);
- }, [query]);
-
- return value;
-}
diff --git a/apps/web/app/lib/hooks/use-memories.tsx b/apps/web/app/lib/hooks/use-memories.tsx
deleted file mode 100644
index 66a3d60d..00000000
--- a/apps/web/app/lib/hooks/use-memories.tsx
+++ /dev/null
@@ -1,276 +0,0 @@
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { toast } from "sonner";
-import { typeDecider } from "~/components/memories/SharedCard";
-import { Memory } from "~/lib/types/memory";
-
-interface MemoriesResponse {
- items: Memory[];
- total: number;
-}
-
-interface CachedMemories {
- items: Memory[];
- total: number;
- nextCursor: number;
-}
-
-interface AddMemoryResponse {
- message: string;
- id: string;
- type: string;
-}
-
-export function useMemories(start = 0, count = 40, spaceId?: string) {
- const queryClient = useQueryClient();
- const cacheKey = ["memories", spaceId];
-
- const { data: memoriesData, isLoading: isInitialLoading } = useQuery<CachedMemories>({
- queryKey: cacheKey,
- queryFn: async () => {
- const url = new URL(`/backend/v1/memories`, window.location.origin);
- url.searchParams.set("start", "0");
- url.searchParams.set("count", count.toString());
- if (spaceId) url.searchParams.set("spaceId", spaceId);
-
- const response = await fetch(url.toString(), {
- credentials: "include",
- });
- if (!response.ok) throw new Error("Failed to fetch memories");
-
- const data = (await response.json()) as MemoriesResponse;
-
- return {
- items: data.items,
- total: data.total,
- nextCursor: data.items.length,
- };
- },
- staleTime: 1000 * 60 * 5, // Cache for 5 minutes
- });
-
- const fetchNextPage = useMutation({
- mutationFn: async () => {
- if (!memoriesData || memoriesData.nextCursor >= memoriesData.total) {
- return null;
- }
-
- const url = new URL(`/backend/v1/memories`, window.location.origin);
- url.searchParams.set("start", memoriesData.nextCursor.toString());
- url.searchParams.set("count", count.toString());
- if (spaceId) url.searchParams.set("spaceId", spaceId);
-
- const response = await fetch(url.toString(), {
- credentials: "include",
- });
- if (!response.ok) throw new Error("Failed to fetch next page");
-
- const data = (await response.json()) as MemoriesResponse;
-
- return data;
- },
- onSuccess: (data) => {
- if (!data) return;
-
- queryClient.setQueryData<CachedMemories>(cacheKey, (old) => {
- if (!old)
- return {
- items: data.items,
- total: data.total,
- nextCursor: data.items.length,
- };
-
- // Merge new items, avoiding duplicates
- const existingIds = new Set(old.items.map((item) => item.uuid));
- const newItems = data.items.filter((item) => !existingIds.has(item.uuid));
-
- return {
- items: [...old.items, ...newItems],
- total: data.total,
- nextCursor: old.nextCursor + newItems.length,
- };
- });
- },
- });
-
- const deleteMemory = useMutation({
- mutationFn: async (memoryId: string) => {
- const response = await fetch(`/backend/v1/memories/${memoryId}`, {
- method: "DELETE",
- credentials: "include",
- });
- if (!response.ok) throw new Error("Failed to delete memory");
- return response.json();
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["memories"] });
- },
- onError: (error: Error) => {
- toast.error(`Error deleting memory: ${error.message}`);
- },
- });
-
- const addMemory = useMutation({
- mutationFn: async ({ content, spaces }: { content: string; spaces: string[] }) => {
- const type = typeDecider(content);
- const optimisticMemory: Memory = {
- id: -1,
- content,
- type,
- createdAt: new Date(),
- description: null,
- ogImage: null,
- title: type === "note" ? content : null,
- url: type === "page" ? content : null,
- uuid: crypto.randomUUID(),
- updatedAt: null,
- raw: null,
- userId: 0,
- isSuccessfullyProcessed: false,
- errorMessage: null,
- contentHash: null,
- };
-
- queryClient.setQueryData<CachedMemories>(cacheKey, (old) => {
- if (!old)
- return {
- items: [optimisticMemory],
- total: 1,
- nextCursor: 1,
- };
-
- return {
- items: [optimisticMemory, ...old.items],
- total: old.total + 1,
- nextCursor: old.nextCursor + 1,
- };
- });
-
- const toastId = toast.loading("Adding content to your second brain...");
-
- try {
- const response = await fetch(`/backend/v1/add`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ content, spaces }),
- credentials: "include",
- });
-
- if (!response.ok) {
- const errorText = await response.text();
- // Remove optimistic update on error
- queryClient.setQueryData<CachedMemories>(cacheKey, (old) => {
- if (!old) return { items: [], total: 0, nextCursor: 0 };
- return {
- items: old.items.filter((m) => m.id !== optimisticMemory.id),
- total: old.total - 1,
- nextCursor: old.nextCursor - 1,
- };
- });
- toast.error(errorText, { id: toastId, richColors: true });
- throw new Error(errorText);
- }
-
- const result = (await response.json()) as AddMemoryResponse;
-
- toast.loading("Content queued for processing...", { id: toastId });
-
- const pollForMemory = async (): Promise<Memory> => {
- const response = await fetch(`/backend/v1/memories/${result.id}`, {
- credentials: "include",
- });
- if (!response.ok) {
- console.error(await response.text());
- toast.error("Failed to fetch processed memory", { id: toastId });
- throw new Error("Failed to fetch processed memory");
- }
- return response.json();
- };
-
- let attempts = 0;
- const maxAttempts = 15;
-
- while (attempts < maxAttempts) {
- await new Promise((resolve) => setTimeout(resolve, 2000));
- try {
- const processedMemory = await pollForMemory();
- if (processedMemory.isSuccessfullyProcessed) {
- toast.success("Memory added successfully!", { id: toastId });
- queryClient.setQueryData<CachedMemories>(cacheKey, (old) => {
- if (!old)
- return {
- items: [processedMemory],
- total: 1,
- nextCursor: 1,
- };
-
- return {
- items: old.items.map((m) => (m.id === optimisticMemory.id ? processedMemory : m)),
- total: old.total,
- nextCursor: old.nextCursor,
- };
- });
- return processedMemory;
- }
-
- if (attempts % 2 === 0) {
- toast.loading("Visiting and reading the website...", { id: toastId });
- }
- } catch (error) {
- toast.error("Failed to fetch processed memory", { id: toastId });
- console.error("Error polling for memory:", error);
- }
- attempts++;
- }
-
- return optimisticMemory;
- } catch (error) {
- // Remove optimistic update on any error
- queryClient.setQueryData<CachedMemories>(cacheKey, (old) => {
- if (!old) return { items: [], total: 0, nextCursor: 0 };
- return {
- items: old.items.filter((m) => m.id !== optimisticMemory.id),
- total: old.total - 1,
- nextCursor: old.nextCursor - 1,
- };
- });
- throw error;
- }
- },
- onError: (error: Error) => {
- queryClient.invalidateQueries({ queryKey: ["memories"] });
- },
- });
-
- const wrappedAddMemory = async (params: { content: string; spaces: string[] }) => {
- return addMemory.mutateAsync(params);
- };
-
- const wrappedDeleteMemory = async (memoryId: string) => {
- const promise = deleteMemory.mutateAsync(memoryId);
- toast.promise(promise, {
- loading: "Deleting memory...",
- success: "Memory deleted successfully",
- error: (err) => `Failed to delete memory: ${err.message}`,
- });
- return promise;
- };
-
- return {
- memories: memoriesData?.items ?? [],
- isLoading: isInitialLoading, // Only show loading state for initial fetch, not for fetchNextPage
- loadMore: () => {
- if (
- !fetchNextPage.isPending &&
- memoriesData?.nextCursor &&
- memoriesData?.nextCursor < memoriesData?.total
- ) {
- return fetchNextPage.mutateAsync();
- }
- return Promise.resolve();
- },
- hasMore: memoriesData ? memoriesData.nextCursor < memoriesData.total : false,
- total: memoriesData?.total ?? 0,
- addMemory: wrappedAddMemory,
- mutate: () => queryClient.invalidateQueries({ queryKey: cacheKey }),
- };
-}
diff --git a/apps/web/app/lib/hooks/use-mounted.ts b/apps/web/app/lib/hooks/use-mounted.ts
deleted file mode 100644
index 1bc7bde2..00000000
--- a/apps/web/app/lib/hooks/use-mounted.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import * as React from 'react';
-
-export function useMounted() {
- const [mounted, setMounted] = React.useState(false);
-
- React.useEffect(() => {
- setMounted(true);
- }, []);
-
- return mounted;
-}
diff --git a/apps/web/app/lib/hooks/use-spaces.tsx b/apps/web/app/lib/hooks/use-spaces.tsx
deleted file mode 100644
index 1654fd47..00000000
--- a/apps/web/app/lib/hooks/use-spaces.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import { Space } from "@supermemory/db/schema";
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { toast } from "sonner";
-
-export type ExtraSpaceMetaData = {
- permissions: {
- canRead: boolean;
- canEdit: boolean;
- isOwner: boolean;
- isPublic: boolean;
- };
- owner: {
- id: string;
- name: string;
- email: string;
- profileImage: string;
- } | null;
- favorited: boolean;
-};
-
-type SpaceResponse = {
- spaces: (Space & ExtraSpaceMetaData)[];
-};
-
-type CreateSpaceResponse = {
- message: string;
- space: {
- name: string;
- uuid: string;
- isPublic: boolean;
- };
-};
-
-export async function fetchSpaces(): Promise<SpaceResponse> {
- const response = await fetch(`/backend/v1/spaces`, {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- },
- credentials: "include",
- });
-
- if (!response.ok) {
- throw new Error("Failed to fetch spaces");
- }
-
- const resp = (await response.json()) as SpaceResponse;
-
- resp.spaces.push({
- id: 0,
- uuid: "<HOME>",
- name: "Home",
- createdAt: new Date(),
- updatedAt: new Date(),
- ownerId: 0,
- isPublic: false,
- permissions: {
- canRead: false,
- canEdit: false,
- isOwner: false,
- isPublic: false,
- },
- owner: null,
- favorited: false,
- });
-
- return resp;
-}
-
-async function createSpace(data: {
- spaceName: string;
- isPublic: boolean;
-}): Promise<CreateSpaceResponse> {
- const response = await fetch(`/backend/v1/spaces/create`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(data),
- credentials: "include",
- });
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error((error as { error: string }).error || "Failed to create space");
- }
-
- return response.json();
-}
-
-async function makeFavorite(spaceId: string) {
- const response = await fetch(`/backend/v1/spaces/favorite/${spaceId}`, {
- method: "POST",
- credentials: "include",
- });
-
- if (!response.ok) {
- throw new Error("Failed to make favorite");
- }
-
- return response.json();
-}
-
-export function useSpaces() {
- const queryClient = useQueryClient();
- const { data, isLoading, error, refetch } = useQuery({
- queryKey: ["spaces"],
- queryFn: fetchSpaces,
- staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
- });
-
- const createMutation = useMutation({
- mutationFn: async (data: { spaceName: string; isPublic: boolean }) => {
- return createSpace(data);
- },
- onSuccess: (data) => {
- toast.success(data.message);
- queryClient.invalidateQueries({ queryKey: ["spaces"] });
- },
- onError: (error: Error) => {
- console.error("Failed to create space", error);
- toast.error(error.message);
- },
- });
-
- return {
- spaces: data?.spaces ?? [],
- isLoading,
- error,
- refetch,
- createSpace: createMutation.mutate,
- isCreating: createMutation.isPending,
- createError: createMutation.error,
- };
-}
diff --git a/apps/web/app/lib/hooks/use-text-overflow.ts b/apps/web/app/lib/hooks/use-text-overflow.ts
deleted file mode 100644
index e1ba80c0..00000000
--- a/apps/web/app/lib/hooks/use-text-overflow.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useCallback, useEffect, useRef, useState } from "react";
-
-export const useTextOverflow = (content: string, lineCount: number) => {
- const [showFade, setShowFade] = useState(false);
- const contentRef = useRef<HTMLParagraphElement>(null);
-
- const checkOverflow = useCallback(() => {
- if (contentRef.current) {
- const lineHeight = parseInt(window.getComputedStyle(contentRef.current).lineHeight);
- const height = contentRef.current.offsetHeight;
- setShowFade(height > lineHeight * lineCount);
- }
- }, [lineCount]);
-
- useEffect(() => {
- checkOverflow();
- window.addEventListener("resize", checkOverflow);
- return () => window.removeEventListener("resize", checkOverflow);
- }, [checkOverflow, content]);
-
- return { contentRef, showFade };
-};
diff --git a/apps/web/app/lib/hooks/use-upload-file.ts b/apps/web/app/lib/hooks/use-upload-file.ts
deleted file mode 100644
index fa39fd77..00000000
--- a/apps/web/app/lib/hooks/use-upload-file.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { useFetcher } from "@remix-run/react";
-import { useFetcherWithPromise } from "./use-fetcher-with-promise";
-
-export function useUploadFile() {
- const fetcher = useFetcherWithPromise<{ url: string, error?: string }>();
-
- const uploadFile = async (file: File) => {
- const formData = new FormData();
- formData.append("file", file);
- formData.append("name", file.name);
- const response = await fetcher.submit(formData, {
- method: "post",
- action: "/action/upload",
- encType: "multipart/form-data",
- })
-
- return {
- url: response?.url,
- error: response?.error,
- };
- };
-
- const isUploading = fetcher.state === "submitting" || fetcher.state === "loading";
-
- return {
- uploadFile,
- isUploading,
- ...fetcher,
- };
-}
diff --git a/apps/web/app/lib/misc.ts b/apps/web/app/lib/misc.ts
deleted file mode 100644
index 40a47a15..00000000
--- a/apps/web/app/lib/misc.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import process from "node:process";
-
-function getRequiredEnvVarFromObj(
- obj: Record<string, string | undefined>,
- key: string,
- devValue: string = `${key}-dev-value`,
-) {
- let value = devValue;
- const envVal = obj[key];
- if (envVal) {
- value = envVal;
- } else if (obj.NODE_ENV === "production") {
- throw new Error(`${key} is a required env variable`);
- }
- return value;
-}
-
-export function getRequiredServerEnvVar(key: string, devValue: string = `${key}-dev-value`) {
- return getRequiredEnvVarFromObj(process.env, key, devValue);
-}
diff --git a/apps/web/app/lib/stripe.constants.ts b/apps/web/app/lib/stripe.constants.ts
deleted file mode 100644
index 02d224ea..00000000
--- a/apps/web/app/lib/stripe.constants.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import Stripe from "stripe";
-
-export const allowedEvents = [
- "checkout.session.completed",
- "customer.subscription.created",
- "customer.subscription.updated",
- "customer.subscription.deleted",
- "customer.subscription.paused",
- "customer.subscription.resumed",
- "customer.subscription.pending_update_applied",
- "customer.subscription.pending_update_expired",
- "customer.subscription.trial_will_end",
- "invoice.paid",
- "invoice.payment_failed",
- "invoice.payment_action_required",
- "invoice.upcoming",
- "invoice.marked_uncollectible",
- "invoice.payment_succeeded",
- "payment_intent.succeeded",
- "payment_intent.payment_failed",
- "payment_intent.canceled",
-] as Stripe.Event.Type[];
-
-export type STRIPE_SUB_CACHE =
- | {
- subscriptionId: string | null;
- status: Stripe.Subscription.Status;
- priceId: string | null;
- currentPeriodStart: number | null;
- currentPeriodEnd: number | null;
- cancelAtPeriodEnd: boolean;
- paymentMethod: {
- brand: string | null; // e.g., "visa", "mastercard"
- last4: string | null; // e.g., "4242"
- } | null;
- }
- | {
- status: "none";
- };
diff --git a/apps/web/app/lib/stripe.ts b/apps/web/app/lib/stripe.ts
deleted file mode 100644
index fe6100e3..00000000
--- a/apps/web/app/lib/stripe.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { database, eq } from "@supermemory/db";
-import { users } from "@supermemory/db/schema";
-import { Stripe } from "stripe";
-import { allowedEvents } from "./stripe.constants";
-
-export async function syncStripeDataToDb(
- userId: string,
- db: ReturnType<typeof database>,
- env: Env,
-) {
- const stripe = new Stripe(env.STRIPE_CHECKOUT_KEY);
-
- const user = await db.query.users.findFirst({
- where: eq(users.uuid, userId),
- });
-
- if (!user || !user.stripeCustomerId) {
- return;
- }
-
- // Get latest subscription data
- const subscriptions = await stripe.subscriptions.list({
- customer: user.stripeCustomerId,
- limit: 1,
- status: "all",
- expand: ["data.default_payment_method"],
- });
-
- // Get one-time purchases
- const charges = await stripe.charges.list({
- customer: user.stripeCustomerId,
- limit: 100,
- });
-
- // Default to free tier
- let tier: "free" | "premium" = "free";
-
- // Check subscriptions first
- if (subscriptions.data.length > 0) {
- const subscription = subscriptions.data[0];
- if (subscription.status === "active") {
- tier = "premium";
- }
- }
-
- // Check one-time purchases if no active subscription
- if (tier === "free" && charges.data.length > 0) {
- // Look for successful lifetime purchase
- const lifetimePurchase = charges.data.find((charge) => charge.paid && charge.amount >= 19900);
- if (lifetimePurchase) {
- tier = "premium";
- }
- }
-
- // Update user tier in database
- await db
- .update(users)
- .set({
- tier,
- updatedAt: new Date(),
- })
- .where(eq(users.uuid, userId));
-
- return {
- tier,
- customerId: user.stripeCustomerId,
- subscription: subscriptions.data[0] || null,
- charges: charges.data,
- };
-} \ No newline at end of file
diff --git a/apps/web/app/lib/theme-provider.tsx b/apps/web/app/lib/theme-provider.tsx
deleted file mode 100644
index 291a71d3..00000000
--- a/apps/web/app/lib/theme-provider.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
-import type { Dispatch, ReactNode, SetStateAction } from "react";
-
-import { useFetcher } from "@remix-run/react";
-
-enum Theme {
- DARK = "dark",
- LIGHT = "light",
-}
-const themes: Array<Theme> = Object.values(Theme);
-
-type ThemeContextType = [Theme | null, Dispatch<SetStateAction<Theme | null>>];
-
-const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
-
-const prefersLightMQ = "(prefers-color-scheme: light)";
-const getPreferredTheme = () =>
- window.matchMedia(prefersLightMQ).matches ? Theme.LIGHT : Theme.DARK;
-
-function ThemeProvider({
- children,
- specifiedTheme,
-}: {
- children: ReactNode;
- specifiedTheme: Theme | null;
-}) {
- const [theme, setTheme] = useState<Theme | null>(() => {
- if (specifiedTheme) {
- if (themes.includes(specifiedTheme)) {
- return specifiedTheme;
- } else {
- return null;
- }
- }
-
- if (typeof window !== "object") {
- return null;
- }
-
- return getPreferredTheme();
- });
-
- const persistTheme = useFetcher();
-
- const mountRun = useRef(false);
-
- useEffect(() => {
- if (!mountRun.current) {
- mountRun.current = true;
- return;
- }
- if (!theme) {
- return;
- }
-
- persistTheme.submit({ theme }, { action: "action/set-theme", method: "post" });
- }, [theme]);
-
- useEffect(() => {
- const mediaQuery = window.matchMedia(prefersLightMQ);
- const handleChange = () => {
- setTheme(mediaQuery.matches ? Theme.LIGHT : Theme.DARK);
- };
- mediaQuery.addEventListener("change", handleChange);
- return () => mediaQuery.removeEventListener("change", handleChange);
- }, []);
-
- const contextValue = useMemo<ThemeContextType>(() => [theme, setTheme], [theme, setTheme]);
-
- return <ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>;
-}
-
-const clientThemeCode = `
-// hi there dear reader 👋
-// this is how I make certain we avoid a flash of the wrong theme. If you select
-// a theme, then I'll know what you want in the future and you'll not see this
-// script anymore.
-;(() => {
- const theme = window.matchMedia(${JSON.stringify(prefersLightMQ)}).matches
- ? 'light'
- : 'dark';
-
- const cl = document.documentElement.classList;
-
- const themeAlreadyApplied = cl.contains('light') || cl.contains('dark');
- if (themeAlreadyApplied) {
- // this script shouldn't exist if the theme is already applied!
- console.warn(
- "Hi there, could you let Matt know you're seeing this message? Thanks!",
- );
- } else {
- cl.add(theme);
- }
-
- const meta = document.querySelector('meta[name=color-scheme]');
- if (meta) {
- if (theme === 'dark') {
- meta.content = 'dark light';
- } else if (theme === 'light') {
- meta.content = 'light dark';
- }
- } else {
- console.warn(
- "Hey, could you let Matt know you're seeing this message? Thanks!",
- );
- }
-})();
-`;
-
-function NonFlashOfWrongThemeEls({ ssrTheme }: { ssrTheme: boolean }) {
- const [theme] = useTheme();
-
- return (
- <>
- <meta name="color-scheme" content={theme === "light" ? "light dark" : "dark light"} />
- {ssrTheme ? null : <script dangerouslySetInnerHTML={{ __html: clientThemeCode }} />}
- </>
- );
-}
-
-function useTheme() {
- const context = useContext(ThemeContext);
- if (context === undefined) {
- throw new Error("useTheme must be used within a ThemeProvider");
- }
- return context;
-}
-
-function isTheme(value: unknown): value is Theme {
- return typeof value === "string" && themes.includes(value as Theme);
-}
-
-export { isTheme, NonFlashOfWrongThemeEls, Theme, ThemeProvider, useTheme };
diff --git a/apps/web/app/lib/theme.server.ts b/apps/web/app/lib/theme.server.ts
deleted file mode 100644
index a9b520f3..00000000
--- a/apps/web/app/lib/theme.server.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { createCookieSessionStorage } from "@remix-run/cloudflare";
-
-import { Theme, isTheme } from "./theme-provider";
-
-const themeStorage = createCookieSessionStorage({
- cookie: {
- name: "remix__theme",
- secure: true,
- secrets: ['theme-secret-sm'],
- sameSite: "lax",
- path: "/",
- httpOnly: true,
- },
-});
-
-async function getThemeSession(request: Request) {
- const session = await themeStorage.getSession(request.headers.get("Cookie"));
- return {
- getTheme: () => {
- const themeValue = session.get("theme");
- return isTheme(themeValue) ? themeValue : Theme.DARK;
- },
- setTheme: (theme: Theme) => session.set("theme", theme),
- commit: () => themeStorage.commitSession(session),
- };
-}
-
-export { getThemeSession };
diff --git a/apps/web/app/lib/types/memory.ts b/apps/web/app/lib/types/memory.ts
deleted file mode 100644
index 9ac3dc5d..00000000
--- a/apps/web/app/lib/types/memory.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { documents } from "@supermemory/db/schema";
-import { memoryTypes } from "~/lib/constants/typeIcons";
-
-export type Memory = typeof documents.$inferSelect;
-
-export interface WebsiteMetadata {
- title: string;
- description: string;
- image: string;
- dominantColor: string;
- isDark: boolean;
-}
diff --git a/apps/web/app/lib/types/safety.ts b/apps/web/app/lib/types/safety.ts
deleted file mode 100644
index 6e8b8688..00000000
--- a/apps/web/app/lib/types/safety.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { UserContent } from "ai";
-
-export function assertNotString(value: UserContent) {
- if (typeof value === "string") {
- throw new Error("Value is a string");
- }
- return value;
-}
diff --git a/apps/web/app/lib/utils.ts b/apps/web/app/lib/utils.ts
deleted file mode 100644
index ac680b30..00000000
--- a/apps/web/app/lib/utils.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { type ClassValue, clsx } from "clsx";
-import { twMerge } from "tailwind-merge";
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
-}
diff --git a/apps/web/app/lib/utils/metadata.ts b/apps/web/app/lib/utils/metadata.ts
deleted file mode 100644
index e27f45a7..00000000
--- a/apps/web/app/lib/utils/metadata.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export async function fetchWebsiteMetadata(url: string) {
- // Implement logic to fetch metadata from the given URL
- // This could involve making an API call to a service that provides metadata for websites
- // Return an object with title, description, and image properties
-}
diff --git a/apps/web/app/lib/utils/tweet.ts b/apps/web/app/lib/utils/tweet.ts
deleted file mode 100644
index cb349ef6..00000000
--- a/apps/web/app/lib/utils/tweet.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import type { Tweet } from "react-tweet/api";
-
-export const getRawTweet = (tweet: string) => {
- // Get the content inside the last <raw> tag, there can any number of <raw> tags in the tweet (or just one)
- const rawTag = /<raw>(.*)<\/raw>/g;
- const match = rawTag.exec(tweet);
- if (match) {
- return match[1];
- }
- return `{
- "error": "No <raw> tag found"
- }`;
-};
-
-export const getTweet = (tweet: string) => {
- const rawTweet = getRawTweet(tweet);
- try {
- return JSON.parse(rawTweet) as Tweet;
- } catch (e) {
- return { error: "Error parsing tweet from text" };
- }
-};
diff --git a/apps/web/app/manifest.ts b/apps/web/app/manifest.ts
new file mode 100644
index 00000000..17611deb
--- /dev/null
+++ b/apps/web/app/manifest.ts
@@ -0,0 +1,20 @@
+import type { MetadataRoute } from 'next'
+
+export default function manifest(): MetadataRoute.Manifest {
+ return {
+ name: 'Supermemory',
+ short_name: 'supermemory',
+ description: 'Your memories, wherever you are',
+ start_url: '/',
+ display: 'standalone',
+ background_color: '#ffffff',
+ theme_color: '#000000',
+ icons: [
+ {
+ src: '/images/logo.png',
+ sizes: '192x192',
+ type: 'image/png',
+ }
+ ],
+ }
+} \ No newline at end of file
diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx
new file mode 100644
index 00000000..98cf6b58
--- /dev/null
+++ b/apps/web/app/not-found.tsx
@@ -0,0 +1,27 @@
+"use client" // Error boundaries must be Client Components
+
+import { Button } from "@ui/components/button"
+import { Title1Bold } from "@ui/text/title/title-1-bold"
+import { useRouter } from "next/navigation"
+import { useEffect } from "react"
+
+export default function NotFound({
+ error,
+}: {
+ error: Error & { digest?: string }
+}) {
+ const router = useRouter()
+ useEffect(() => {
+ // Log the error to an error reporting service
+ console.error(error)
+ }, [error])
+
+ return (
+ <html lang="en">
+ <body className="flex flex-col items-center justify-center h-screen">
+ <Title1Bold>Page not found</Title1Bold>
+ <Button onClick={() => router.back()}>Go back</Button>
+ </body>
+ </html>
+ )
+}
diff --git a/apps/web/app/opengraph-image.png b/apps/web/app/opengraph-image.png
new file mode 100644
index 00000000..5fbdf22c
--- /dev/null
+++ b/apps/web/app/opengraph-image.png
Binary files differ
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
new file mode 100644
index 00000000..05b32a15
--- /dev/null
+++ b/apps/web/app/page.tsx
@@ -0,0 +1,681 @@
+"use client"
+
+import { useIsMobile } from "@hooks/use-mobile";
+import { useAuth } from "@lib/auth-context";
+import { $fetch } from "@repo/lib/api";
+import { MemoryGraph } from "@repo/ui/memory-graph";
+import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api";
+import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
+import { Logo, LogoFull } from "@ui/assets/Logo";
+import { GlassMenuEffect } from "@ui/other/glass-effect";
+import { Button } from "@ui/components/button";
+import { Gift, LayoutGrid, List, LoaderIcon, MessageSquare, Unplug } from "lucide-react";
+import { AnimatePresence, motion } from "motion/react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import type { z } from "zod";
+import { MemoryListView } from "@/components/memory-list-view";
+import Menu from "@/components/menu";
+import type { TourStep } from "@/components/tour";
+import { TourAlertDialog, useTour } from "@/components/tour";
+import { useProject } from "@/stores";
+import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants";
+import { useViewMode } from "@/lib/view-mode-context";
+import { useChatOpen } from "@/stores";
+import { ChatRewrite } from "@/components/views/chat";
+import { useGraphHighlights } from "@/stores/highlights";
+import { ProjectSelector } from "@/components/project-selector";
+import { AddMemoryView } from "@/components/views/add-memory";
+import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal";
+import { ConnectAIModal } from "@/components/connect-ai-modal";
+import { InstallPrompt } from "@/components/install-prompt";
+
+type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
+type DocumentWithMemories = DocumentsResponse["documents"][0]
+
+const MemoryGraphPage = () => {
+ const { documentIds: allHighlightDocumentIds } = useGraphHighlights();
+ const isMobile = useIsMobile();
+ const { viewMode, setViewMode, isInitialized } = useViewMode();
+ const { selectedProject } = useProject();
+ const { setSteps, isTourCompleted } = useTour();
+ const { isOpen, setIsOpen } = useChatOpen();
+ const [injectedDocs, setInjectedDocs] = useState<DocumentWithMemories[]>([])
+ const [showAddMemoryView, setShowAddMemoryView] = useState(false)
+ const [showReferralModal, setShowReferralModal] = useState(false)
+
+ // Fetch projects meta to detect experimental flag
+ const { data: projectsMeta = [] } = useQuery({
+ queryKey: ["projects"],
+ queryFn: async () => {
+ const response = await $fetch("@get/projects")
+ return response.data?.projects ?? []
+ },
+ staleTime: 5 * 60 * 1000,
+ })
+
+ const isCurrentProjectExperimental = !!projectsMeta.find(
+ (p: any) => p.containerTag === selectedProject,
+ )?.isExperimental
+
+ // Tour state
+ const [showTourDialog, setShowTourDialog] = useState(false)
+
+ // Define tour steps with useMemo to prevent recreation
+ const tourSteps: TourStep[] = useMemo(() => {
+ return [
+ {
+ content: (
+ <div>
+ <h3 className="font-semibold text-lg mb-2 text-white">
+ Memories Overview
+ </h3>
+ <p className="text-gray-200">
+ This is your memory graph. Each node represents a memory, and
+ connections show relationships between them.
+ </p>
+ </div>
+ ),
+ selectorId: TOUR_STEP_IDS.MEMORY_GRAPH,
+ position: "center",
+ },
+ {
+ content: (
+ <div>
+ <h3 className="font-semibold text-lg mb-2 text-white">
+ Add Memories
+ </h3>
+ <p className="text-gray-200">
+ Click here to add new memories to your knowledge base. You can add
+ text, links, or connect external sources.
+ </p>
+ </div>
+ ),
+ selectorId: TOUR_STEP_IDS.MENU_ADD_MEMORY,
+ position: "right",
+ },
+ {
+ content: (
+ <div>
+ <h3 className="font-semibold text-lg mb-2 text-white">
+ Connections
+ </h3>
+ <p className="text-gray-200">
+ Connect your external accounts like Google Drive, Notion, or
+ OneDrive to automatically sync and organize your content.
+ </p>
+ </div>
+ ),
+ selectorId: TOUR_STEP_IDS.MENU_CONNECTIONS,
+ position: "right",
+ },
+ {
+ content: (
+ <div>
+ <h3 className="font-semibold text-lg mb-2 text-white">Projects</h3>
+ <p className="text-gray-200">
+ Organize your memories into projects. Switch between different
+ contexts easily.
+ </p>
+ </div>
+ ),
+ selectorId: TOUR_STEP_IDS.MENU_PROJECTS,
+ position: "right",
+ },
+ {
+ content: (
+ <div>
+ <h3 className="font-semibold text-lg mb-2 text-white">
+ MCP Servers
+ </h3>
+ <p className="text-gray-200">
+ Access Model Context Protocol servers to give AI tools access to
+ your memories securely.
+ </p>
+ </div>
+ ),
+ selectorId: TOUR_STEP_IDS.MENU_MCP,
+ position: "right",
+ },
+ {
+ content: (
+ <div>
+ <h3 className="font-semibold text-lg mb-2 text-white">Billing</h3>
+ <p className="text-gray-200">
+ Manage your subscription and billing information.
+ </p>
+ </div>
+ ),
+ selectorId: TOUR_STEP_IDS.MENU_BILLING,
+ position: "right",
+ },
+ {
+ content: (
+ <div>
+ <h3 className="font-semibold text-lg mb-2 text-white">
+ View Toggle
+ </h3>
+ <p className="text-gray-200">
+ Switch between graph view and list view to see your memories in
+ different ways.
+ </p>
+ </div>
+ ),
+ selectorId: TOUR_STEP_IDS.VIEW_TOGGLE,
+ position: "left",
+ },
+ {
+ content: (
+ <div>
+ <h3 className="font-semibold text-lg mb-2 text-white">Legend</h3>
+ <p className="text-gray-200">
+ Understand the different types of nodes and connections in your
+ memory graph.
+ </p>
+ </div>
+ ),
+ selectorId: TOUR_STEP_IDS.LEGEND,
+ position: "left",
+ },
+ {
+ content: (
+ <div>
+ <h3 className="font-semibold text-lg mb-2 text-white">
+ Chat Assistant
+ </h3>
+ <p className="text-gray-200">
+ Ask questions or add new memories using our AI-powered chat
+ interface.
+ </p>
+ </div>
+ ),
+ selectorId: TOUR_STEP_IDS.FLOATING_CHAT,
+ position: "left",
+ },
+ ]
+ }, [])
+
+ // Check if tour has been completed before
+ useEffect(() => {
+ const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true"
+ if (!hasCompletedTour && !isTourCompleted) {
+ const timer = setTimeout(() => {
+ setShowTourDialog(true)
+ }, 1000) // Show after 1 second
+ return () => clearTimeout(timer)
+ }
+ }, [isTourCompleted])
+
+ // Set up tour steps
+ useEffect(() => {
+ setSteps(tourSteps)
+ }, [setSteps, tourSteps])
+
+ // Save tour completion to localStorage
+ useEffect(() => {
+ if (isTourCompleted) {
+ localStorage.setItem(TOUR_STORAGE_KEY, "true")
+ }
+ }, [isTourCompleted])
+
+ // Progressive loading via useInfiniteQuery
+ const IS_DEV = process.env.NODE_ENV === "development"
+ const PAGE_SIZE = IS_DEV ? 3 : 100
+ const MAX_TOTAL = 1000
+
+ const {
+ data,
+ error,
+ isPending,
+ isFetchingNextPage,
+ hasNextPage,
+ fetchNextPage,
+ } = useInfiniteQuery<DocumentsResponse, Error>({
+ queryKey: ["documents-with-memories", selectedProject],
+ initialPageParam: 1,
+ queryFn: async ({ pageParam }) => {
+ const response = await $fetch("@post/memories/documents", {
+ body: {
+ page: pageParam as number,
+ limit: (pageParam as number) === 1 ? (IS_DEV ? 3 : 500) : PAGE_SIZE,
+ sort: "createdAt",
+ order: "desc",
+ containerTags: selectedProject ? [selectedProject] : undefined,
+ },
+ disableValidation: true,
+ })
+
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to fetch documents")
+ }
+
+ return response.data
+ },
+ getNextPageParam: (lastPage, allPages) => {
+ const loaded = allPages.reduce(
+ (acc, p) => acc + (p.documents?.length ?? 0),
+ 0,
+ )
+ if (loaded >= MAX_TOTAL) return undefined
+
+ const { currentPage, totalPages } = lastPage.pagination
+ if (currentPage < totalPages) {
+ return currentPage + 1
+ }
+ return undefined
+ },
+ staleTime: 5 * 60 * 1000,
+ })
+
+ const baseDocuments = useMemo(() => {
+ return data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? []
+ }, [data])
+
+ const allDocuments = useMemo(() => {
+ if (injectedDocs.length === 0) return baseDocuments
+ const byId = new Map<string, DocumentWithMemories>()
+ for (const d of injectedDocs) byId.set(d.id, d)
+ for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d)
+ return Array.from(byId.values())
+ }, [baseDocuments, injectedDocs])
+
+ const totalLoaded = allDocuments.length
+ const hasMore = hasNextPage
+ const isLoadingMore = isFetchingNextPage
+
+ const loadMoreDocuments = useCallback(async (): Promise<void> => {
+ if (hasNextPage && !isFetchingNextPage) {
+ await fetchNextPage()
+ return
+ }
+ return
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage])
+
+ // Reset injected docs when project changes
+ useEffect(() => { setInjectedDocs([]) }, [selectedProject])
+
+ // Surgical fetch of missing highlighted documents (customId-based IDs from search)
+ useEffect(() => {
+ if (!isOpen) return
+ if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0) return
+ const present = new Set<string>()
+ for (const d of [...baseDocuments, ...injectedDocs]) {
+ if (d.id) present.add(d.id)
+ if ((d as any).customId) present.add((d as any).customId as string)
+ }
+ const missing = allHighlightDocumentIds.filter((id: string) => !present.has(id))
+ if (missing.length === 0) return
+ let cancelled = false
+ const run = async () => {
+ try {
+ const resp = await $fetch("@post/memories/documents/by-ids", {
+ body: {
+ ids: missing,
+ by: "customId",
+ containerTags: selectedProject ? [selectedProject] : undefined,
+ },
+ disableValidation: true,
+ })
+ if (cancelled || (resp as any)?.error) return
+ const extraDocs = (resp as any)?.data?.documents as DocumentWithMemories[] | undefined
+ if (!extraDocs || extraDocs.length === 0) return
+ setInjectedDocs((prev) => {
+ const seen = new Set<string>([...prev.map((d) => d.id), ...baseDocuments.map((d) => d.id)])
+ const merged = [...prev]
+ for (const doc of extraDocs) {
+ if (!seen.has(doc.id)) {
+ merged.push(doc)
+ seen.add(doc.id)
+ }
+ }
+ return merged
+ })
+ } catch { }
+ }
+ void run()
+ return () => { cancelled = true }
+ }, [isOpen, allHighlightDocumentIds.join('|'), baseDocuments, injectedDocs, selectedProject, $fetch])
+
+ // Handle view mode change
+ const handleViewModeChange = useCallback(
+ (mode: "graph" | "list") => {
+ setViewMode(mode)
+ },
+ [setViewMode],
+ )
+
+ // Prevent body scrolling
+ useEffect(() => {
+ document.body.style.overflow = 'hidden'
+ document.body.style.height = '100vh'
+ document.documentElement.style.overflow = 'hidden'
+ document.documentElement.style.height = '100vh'
+
+ return () => {
+ document.body.style.overflow = ''
+ document.body.style.height = ''
+ document.documentElement.style.overflow = ''
+ document.documentElement.style.height = ''
+ }
+ }, [])
+
+ return (
+ <div className="relative h-screen bg-[#0f1419] overflow-hidden touch-none">
+ {/* Main content area */}
+ <motion.div
+ animate={{
+ marginRight: isOpen && !isMobile ? 600 : 0,
+ }}
+ className="h-full relative"
+ transition={{
+ duration: 0.2,
+ ease: [0.4, 0, 0.2, 1], // Material Design easing - snappy but smooth
+ }}
+ >
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="absolute md:top-4 md:right-4 md:bottom-auto md:left-auto bottom-8 left-6 z-20 rounded-xl overflow-hidden"
+ id={TOUR_STEP_IDS.VIEW_TOGGLE}
+ initial={{ opacity: 0, y: -20 }}
+ transition={{ type: "spring", stiffness: 300, damping: 25 }}
+ >
+ <GlassMenuEffect rounded="rounded-xl" />
+ <div className="relative z-10 p-2 flex gap-1">
+ <motion.button
+ animate={{
+ color: viewMode === "graph" ? "#93c5fd" : "#cbd5e1",
+ }}
+ className="relative h-8 px-3 flex items-center gap-2 text-sm font-medium rounded-md transition-colors"
+ onClick={() => handleViewModeChange("graph")}
+ transition={{ duration: 0.2 }}
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ >
+ {viewMode === "graph" && (
+ <motion.div
+ className="absolute inset-0 bg-blue-500/20 rounded-md"
+ layoutId="activeBackground"
+ transition={{
+ type: "spring",
+ stiffness: 400,
+ damping: 30,
+ }}
+ />
+ )}
+ <span className="relative z-10 flex items-center gap-2">
+ <LayoutGrid className="w-4 h-4" />
+ <span className="hidden md:inline">Graph</span>
+ </span>
+ </motion.button>
+
+ <motion.button
+ animate={{
+ color: viewMode === "list" ? "#93c5fd" : "#cbd5e1",
+ }}
+ className="relative h-8 px-3 flex items-center gap-2 text-sm font-medium rounded-md transition-colors"
+ onClick={() => handleViewModeChange("list")}
+ transition={{ duration: 0.2 }}
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ >
+ {viewMode === "list" && (
+ <motion.div
+ className="absolute inset-0 bg-blue-500/20 rounded-md"
+ layoutId="activeBackground"
+ transition={{
+ type: "spring",
+ stiffness: 400,
+ damping: 30,
+ }}
+ />
+ )}
+ <span className="relative z-10 flex items-center gap-2">
+ <List className="w-4 h-4" />
+ <span className="hidden md:inline">List</span>
+ </span>
+ </motion.button>
+ </div>
+ </motion.div>
+
+ {/* Animated content switching */}
+ <AnimatePresence mode="wait">
+ {viewMode === "graph" ? (
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ className="absolute inset-0"
+ exit={{ opacity: 0, scale: 0.95 }}
+ id={TOUR_STEP_IDS.MEMORY_GRAPH}
+ initial={{ opacity: 0, scale: 0.95 }}
+ key="graph"
+ transition={{
+ type: "spring",
+ stiffness: 500,
+ damping: 30,
+ }}
+ >
+ <MemoryGraph
+ documents={allDocuments}
+ error={error}
+ hasMore={hasMore}
+ isLoading={isPending}
+ isLoadingMore={isLoadingMore}
+ legendId={TOUR_STEP_IDS.LEGEND}
+ loadMoreDocuments={loadMoreDocuments}
+ showSpacesSelector={false}
+ totalLoaded={totalLoaded}
+ variant="consumer"
+ highlightDocumentIds={allHighlightDocumentIds}
+ highlightsVisible={isOpen}
+ occludedRightPx={isOpen && !isMobile ? 600 : 0}
+ autoLoadOnViewport={false}
+ isExperimental={isCurrentProjectExperimental}
+ >
+ <div className="absolute inset-0 flex items-center justify-center">
+ <div className="rounded-xl overflow-hidden">
+ <div className="relative z-10 text-slate-200 px-6 py-4 text-center">
+ <p className="text-lg font-medium mb-2">No Memories to Visualize</p>
+ <button
+ type="button"
+ className="text-sm text-blue-400 hover:text-blue-300 transition-colors cursor-pointer underline"
+ onClick={() => setShowAddMemoryView(true)}
+ >
+ Create one?
+ </button>
+ </div>
+ </div>
+ </div>
+ </MemoryGraph>
+ </motion.div>
+ ) : (
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ className="absolute inset-0 md:ml-18"
+ exit={{ opacity: 0, scale: 0.95 }}
+ id={TOUR_STEP_IDS.MEMORY_LIST}
+ initial={{ opacity: 0, scale: 0.95 }}
+ key="list"
+ transition={{
+ type: "spring",
+ stiffness: 500,
+ damping: 30,
+ }}
+ >
+ <MemoryListView
+ documents={allDocuments}
+ error={error}
+ hasMore={hasMore}
+ isLoading={isPending}
+ isLoadingMore={isLoadingMore}
+ loadMoreDocuments={loadMoreDocuments}
+ totalLoaded={totalLoaded}
+ >
+ <div className="absolute inset-0 flex items-center justify-center">
+ <div className="rounded-xl overflow-hidden">
+ <div className="relative z-10 text-slate-200 px-6 py-4 text-center">
+ <p className="text-lg font-medium mb-2">No Memories to Visualize</p>
+ <button
+ className="text-sm text-blue-400 hover:text-blue-300 transition-colors cursor-pointer underline"
+ onClick={() => setShowAddMemoryView(true)}
+ type="button"
+ >
+ Create one?
+ </button>
+ </div>
+ </div>
+ </div>
+ </MemoryListView>
+ </motion.div>
+ )}
+ </AnimatePresence>
+
+ {/* Top Bar */}
+ <div className="absolute top-2 left-0 right-0 z-10 p-4 flex items-center justify-between">
+ <div className="flex items-center gap-3 justify-between w-full md:w-fit md:justify-start">
+ <Link
+ className="pointer-events-auto"
+ href="https://supermemory.ai"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ <LogoFull className="h-8 hidden md:block" id={TOUR_STEP_IDS.LOGO} />
+ <Logo className="h-8 md:hidden" id={TOUR_STEP_IDS.LOGO} />
+ </Link>
+
+ <div className="hidden sm:block">
+ <ProjectSelector />
+ </div>
+
+ <ConnectAIModal>
+ <Button
+ variant="outline"
+ size="sm"
+ className="bg-white/5 hover:bg-white/10 border-white/20 text-white hover:text-white px-2 sm:px-3"
+ >
+ <Unplug className="h-4 w-4" />
+ <span className="hidden sm:inline ml-2">Connect to your AI</span>
+ <span className="sm:hidden ml-1">Connect AI</span>
+ </Button>
+ </ConnectAIModal>
+ </div>
+
+ <div>
+ <Menu />
+ </div>
+ </div>
+
+ {/* Floating Open Chat Button */}
+ {!isOpen && !isMobile && (
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ className="fixed bottom-6 right-6 z-50"
+ initial={{ opacity: 0, scale: 0.8 }}
+ transition={{
+ type: "spring",
+ stiffness: 300,
+ damping: 25,
+ }}
+ >
+ <Button
+ className="h-14 px-4 bg-blue-600 hover:bg-blue-700 text-white shadow-lg hover:shadow-xl transition-all duration-200 rounded-full flex items-center gap-2"
+ onClick={() => setIsOpen(true)}
+ size="lg"
+ >
+ <MessageSquare className="h-5 w-5" />
+ <span className="font-medium">Open Chat</span>
+ </Button>
+ </motion.div>
+ )}
+ </motion.div>
+
+ {/* Chat panel - positioned absolutely */}
+ <motion.div
+ className="fixed top-0 right-0 h-full z-50 md:z-auto"
+ style={{
+ width: isOpen ? (isMobile ? "100vw" : "600px") : 0,
+ pointerEvents: isOpen ? "auto" : "none",
+ }}
+ id={TOUR_STEP_IDS.FLOATING_CHAT}
+ >
+ <motion.div
+ animate={{ x: isOpen ? 0 : (isMobile ? "100%" : 600) }}
+ className="absolute inset-0"
+ exit={{ x: isMobile ? "100%" : 600 }}
+ initial={{ x: isMobile ? "100%" : 600 }}
+ key="chat"
+ transition={{
+ type: "spring",
+ stiffness: 500,
+ damping: 40,
+ }}>
+ <ChatRewrite />
+ </motion.div>
+ </motion.div>
+
+ {showAddMemoryView && (
+ <AddMemoryView
+ initialTab="note"
+ onClose={() => setShowAddMemoryView(false)}
+ />
+ )}
+
+ {/* Tour Alert Dialog */}
+ <TourAlertDialog onOpenChange={setShowTourDialog} open={showTourDialog} />
+
+ {/* Referral/Upgrade Modal */}
+ <ReferralUpgradeModal
+ isOpen={showReferralModal}
+ onClose={() => setShowReferralModal(false)}
+ />
+ </div>
+ )
+}
+
+// Wrapper component to handle auth and waitlist checks
+export default function Page() {
+ const router = useRouter()
+ const { user } = useAuth()
+
+ // Check waitlist status
+ const {
+ data: waitlistStatus,
+ isLoading: isCheckingWaitlist,
+ error: waitlistError,
+ } = useQuery({
+ queryKey: ["waitlist-status", user?.id],
+ queryFn: async () => {
+ try {
+ const response = await $fetch("@get/waitlist/status")
+ return response.data
+ } catch (error) {
+ console.error("Error checking waitlist status:", error)
+ // Return null to indicate error, will handle in useEffect
+ return null
+ }
+ },
+ enabled: !!user && !user.isAnonymous,
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ retry: 1, // Only retry once on failure
+ })
+
+ useEffect(() => {
+ if (waitlistStatus && !waitlistStatus.accessGranted) {
+ router.push("/waitlist")
+ }
+ }, [])
+
+ // Show loading state while checking authentication and waitlist status
+ if (!user || isCheckingWaitlist) {
+ return (
+ <div className="min-h-screen flex items-center justify-center bg-[#0f1419]">
+ <div className="flex flex-col items-center gap-4">
+ <LoaderIcon className="w-8 h-8 text-orange-500 animate-spin" />
+ <p className="text-white/60">Loading...</p>
+ </div>
+ </div>
+ )
+ }
+
+ // If we have a user and they have access, show the main component
+ return <><MemoryGraphPage /><InstallPrompt /></>;
+}
diff --git a/apps/web/app/ref/[code]/page.tsx b/apps/web/app/ref/[code]/page.tsx
new file mode 100644
index 00000000..d1456152
--- /dev/null
+++ b/apps/web/app/ref/[code]/page.tsx
@@ -0,0 +1,227 @@
+"use client"
+
+import { $fetch } from "@lib/api"
+import { Button } from "@ui/components/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@ui/components/card"
+import { CheckIcon, CopyIcon, LoaderIcon, ShareIcon } from "lucide-react"
+import Link from "next/link"
+import { useRouter, useParams } from "next/navigation"
+import { useEffect, useState } from "react"
+import { toast } from "sonner"
+
+export default function ReferralPage() {
+ const router = useRouter()
+ const params = useParams()
+ const referralCode = params.code as string
+
+ const [isJoiningWaitlist, setIsJoiningWaitlist] = useState(false)
+ const [isLoading, setIsLoading] = useState(true)
+ const [referralData, setReferralData] = useState<{
+ referrerName?: string
+ valid: boolean
+ } | null>(null)
+ const [copiedLink, setCopiedLink] = useState(false)
+
+ const referralLink = `https://supermemory.ai/ref/${referralCode}`
+
+ // Verify referral code and get referrer info
+ useEffect(() => {
+ async function checkReferral() {
+ if (!referralCode) {
+ setIsLoading(false)
+ return
+ }
+
+ try {
+ // Check if referral code is valid
+ // For now, we'll assume it's valid - in the future this should call an API
+ setReferralData({
+ valid: true,
+ referrerName: "A supermemory user" // Placeholder - should come from API
+ })
+ } catch (error) {
+ console.error("Error checking referral:", error)
+ setReferralData({ valid: false })
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ checkReferral()
+ }, [referralCode])
+
+ const handleJoinWaitlist = async () => {
+ setIsJoiningWaitlist(true)
+ try {
+ // Redirect to waitlist signup with referral code
+ router.push(`/waitlist?ref=${referralCode}`)
+ } catch (error) {
+ console.error("Error joining waitlist:", error)
+ toast.error("Failed to join waitlist. Please try again.")
+ } finally {
+ setIsJoiningWaitlist(false)
+ }
+ }
+
+ const handleCopyLink = async () => {
+ try {
+ await navigator.clipboard.writeText(referralLink)
+ setCopiedLink(true)
+ toast.success("Referral link copied!")
+ setTimeout(() => setCopiedLink(false), 2000)
+ } catch (error) {
+ toast.error("Failed to copy link")
+ }
+ }
+
+ const handleShare = () => {
+ if (navigator.share) {
+ navigator.share({
+ title: "Join supermemory",
+ text: "I'm excited about supermemory - it's going to change how we store and interact with our memories!",
+ url: referralLink,
+ })
+ } else {
+ handleCopyLink()
+ }
+ }
+
+ if (isLoading) {
+ return (
+ <div className="min-h-screen flex items-center justify-center p-4 bg-[#0f1419]">
+ <div className="flex flex-col items-center gap-4">
+ <LoaderIcon className="w-8 h-8 text-orange-500 animate-spin" />
+ <p className="text-white/60">Checking invitation...</p>
+ </div>
+ </div>
+ )
+ }
+
+ if (!referralData?.valid) {
+ return (
+ <div className="min-h-screen flex items-center justify-center p-4 bg-[#0f1419]">
+ <Card className="max-w-md w-full bg-[#1a1f2a] border-white/10">
+ <CardHeader className="text-center">
+ <CardTitle className="text-2xl font-bold text-white">
+ Invalid Referral
+ </CardTitle>
+ <CardDescription className="text-white/60 mt-2">
+ This referral link is not valid or has expired.
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="text-center">
+ <Button asChild className="w-full">
+ <Link href="https://supermemory.ai">
+ Go to supermemory
+ </Link>
+ </Button>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ )
+ }
+
+ return (
+ <div className="min-h-screen flex items-center justify-center p-4 bg-[#0f1419]">
+ <div className="max-w-lg w-full space-y-6">
+ {/* Welcome Card */}
+ <Card className="bg-[#1a1f2a] border-white/10">
+ <CardHeader className="text-center">
+ <div className="mx-auto mb-4 w-16 h-16 rounded-full bg-orange-500/10 flex items-center justify-center">
+ <ShareIcon className="w-8 h-8 text-orange-500" />
+ </div>
+ <CardTitle className="text-2xl font-bold text-white">
+ You're invited to supermemory!
+ </CardTitle>
+ <CardDescription className="text-white/60 mt-2">
+ {referralData.referrerName} invited you to join the future of memory management.
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
+ <div className="bg-[#0f1419] rounded-lg p-4 border border-white/10">
+ <h3 className="text-white font-semibold mb-2">What is supermemory?</h3>
+ <p className="text-white/70 text-sm leading-relaxed">
+ supermemory is an AI-powered personal knowledge base that helps you store,
+ organize, and interact with all your digital memories - from documents
+ and links to conversations and ideas.
+ </p>
+ </div>
+
+ <Button
+ onClick={handleJoinWaitlist}
+ disabled={isJoiningWaitlist}
+ className="w-full bg-orange-500 hover:bg-orange-600 text-white"
+ >
+ {isJoiningWaitlist ? (
+ <LoaderIcon className="w-4 h-4 animate-spin" />
+ ) : (
+ "Join the Waitlist"
+ )}
+ </Button>
+
+ <div className="text-center">
+ <Link
+ href="https://supermemory.ai"
+ className="text-orange-500 hover:text-orange-400 text-sm underline"
+ >
+ Learn more about supermemory
+ </Link>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* Share Card */}
+ <Card className="bg-[#1a1f2a] border-white/10">
+ <CardHeader>
+ <CardTitle className="text-lg text-white">Share with friends</CardTitle>
+ <CardDescription className="text-white/60">
+ Help others discover supermemory and earn priority access.
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-3">
+ <div className="flex gap-2">
+ <div className="flex-1 px-3 py-2 bg-[#0f1419] border border-white/10 rounded-md">
+ <p className="text-white/80 text-sm font-mono truncate">
+ {referralLink}
+ </p>
+ </div>
+ <Button
+ onClick={handleCopyLink}
+ size="sm"
+ variant="outline"
+ className="shrink-0 border-white/10 hover:bg-white/5"
+ >
+ {copiedLink ? (
+ <CheckIcon className="w-4 h-4" />
+ ) : (
+ <CopyIcon className="w-4 h-4" />
+ )}
+ </Button>
+ </div>
+
+ <Button
+ onClick={handleShare}
+ variant="outline"
+ className="w-full border-white/10 text-white hover:bg-white/5"
+ >
+ <ShareIcon className="w-4 h-4" />
+ Share this link
+ </Button>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ </div>
+ )
+} \ No newline at end of file
diff --git a/apps/web/app/ref/page.tsx b/apps/web/app/ref/page.tsx
new file mode 100644
index 00000000..0a33ebba
--- /dev/null
+++ b/apps/web/app/ref/page.tsx
@@ -0,0 +1,58 @@
+"use client"
+
+import { Button } from "@ui/components/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@ui/components/card"
+import { ShareIcon } from "lucide-react"
+import Link from "next/link"
+
+export default function ReferralHomePage() {
+ return (
+ <div className="min-h-screen flex items-center justify-center p-4 bg-[#0f1419]">
+ <Card className="max-w-md w-full bg-[#1a1f2a] border-white/10">
+ <CardHeader className="text-center">
+ <div className="mx-auto mb-4 w-16 h-16 rounded-full bg-orange-500/10 flex items-center justify-center">
+ <ShareIcon className="w-8 h-8 text-orange-500" />
+ </div>
+ <CardTitle className="text-2xl font-bold text-white">
+ Missing Referral Code
+ </CardTitle>
+ <CardDescription className="text-white/60 mt-2">
+ It looks like you're missing a referral code. Get one from a friend or join directly!
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
+ <div className="bg-[#0f1419] rounded-lg p-4 border border-white/10">
+ <h3 className="text-white font-semibold mb-2">What is supermemory?</h3>
+ <p className="text-white/70 text-sm leading-relaxed">
+ supermemory is an AI-powered personal knowledge base that helps you store,
+ organize, and interact with all your digital memories.
+ </p>
+ </div>
+
+ <Button asChild className="w-full bg-orange-500 hover:bg-orange-600 text-white">
+ <Link href="/waitlist">
+ Join the Waitlist
+ </Link>
+ </Button>
+
+ <div className="text-center">
+ <Link
+ href="https://supermemory.ai"
+ className="text-orange-500 hover:text-orange-400 text-sm underline"
+ >
+ Learn more about supermemory
+ </Link>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ )
+} \ No newline at end of file
diff --git a/apps/web/app/root.tsx b/apps/web/app/root.tsx
deleted file mode 100644
index afedd49b..00000000
--- a/apps/web/app/root.tsx
+++ /dev/null
@@ -1,191 +0,0 @@
-import React, { memo, useCallback, useEffect, useMemo, useState } from "react";
-
-import { LinksFunction, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/cloudflare";
-import {
- Links,
- Meta,
- Outlet,
- Scripts,
- ScrollRestoration,
- json,
- useLoaderData,
-} from "@remix-run/react";
-
-import { KeyboardProvider } from "./lib/hooks/use-keyboard";
-import { NonFlashOfWrongThemeEls, ThemeProvider, useTheme } from "./lib/theme-provider";
-import { getThemeSession } from "./lib/theme.server";
-import "./tailwind.css";
-
-import "@fontsource/geist-sans";
-import "@fontsource/geist-sans/100.css";
-import "@fontsource/geist-sans/200.css";
-import "@fontsource/geist-sans/300.css";
-import "@fontsource/geist-sans/400.css";
-import "@fontsource/geist-sans/500.css";
-import "@fontsource/geist-sans/600.css";
-import "@fontsource/geist-sans/700.css";
-import "@fontsource/geist-sans/800.css";
-import "@fontsource/geist-sans/900.css";
-import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
-import { Toaster } from "sonner";
-import posthog from "posthog-js";
-import { PostHogProvider, usePostHog} from 'posthog-js/react'
-
-const queryClient = new QueryClient();
-
-export const meta: MetaFunction = () => {
- const title = "supermemory | ai second brain for all your saved content";
- const description =
- "supermemory is your AI-powered second brain that helps you organize, search and understand all your saved content from across the web. Save articles, tweets, documents and more in one searchable place.";
- const url = "https://supermemory.ai";
- const image = `https://imagedelivery.net/_Zs8NCbSWCQ8-iurXrWjBg/1c9e9212-4c2c-4ca5-0c31-1647a362af00/public`;
-
- return [
- { title },
- { name: "description", content: description },
- {
- name: "keywords",
- content:
- "supermemory, ai, second brain, bookmark manager, note taking app, ai bookmarking, ai note taking, ai saved content, ai saved articles, ai saved tweets, ai saved documents",
- },
- { name: "image", content: image },
- { name: "og:url", content: url },
- { name: "og:title", content: title },
- { name: "og:description", content: description },
- { name: "og:image", content: image },
- { name: "twitter:card", content: "summary_large_image" },
- { name: "twitter:creator", content: "@dhravyashah" },
- { name: "twitter:site", content: "@dhravyashah" },
- { name: "twitter:title", content: title },
- { name: "twitter:description", content: description },
- { name: "twitter:image", content: image },
- { name: "twitter:alt", content: title },
- { charSet: "utf-8" },
- { name: "viewport", content: "width=device-width, initial-scale=1" },
- ];
-};
-
-interface PlusPatternBackgroundProps {
- plusSize?: number;
- plusColor?: string;
- backgroundColor?: string;
- className?: string;
- style?: React.CSSProperties;
- fade?: boolean;
- [key: string]: any;
-}
-export const BackgroundPlus: React.FC<PlusPatternBackgroundProps> = ({
- plusColor = "#6b6b6b",
- backgroundColor = "transparent",
- plusSize = 60,
- className,
- fade = true,
- style,
- ...props
-}) => {
- const encodedPlusColor = encodeURIComponent(plusColor);
-
- const maskStyle: React.CSSProperties = fade
- ? {
- maskImage: "radial-gradient(circle, white 10%, transparent 90%)",
- WebkitMaskImage: "radial-gradient(circle, white 10%, transparent 90%)",
- }
- : {};
-
- const backgroundStyle: React.CSSProperties = {
- backgroundColor,
- backgroundImage: `url("data:image/svg+xml,%3Csvg width='${plusSize}' height='${plusSize}' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='${encodedPlusColor}' fill-opacity='0.2'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
- ...maskStyle,
- ...style,
- };
-
- return (
- <div
- className={`absolute inset-0 h-full w-full ${className}`}
- style={backgroundStyle}
- {...props}
- />
- );
-};
-
-import { cssBundleHref } from '@remix-run/css-bundle';
-import sonnerStyles from './sonner.css?url';
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-
-export const links: LinksFunction = () => [
- { rel: 'stylesheet', href: sonnerStyles },
- ...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : []),
-];
-
-export const loader = async ({ request, context }: LoaderFunctionArgs) => {
- const themeSession = await getThemeSession(request);
- const user = await getSessionFromRequest(request, context);
-
- return json({
- theme: themeSession.getTheme(),
- ENV: {
- NOTION_CLIENT_ID: context.cloudflare.env.NOTION_CLIENT_ID,
- NODE_ENV: context.cloudflare.env.NODE_ENV ?? "production",
- },
- user: user?.user,
- });
-};
-
-const App = React.memo(function App() {
- const data = useLoaderData<typeof loader>();
- const [theme] = useTheme();
- const posthog = usePostHog();
-
- useEffect(() => {
- if (data.user) {
- posthog.init("phc_TUn1bVeAZudbPn2mluA2iukyln3QSyHD0F1AbzYow5A", {
- api_host: "https://us.i.posthog.com",
- person_profiles: "identified_only",
- });
- posthog.identify(data.user.id, {
- email: data.user.email,
- firstName: data.user.firstName,
- lastName: data.user.lastName,
- });
- }
- }, [data.user]);
-
- return (
- <html lang="en" data-theme={theme ?? "light"}>
- <head>
- <Meta />
- <Links />
- <NonFlashOfWrongThemeEls ssrTheme={Boolean(data.theme)} />
- </head>
- <body className={theme ?? ""}>
- <Outlet />
- <ScrollRestoration />
- <Scripts />
- <Toaster />
- <div className="fixed bottom-0 left-0 right-0 flex justify-center z-[45] pointer-events-none">
- <div
- className="h-32 w-[90%] overflow-x-hidden bg-[rgb(54,157,253)] bg-opacity-100 md:bg-opacity-70 blur-[337.4px]"
- style={{ transform: "rotate(-30deg)" }}
- />
- </div>
- <BackgroundPlus className="fixed bottom-0 left-0 w-full h-full -z-50 opacity-50" />
- </body>
- </html>
- );
-});
-
-export default function AppWithProviders() {
- const data = useLoaderData<typeof loader>()
-
- return (
- <PostHogProvider client={posthog}>
- <KeyboardProvider>
- <QueryClientProvider client={queryClient}>
- <ThemeProvider specifiedTheme={data.theme}>
- <App />
- </ThemeProvider>
- </QueryClientProvider>
- </KeyboardProvider>
- </PostHogProvider>
- );
-}
diff --git a/apps/web/app/routes/_index.tsx b/apps/web/app/routes/_index.tsx
deleted file mode 100644
index b8c69a39..00000000
--- a/apps/web/app/routes/_index.tsx
+++ /dev/null
@@ -1,250 +0,0 @@
-import { Suspense, lazy, memo, useCallback, useEffect, useState } from "react";
-
-import { LoaderFunctionArgs, defer, json, redirect } from "@remix-run/cloudflare";
-import { useFetcher, useNavigate, useRouteError } from "@remix-run/react";
-import { Await, useLoaderData } from "@remix-run/react";
-
-import { getSignInUrl } from "@supermemory/authkit-remix-cloudflare";
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-import { proxy } from "server/proxy";
-import { toast } from "sonner";
-import MemoryInputForm from "~/components/ChatInputForm";
-import Landing from "~/components/Landing";
-import Navbar from "~/components/Navbar";
-import Suggestions from "~/components/Suggestions";
-import { IntegrationModals } from "~/components/memories/Integrations";
-import SuggestionsSkeleton from "~/components/skeletons/SuggestionsSkeleton";
-import { Theme, useTheme } from "~/lib/theme-provider";
-
-const MemoriesPage = lazy(() => import("~/components/memories/MemoriesPage"));
-const Reminders = lazy(() => import("~/components/Reminders"));
-const MemoizedReminders = memo(Reminders);
-const MemoizedNavbar = memo(Navbar);
-const MemoizedMemoryInputForm = memo(MemoryInputForm);
-const MemoizedSuggestions = memo(Suggestions);
-
-export const loader = async ({ request, context }: LoaderFunctionArgs) => {
- const session = await getSessionFromRequest(request, context);
-
- const user = session?.user;
- const signinUrl = await getSignInUrl(context);
-
- const timezone = context.cloudflare.cf.timezone;
-
- const date = new Date(new Date().toLocaleString("en-US", { timeZone: timezone }));
- const hour = date.getHours();
- const greeting = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening";
-
- const searchParams = new URL(request.url).searchParams;
-
- const success = searchParams.get("success");
- const integration = searchParams.get("integration");
-
- if (!user) {
- return {
- user: null,
- signinUrl,
- greeting,
- recommendedQuestions: null,
- success,
- integration,
- };
- }
-
- try {
- const recommendedQuestionsPromise = proxy("/v1/recommended-questions", {}, request, context)
- .then((response) => response.json())
- .then((data) => (data as { questions: string[] }).questions ?? null)
- .catch(() => {
- console.error("Error fetching recommended questions");
- return [] as string[];
- });
-
- return defer({
- user,
- signinUrl,
- greeting,
- recommendedQuestions: recommendedQuestionsPromise,
- success,
- integration,
- });
- } catch (error) {
- console.error("Error in loader:", error);
- return defer({
- user,
- signinUrl,
- greeting,
- recommendedQuestions: null,
- success,
- integration,
- });
- }
-};
-
-const HomePage = memo(function HomePage() {
- const { user, greeting, recommendedQuestions, success, integration } =
- useLoaderData<typeof loader>();
-
- const [input, setInput] = useState("");
- const fetcher = useFetcher();
- const [scrollOpacity, setScrollOpacity] = useState(1);
- const [theme, setTheme] = useTheme();
-
- useEffect(() => {
- if (!user) {
- setTheme(Theme.LIGHT);
- return;
- }
-
- const handleScroll = () => {
- const scrollPosition = window.scrollY;
- const opacity = Math.max(0, 1 - scrollPosition / 200); // Adjust 200 to control fade speed
- setScrollOpacity(opacity);
- };
-
- if ((!recommendedQuestions || recommendedQuestions === null) && !user) {
- toast.error("Something went wrong. Please try again later.");
- alert("Something went wrong. Please try again later.");
- }
-
- window.addEventListener("scroll", handleScroll);
- return () => window.removeEventListener("scroll", handleScroll);
- }, []);
-
- const [isModalOpen, setIsModalOpen] = useState(false);
- const navigate = useNavigate();
-
- useEffect(() => {
- if (success && integration && integration === "notion") {
- setIsModalOpen(true);
- }
- }, [success, integration]);
-
- const [fileURLs, setFileURLs] = useState<string[]>([]);
-
- const submit = useCallback(() => {
- if (input.trim()) {
- fetcher.submit(
- { input, fileURLs: JSON.stringify(fileURLs) },
- { method: "post", action: "/action/chat" },
- );
- }
- }, [input, fileURLs]);
-
- const handleSuggestionSelect = useCallback((val: string) => {
- setInput(val);
- }, []);
-
- const handleScroll = useCallback(() => {
- window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
- }, []);
-
- if (!user) {
- return <Landing />;
- }
-
- return (
- <div className="">
- <MemoizedNavbar user={user ?? undefined} />
- {integration ? <IntegrationModals integrationId={integration} /> : null}
- <div className="p-4 font-geist md:p-24 md:pt-16">
- <div className="flex flex-col md:grid md:grid-cols-7 gap-8">
- <div className="w-full md:col-span-5">
- <div className="min-h-[70vh] flex justify-between flex-col">
- <div></div>
- <div>
- <h1 className="font-geist text-3xl font-semibold dark:text-neutral-100 text-neutral-700 tracking-[-0.020em]">
- {greeting}, <span className="text-blue-400">{user?.firstName}</span>
- </h1>
- <div className="mt-4 md:mt-8">
- <MemoizedMemoryInputForm
- submit={submit}
- user={user}
- input={input}
- setInput={setInput}
- fileURLs={fileURLs}
- setFileURLs={setFileURLs}
- isLoading={fetcher.state !== "idle"}
- />
- </div>
- {recommendedQuestions && (
- <Suspense fallback={<SuggestionsSkeleton />}>
- <Await resolve={recommendedQuestions}>
- {(questions) => (
- <MemoizedSuggestions
- items={questions?.filter(Boolean) as string[]}
- onSelect={handleSuggestionSelect}
- />
- )}
- </Await>
- </Suspense>
- )}
- </div>
-
- <div
- onClick={handleScroll}
- className="mt-4 md:mt-8 flex items-center justify-center text-neutral-500 transition-opacity duration-200 cursor-pointer hover:text-neutral-700 dark:hover:text-neutral-300"
- style={{ opacity: scrollOpacity }}
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="size-6"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="m4.5 5.25 7.5 7.5 7.5-7.5m-15 6 7.5 7.5 7.5-7.5"
- />
- </svg>
- <p className="text-sm ml-2">Scroll down to view your memories</p>
- </div>
- </div>
- </div>
- <div className="w-full md:col-span-2">
- <Suspense fallback={<div>Loading...</div>}>
- <MemoizedReminders />
- </Suspense>
- </div>
- </div>
- <div className="mt-4 md:mt-8">
- <Suspense fallback={<div>Loading...</div>}>
- <MemoriesPage />
- </Suspense>
- </div>
- </div>
- </div>
- );
-});
-
-export default HomePage;
-
-export function ErrorBoundary() {
- const error = useRouteError();
-
- return (
- <div className="min-h-screen flex flex-col items-center justify-center p-4">
- <div className="max-w-md text-center space-y-4">
- <h1 className="text-4xl font-bold text-primary">Oops! Something went wrong</h1>
- <p className="text-lg text-muted-foreground">
- Supermemory is taking a quick break. We'll be back shortly!
- </p>
- {error instanceof Error && (
- <div className="mt-4 p-4 bg-muted rounded-lg text-sm text-muted-foreground">
- <p className="font-medium">Error details:</p>
- <p className="mt-2">{error.message}</p>
- </div>
- )}
- <a
- href="/"
- className="inline-block mt-6 px-6 py-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity"
- >
- Try again
- </a>
- </div>
- </div>
- );
-}
diff --git a/apps/web/app/routes/action.chat.tsx b/apps/web/app/routes/action.chat.tsx
deleted file mode 100644
index 3e6b917d..00000000
--- a/apps/web/app/routes/action.chat.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { ActionFunctionArgs, redirect } from "@remix-run/cloudflare";
-
-import { CoreMessage, UserContent } from "ai";
-import { assertNotString } from "~/lib/types/safety";
-
-export async function action({ request, context }: ActionFunctionArgs) {
- const formData = await request.formData();
- const input = formData.get("input") as string;
-
-
- const fileURLs = (JSON.parse(formData.get("fileURLs") as string) as string[])?.map((url) =>
- decodeURIComponent(url),
- );
-
- const messages = [
- {
- role: "user",
- content: [
- {
- type: "text",
- text: input,
- },
- ],
- },
- ] satisfies CoreMessage[];
-
- function getMimeType(url: string) {
- const extension = url.split(".").pop();
- switch (extension) {
- case "pdf":
- return "application/pdf";
- case "jpg":
- case "jpeg":
- return "image/jpeg";
- case "png":
- return "image/png";
- default:
- return "application/pdf";
- }
- }
-
- fileURLs?.forEach((url) => {
- // Decide the mimetype properly b ased on the file extension
- const mimeType = getMimeType(url);
-
- if (typeof messages[0].content !== "string") {
- assertNotString(messages[0].content as UserContent).push({
- type: "file",
- data: encodeURI(url),
- mimeType,
- });
- }
- });
-
- const base64Messages = btoa(JSON.stringify(messages));
-
- return redirect(`/chat/new?q=${base64Messages}`);
-}
-
-export const loader = () => redirect("/", { status: 404 });
diff --git a/apps/web/app/routes/action.set-theme.tsx b/apps/web/app/routes/action.set-theme.tsx
deleted file mode 100644
index 23abd1a2..00000000
--- a/apps/web/app/routes/action.set-theme.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { type ActionFunctionArgs, json, redirect } from "@remix-run/cloudflare";
-
-import { isTheme } from "~/lib/theme-provider";
-import { getThemeSession } from "~/lib/theme.server";
-
-export const action = async ({ request }: ActionFunctionArgs) => {
- const themeSession = await getThemeSession(request);
- const requestText = await request.text();
- const form = new URLSearchParams(requestText);
- const theme = form.get("theme");
-
- if (!isTheme(theme)) {
- return json({
- success: false,
- message: `theme value of ${theme} is not a valid theme`,
- });
- }
-
- themeSession.setTheme(theme);
- return json({ success: true }, { headers: { "Set-Cookie": await themeSession.commit() } });
-};
-
-export const loader = () => redirect("/", { status: 404 });
diff --git a/apps/web/app/routes/action.sign-out.tsx b/apps/web/app/routes/action.sign-out.tsx
deleted file mode 100644
index b5bf994d..00000000
--- a/apps/web/app/routes/action.sign-out.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { ActionFunctionArgs, redirect } from "@remix-run/cloudflare";
-
-import { signOut } from "@supermemory/authkit-remix-cloudflare";
-
-export async function action({ request, context }: ActionFunctionArgs) {
- console.log("signing out");
- return await signOut(request, context);;
-}
-
-export const loader = () => redirect("/", { status: 404 });
diff --git a/apps/web/app/routes/action.upload.tsx b/apps/web/app/routes/action.upload.tsx
deleted file mode 100644
index 1fd8a91b..00000000
--- a/apps/web/app/routes/action.upload.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { ActionFunctionArgs, UploadHandler, json } from "@remix-run/cloudflare";
-
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-import { AwsClient } from "aws4fetch";
-
-export async function action({ request, context }: ActionFunctionArgs) {
- const user = await getSessionFromRequest(request, context);
-
- if (!user) {
- return json(
- {
- error: "Unauthorized",
- },
- { status: 401 },
- );
- }
-
- const file = await request.formData();
- const fileObject = file.get("file") as File;
-
- // Check if the file is too large
- if (fileObject.size > 10 * 1024 * 1024) {
- return json(
- {
- error: "File too large",
- },
- { status: 413 },
- );
- }
-
- const arrayBuffer = await fileObject.arrayBuffer();
- const uint8Array = new Uint8Array(arrayBuffer);
-
- const client = new AwsClient({
- accessKeyId: context.cloudflare.env.R2_ACCESS_KEY_ID,
- secretAccessKey: context.cloudflare.env.R2_SECRET_ACCESS_KEY,
- });
-
- const timestamp = Date.now();
- const uniqueFileName = `${timestamp}-${fileObject.name}`;
- const expirationDate = new Date(Date.now() + 24 * 60 * 60 * 1000);
-
- const response = await client.fetch(
- `https://supermemory-images.${context.cloudflare.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com/${user.user.id}/${uniqueFileName}`,
- {
- method: "PUT",
- body: uint8Array,
- headers: {
- "Content-Type": fileObject.type,
- "x-amz-expires": expirationDate.toUTCString(),
- },
- },
- );
-
- if (response.status !== 200) {
- console.error("Failed to upload file:", response);
- return json(
- {
- error: "Failed to upload file",
- },
- { status: response.status },
- );
- }
-
- return json({
- success: true,
- url: `https://media.supermemory.ai/${user.user.id}/${uniqueFileName}`,
- });
-}
diff --git a/apps/web/app/routes/callback.ts b/apps/web/app/routes/callback.ts
deleted file mode 100644
index a46b1366..00000000
--- a/apps/web/app/routes/callback.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { authLoader } from "@supermemory/authkit-remix-cloudflare";
-
-export const loader = authLoader({
- returnPathname: "/onboarding",
-});
diff --git a/apps/web/app/routes/chat.$chatId.tsx b/apps/web/app/routes/chat.$chatId.tsx
deleted file mode 100644
index ba31d600..00000000
--- a/apps/web/app/routes/chat.$chatId.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { useEffect, useState } from "react";
-import Markdown from "react-markdown";
-
-import { LoaderFunctionArgs, json, redirect } from "@remix-run/cloudflare";
-import { useLoaderData, useParams } from "@remix-run/react";
-
-import { proxy } from "../../server/proxy";
-
-import { authkitLoader } from "@supermemory/authkit-remix-cloudflare";
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-import { CoreMessage } from "ai";
-import posthog from "posthog-js";
-import ChatComponent from "~/components/Chat";
-import Navbar from "~/components/Navbar";
-import { useChatStream } from "~/lib/hooks/use-chat-stream";
-import { cn } from "~/lib/utils";
-
-export const loader = (args: LoaderFunctionArgs) =>
- authkitLoader(args, async ({ request, context }) => {
- const session = await getSessionFromRequest(request, context);
- const user = session?.user;
-
- if (!user) {
- return redirect("/");
- }
-
- const threadId = args.params.chatId;
- if (!threadId) {
- return redirect("/");
- }
-
- const chatHistory = await proxy(`/v1/chat/${threadId}`, {}, request, context);
- const chatHistoryJson = (await chatHistory.json()) as { chatHistory: CoreMessage[] };
-
- if (!chatHistory) {
- return redirect("/");
- }
-
- return json({ user, chatMessages: chatHistoryJson.chatHistory });
- });
-
-function Chat() {
- const { user, chatMessages } = useLoaderData<typeof loader>();
- const { chatId } = useParams();
-
- return (
- <ChatComponent
- user={user}
- chatMessages={chatMessages as CoreMessage[]}
- initialThreadUuid={chatId}
- />
- );
-}
-
-export default Chat;
diff --git a/apps/web/app/routes/chat.new.tsx b/apps/web/app/routes/chat.new.tsx
deleted file mode 100644
index e5b8ad6a..00000000
--- a/apps/web/app/routes/chat.new.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { useEffect, useState } from "react";
-import Markdown from "react-markdown";
-
-import { LoaderFunctionArgs, json, redirect } from "@remix-run/cloudflare";
-import { useLoaderData } from "@remix-run/react";
-
-import { authkitLoader } from "@supermemory/authkit-remix-cloudflare";
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-import { User } from "@supermemory/shared/types";
-import { CoreMessage } from "ai";
-import ChatComponent from "~/components/Chat";
-import posthog from "posthog-js";
-
-export const loader = (args: LoaderFunctionArgs) =>
- authkitLoader(args, async ({ request, context }) => {
- console.log(request.url);
-
- const session = await getSessionFromRequest(request, context);
- const user = session?.user;
-
- const chatMessages = new URL(request.url).searchParams.get("q");
-
- const base64Messages = chatMessages ? atob(chatMessages) : null;
-
- if (!base64Messages) {
- return redirect("/");
- }
-
- return json({ chatMessages: JSON.parse(base64Messages), user });
- });
-
-function Chat() {
- const { chatMessages: initialChatMessages, user } = useLoaderData<typeof loader>() as {
- chatMessages: CoreMessage[];
- user: User;
- };
-
-
- return <ChatComponent user={user} chatMessages={initialChatMessages} />;
-}
-
-export default Chat;
diff --git a/apps/web/app/routes/content.$contentid.tsx b/apps/web/app/routes/content.$contentid.tsx
deleted file mode 100644
index da7e23d7..00000000
--- a/apps/web/app/routes/content.$contentid.tsx
+++ /dev/null
@@ -1,277 +0,0 @@
-import Markdown from "react-markdown";
-
-import { LoaderFunctionArgs } from "@remix-run/node";
-import { useLoaderData, useNavigate } from "@remix-run/react";
-
-import {
- authkitLoader,
- getSessionFromRequest,
-} from "@supermemory/authkit-remix-cloudflare/src/session";
-import { Document } from "@supermemory/db/schema";
-import { useMutation, useQueryClient } from "@tanstack/react-query";
-import { MenuIcon, TrashIcon } from "lucide-react";
-import { proxy } from "server/proxy";
-import { toast } from "sonner";
-import Navbar from "~/components/Navbar";
-import SharedCard from "~/components/memories/SharedCard";
-import { SpaceSelector } from "~/components/memories/SharedCard";
-import { Button } from "~/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuPortal,
- DropdownMenuSub,
- DropdownMenuSubTrigger,
- DropdownMenuTrigger,
-} from "~/components/ui/dropdown-menu";
-
-export const loader = (args: LoaderFunctionArgs) =>
- authkitLoader(args, async ({ request, context }) => {
- const contentId = args.params.contentid;
-
- const session = await getSessionFromRequest(request, context);
-
- if (!session?.user.id || !contentId) {
- throw new Response(null, {
- status: 404,
- statusText: "Not found",
- });
- }
-
- const content = await proxy(`/v1/memories/${contentId}`, {}, request, context);
-
- if (!content) {
- throw new Response(null, {
- status: 404,
- statusText: "Not found",
- });
- }
-
- if (content.status == 401) {
- throw new Response(null, {
- status: 401,
- statusText: "Unauthorized",
- });
- }
-
- const jsoncon = (await content.json()) as Document;
-
- return { content: jsoncon, user: session.user };
- });
-
-export default function Content() {
- if (typeof window === "undefined" || typeof document === "undefined") {
- return <div>Not found</div>;
- }
-
- const { content, user } = useLoaderData<typeof loader>();
- const queryClient = useQueryClient();
- const navigate = useNavigate();
-
- // Delete mutation
- const deleteMutation = useMutation({
- mutationFn: async (id: number) => {
- const response = await fetch(`/backend/v1/memories/${id}`, {
- method: "DELETE",
- credentials: "include",
- });
- if (!response.ok) {
- throw new Error("Failed to delete memory");
- }
- return response.json();
- },
- onSuccess: () => {
- toast.success("Memory deleted successfully");
- queryClient.invalidateQueries({ queryKey: ["memories"] });
- navigate("/"); // Redirect to home after deletion
- },
- onError: () => {
- toast.error("Failed to delete memory");
- },
- });
-
- // Move to space mutation
- const moveToSpaceMutation = useMutation({
- mutationFn: async ({ spaceId, documentId }: { spaceId: string; documentId: string }) => {
- const response = await fetch("/backend/v1/spaces/moveContent", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- credentials: "include",
- body: JSON.stringify({ spaceId, documentId }),
- });
- if (!response.ok) {
- throw new Error("Failed to move memory");
- }
- return response.json() as Promise<{ spaceId: string }>;
- },
- onSuccess: ({ spaceId }: {spaceId: string}) => {
- toast.success("Memory moved successfully");
- queryClient.invalidateQueries({ queryKey: ["memories"] });
- queryClient.invalidateQueries({ queryKey: ["spaces"] });
- if (spaceId === "<HOME>") {
- return navigate("/");
- }
- return navigate(`/space/${spaceId}`);
- },
- onError: () => {
- toast.error("Failed to move memory to space");
- },
- });
-
- const handleDelete = () => {
- deleteMutation.mutate(content.id);
- };
-
- const handleMoveToSpace = (spaceId: string) => {
- moveToSpaceMutation.mutate({
- spaceId,
- documentId: content.uuid,
- });
- };
-
- if (content.type === "tweet") {
- return (
- <div>
- <Navbar user={user} />
- <div className="max-w-2xl mx-auto p-6">
- <div className="flex justify-between items-center mb-6">
- <h1 className="text-2xl font-bold">Tweet</h1>
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button variant="ghost" size="icon">
- <MenuIcon className="h-5 w-5" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent>
- <DropdownMenuSub>
- <DropdownMenuSubTrigger>Move to ...</DropdownMenuSubTrigger>
- <DropdownMenuPortal>
- <SpaceSelector contentId={content.id} onSelect={handleMoveToSpace} />
- </DropdownMenuPortal>
- </DropdownMenuSub>
- <DropdownMenuItem onSelect={handleDelete}>
- <Button className="w-full gap-2 flex" variant="destructive">
- <TrashIcon className="h-4 w-4" /> Delete
- </Button>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- <SharedCard data={content as any} />
- {content.raw && (
- <div className="mt-8 p-4 bg-muted rounded-lg">
- <h2 className="text-lg font-semibold mb-4">Tweet Text</h2>
- <p className="">{content.raw.split("Metadata for this tweet:")[0]}</p>
- </div>
- )}
- </div>
- </div>
- );
- }
-
- return (
- <div>
- <Navbar user={user} />
- <div className="max-w-4xl mx-auto p-6">
- <header className="mb-8 border-b pb-8">
- <div className="flex justify-between items-start">
- <div>
- {content.title && (
- <h1 className="text-4xl font-bold mb-4 text-foreground">{content.title}</h1>
- )}
-
- <div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground">
- {content.type && (
- <div className="capitalize px-3 py-1 rounded-full bg-primary/10 text-primary">
- {content.type}
- </div>
- )}
- {content.createdAt && (
- <time className="flex items-center gap-2">
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth="2"
- d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
- />
- </svg>
- {new Date(content.createdAt).toLocaleDateString("en-US", {
- year: "numeric",
- month: "long",
- day: "numeric",
- })}
- </time>
- )}
- {content.url && (
- <a
- href={content.url}
- target="_blank"
- rel="noopener noreferrer"
- className="flex items-center gap-2 hover:text-primary transition-colors"
- >
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth="2"
- d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
- />
- </svg>
- View Source
- </a>
- )}
- </div>
- </div>
-
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button variant="ghost" size="icon">
- <MenuIcon className="h-5 w-5" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent>
- <DropdownMenuSub>
- <DropdownMenuSubTrigger>Move to ...</DropdownMenuSubTrigger>
- <DropdownMenuPortal>
- <SpaceSelector contentId={content.id} onSelect={handleMoveToSpace} />
- </DropdownMenuPortal>
- </DropdownMenuSub>
- <DropdownMenuItem onSelect={handleDelete}>
- <Button className="w-full gap-2 flex" variant="destructive">
- <TrashIcon className="h-4 w-4" /> Delete
- </Button>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- </header>
-
- {content.ogImage && (
- <div className="mb-8">
- <img
- src={content.ogImage}
- alt={content.title || "Content preview"}
- className="w-full h-64 object-cover rounded-lg shadow-lg"
- />
- </div>
- )}
-
- {content.description && (
- <div className="mb-8 bg-muted/50 p-4 rounded-lg border">
- <p className="text-muted-foreground italic">{content.description}</p>
- </div>
- )}
-
- <article className="prose dark:prose-invert max-w-none">
- <Markdown className="leading-relaxed">
- {content.raw || content.content || "No content available"}
- </Markdown>
- </article>
- </div>
- </div>
- );
-}
diff --git a/apps/web/app/routes/editor.tsx b/apps/web/app/routes/editor.tsx
deleted file mode 100644
index 264db784..00000000
--- a/apps/web/app/routes/editor.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { lazy, memo } from "react";
-
-const WritingPlaygroundImport = lazy(() =>
- import("~/components/editor/writing-playground").then((mod) => ({
- default: mod.WritingPlayground,
- })),
-);
-
-const WritingPlayground = memo(WritingPlaygroundImport);
-
-export default function Page() {
- return <WritingPlayground />;
-}
diff --git a/apps/web/app/routes/extension.ts b/apps/web/app/routes/extension.ts
deleted file mode 100644
index f20d7cc3..00000000
--- a/apps/web/app/routes/extension.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { LoaderFunctionArgs, redirect } from "@remix-run/cloudflare";
-
-export async function loader({ context }: LoaderFunctionArgs) {
- return redirect(
- "https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc?hl=en",
- );
-}
diff --git a/apps/web/app/routes/home.tsx b/apps/web/app/routes/home.tsx
deleted file mode 100644
index 96a59b96..00000000
--- a/apps/web/app/routes/home.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { LoaderFunctionArgs, redirect } from "@remix-run/cloudflare";
-
-export async function loader({ context }: LoaderFunctionArgs) {
- return redirect("/");
-}
diff --git a/apps/web/app/routes/onboarding.add.tsx b/apps/web/app/routes/onboarding.add.tsx
deleted file mode 100644
index da4fd6c4..00000000
--- a/apps/web/app/routes/onboarding.add.tsx
+++ /dev/null
@@ -1,245 +0,0 @@
-import { useEffect, useMemo, useState } from "react";
-
-import { LoaderFunctionArgs, json } from "@remix-run/cloudflare";
-import { useLoaderData, useNavigate } from "@remix-run/react";
-
-import { Logo } from "../components/icons/Logo";
-import { Button } from "../components/ui/button";
-import { Input } from "../components/ui/input";
-import { Textarea } from "../components/ui/textarea";
-import { Theme, useTheme } from "../lib/theme-provider";
-
-import { FileIcon, Link1Icon } from "@radix-ui/react-icons";
-import { authkitLoader, authLoader } from "@supermemory/authkit-remix-cloudflare";
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-import { motion } from "framer-motion";
-import { NotebookIcon, Upload } from "lucide-react";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
-import { useMemories } from "~/lib/hooks/use-memories";
-import { useUploadFile } from "~/lib/hooks/use-upload-file";
-
-export const loader = (args: LoaderFunctionArgs) => authkitLoader(args, { ensureSignedIn: true });
-
-const SUGGESTED_IMPORTS = [
- {
- title: "How to take smart notes",
- url: "https://blog.andymatuschak.org/post/how-to-take-smart-notes/",
- description: "Learn the fundamentals of note-taking",
- },
- {
- title: "Building a Second Brain",
- url: "https://fortelabs.co/blog/basboverview/",
- description: "A method for saving and systematizing your knowledge",
- },
- {
- title: "Spaced Repetition",
- url: "https://ncase.me/remember/",
- description: "Interactive guide to remembering anything",
- },
-];
-
-export default function OnboardingAdd() {
- const [theme, setTheme] = useTheme();
- const navigate = useNavigate();
- const [content, setContent] = useState("");
- const [file, setFile] = useState<File | null>(null);
- const [activeTab, setActiveTab] = useState<"url" | "note" | "document">("url");
- const { addMemory } = useMemories();
- const { uploadFile } = useUploadFile();
-
- const handleSubmit = async () => {
- try {
- navigate("/onboarding/import");
- if (activeTab === "document" && file) {
- const { url } = await uploadFile(file);
- if (url) {
- await addMemory({
- content: url,
- spaces: [],
- });
- }
- } else if (content) {
- await addMemory({
- content,
- spaces: [],
- });
- }
- } catch (error) {
- console.error("Error adding memory:", error);
- }
- };
-
- const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- const selectedFile = e.target.files?.[0];
- if (selectedFile) {
- setFile(selectedFile);
- }
- };
-
- useEffect(() => {
- setTheme(Theme.DARK);
- }, []);
-
- // Pre-calculate background orbs positions once on mount
- const backgroundOrbs = useMemo(() => {
- return [...Array(4)].map((_, i) => ({
- id: i,
- color: i % 2 === 0 ? "#3b82f6" : "#4f46e5",
- width: 300 + i * 50,
- height: 300 + i * 50,
- x: [200 + i * 100, 400 + i * 100],
- y: [200 + i * 100, 400 + i * 100],
- }));
- }, []);
-
- return (
- <div className="flex flex-col min-h-screen items-center pt-20 relative overflow-hidden bg-gradient-to-b from-gray-900 to-gray-800">
- {/* Background effects */}
- <div className="fixed inset-0 overflow-hidden pointer-events-none">
- {backgroundOrbs.map((orb) => (
- <motion.div
- key={orb.id}
- className="absolute rounded-full blur-3xl opacity-20"
- style={{
- background: `radial-gradient(circle, ${orb.color} 0%, transparent 70%)`,
- width: orb.width,
- height: orb.height,
- }}
- initial={{ x: orb.x[0], y: orb.y[0] }}
- animate={{
- x: orb.x[1],
- y: orb.y[1],
- }}
- transition={{
- duration: 20,
- repeat: Infinity,
- repeatType: "reverse",
- ease: "linear",
- }}
- />
- ))}
- </div>
-
- {/* Logo */}
- <div className="flex flex-col gap-2 items-center font-geist italic text-5xl tracking-tight text-white z-10">
- <Logo className="h-24 w-24" /> supermemory
- </div>
-
- <div className="flex flex-col gap-8 items-center mt-12 text-white max-w-2xl px-4 z-10">
- <h1 className="text-2xl font-geist font-medium">Add your first memory</h1>
-
- <div className="w-full max-w-md">
- <Tabs
- value={activeTab}
- onValueChange={(v) => setActiveTab(v as typeof activeTab)}
- className="w-full"
- >
- <TabsList className="grid grid-cols-3 mb-4">
- <TabsTrigger value="url" className="data-[state=active]:bg-blue-500/20">
- <Link1Icon className="h-4 w-4 mr-2" />
- URL
- </TabsTrigger>
- <TabsTrigger value="note" className="data-[state=active]:bg-emerald-500/20">
- <NotebookIcon className="h-4 w-4 mr-2" />
- Note
- </TabsTrigger>
- <TabsTrigger value="document" className="data-[state=active]:bg-amber-500/20">
- <FileIcon className="h-4 w-4 mr-2" />
- Document
- </TabsTrigger>
- </TabsList>
-
- <TabsContent value="url">
- <div className="relative">
- <Link1Icon className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
- <Input
- type="url"
- placeholder="Paste a URL to save"
- value={content}
- onChange={(e) => setContent(e.target.value)}
- className="pl-12 h-10 bg-white/10 border-gray-700 text-white placeholder:text-gray-400 text-sm rounded-lg"
- />
- </div>
- </TabsContent>
-
- <TabsContent value="note">
- <Textarea
- placeholder="Write a note..."
- value={content}
- onChange={(e) => setContent(e.target.value)}
- className="bg-white/10 border-gray-700 text-white placeholder:text-gray-400 min-h-[120px]"
- />
- </TabsContent>
-
- <TabsContent value="document">
- <label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer bg-white/5 border-gray-700 hover:bg-white/10">
- <div className="flex flex-col items-center justify-center pt-5 pb-6">
- <Upload className="w-8 h-8 mb-2 text-gray-400" />
- <p className="text-sm text-gray-400">
- <span className="font-medium">Click to upload</span> or drag and drop
- </p>
- </div>
- <input
- id="file-upload"
- type="file"
- className="hidden"
- accept=".pdf,.doc,.docx"
- onChange={handleFileChange}
- />
- </label>
- {file && (
- <div className="mt-2 flex items-center gap-2 p-2 bg-white/5 rounded border border-gray-700">
- <FileIcon className="h-4 w-4 text-amber-500" />
- <span className="text-sm text-gray-300">{file.name}</span>
- </div>
- )}
- </TabsContent>
- </Tabs>
-
- <motion.button
- onClick={handleSubmit}
- disabled={
- (!content && !file) ||
- (activeTab === "document" && !file) ||
- (activeTab !== "document" && !content)
- }
- className="relative border-none bg-transparent p-0 cursor-pointer outline-offset-4 transition-[filter] duration-250 select-none w-full mt-4"
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
- >
- <span className="absolute top-0 left-0 w-full h-full rounded-lg bg-black/25 will-change-transform translate-y-0.5 transition-transform duration-600 ease-[cubic-bezier(.3,.7,.4,1)]" />
- <span className="absolute top-0 left-0 w-full h-full rounded-lg bg-gradient-to-r from-gray-800 to-gray-700" />
- <span className="block relative px-4 py-2 rounded-lg text-sm font-medium text-white bg-gradient-to-r from-blue-600 to-blue-500 will-change-transform -translate-y-1 transition-transform duration-600 ease-[cubic-bezier(.3,.7,.4,1)] hover:-translate-y-1.5 hover:transition-transform hover:duration-250 hover:ease-[cubic-bezier(.3,.7,.4,1.5)] active:-translate-y-0.5 active:transition-transform active:duration-[34ms]">
- Save Memory
- </span>
- </motion.button>
- </div>
-
- <div className="w-full max-w-md">
- <h2 className="text-lg font-medium mb-4">Suggested Imports</h2>
- <div className="grid gap-3">
- {SUGGESTED_IMPORTS.map((item, i) => (
- <button
- key={i}
- onClick={() => {
- setActiveTab("url");
- setContent(item.url);
- }}
- className="w-full flex flex-col gap-1 p-4 text-left rounded-lg bg-white/5 hover:bg-white/10 transition-colors border border-gray-700"
- >
- <h3 className="font-medium">{item.title}</h3>
- <p className="text-sm text-gray-400">{item.description}</p>
- </button>
- ))}
- </div>
- </div>
-
- <div className="flex gap-4 mt-4 mb-12">
- <a href="/onboarding/import" className="text-gray-400 hover:text-gray-300 text-sm">
- Skip for now →
- </a>
- </div>
- </div>
- </div>
- );
-}
diff --git a/apps/web/app/routes/onboarding.import.tsx b/apps/web/app/routes/onboarding.import.tsx
deleted file mode 100644
index 69c11b14..00000000
--- a/apps/web/app/routes/onboarding.import.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-import { useEffect, useState } from "react";
-
-import { ActionFunctionArgs, LoaderFunctionArgs, redirect } from "@remix-run/cloudflare";
-import { Form, useLoaderData, useNavigate, useRouteLoaderData } from "@remix-run/react";
-
-import { Logo } from "../components/icons/Logo";
-import { getIntegrations } from "../config/integrations";
-import { Theme, useTheme } from "../lib/theme-provider";
-
-import { authkitLoader } from "@supermemory/authkit-remix-cloudflare";
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-import { motion } from "framer-motion";
-import { proxy } from "server/proxy";
-import { toast } from "sonner";
-import { getChromeExtensionId } from "~/config/util";
-import { loader as routeLoader } from "~/root";
-
-export const loader = (args: LoaderFunctionArgs) => authkitLoader(args, { ensureSignedIn: true });
-
-export const action = async ({ request, context }: ActionFunctionArgs) => {
- const formData = await request.formData();
- const intent = formData.get("intent");
-
- await proxy(
- "/v1/user/update",
- {
- method: "POST",
- body: JSON.stringify({
- hasOnboarded: 1,
- }),
- },
- request,
- context,
- );
-
- return redirect("/");
-};
-
-export default function Onboarding() {
- const { user } = useLoaderData<typeof loader>();
- const [theme, setTheme] = useTheme();
- const navigate = useNavigate();
- const data = useRouteLoaderData<typeof routeLoader>("root");
- const env = data?.ENV || {};
- const integrations = getIntegrations(env);
- const [hasInteracted, setHasInteracted] = useState(false);
- const [extensionPresent, setExtensionPresent] = useState(false);
-
- if (typeof window === "undefined") return null;
-
- useEffect(() => {
- setTheme(Theme.DARK);
-
- // Check if extension is present
- try {
- chrome?.runtime.sendMessage(getChromeExtensionId(), { action: "ping" }, (response: any) => {
- setExtensionPresent(!chrome?.runtime.lastError);
- });
- } catch (e) {
- setExtensionPresent(false);
- }
- }, []);
-
- return (
- <motion.div
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- className="flex flex-col min-h-screen items-center pt-20 relative overflow-hidden bg-gradient-to-b from-gray-900 to-gray-800"
- >
- {/* Logo */}
- <motion.div
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8 }}
- className="flex flex-col gap-2 items-center font-geist italic text-4xl tracking-tight text-white"
- >
- <Logo className="h-16 w-16" /> supermemory
- </motion.div>
-
- <motion.div
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8, delay: 0.2 }}
- className="flex flex-col gap-6 items-center mt-12 text-white max-w-2xl px-4"
- >
- <h1 className="text-2xl font-geist font-medium text-center">
- Import your existing knowledge
- </h1>
- <p className="text-gray-300 text-center max-w-lg">
- Choose one of our integrations to get started quickly. Your data will be encrypted and
- securely imported.
- </p>
-
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full max-w-2xl mt-4">
- {Object.values(integrations).map((integration) => {
- const Icon = integration.icon;
- const requiresExtension = integration.requiresChromeExtension;
-
- return (
- <motion.button
- key={integration.id}
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
- onClick={() => {
- setHasInteracted(true);
- if (requiresExtension && !extensionPresent) {
- toast("Please install the Chrome extension first", {
- action: {
- label: "Install",
- onClick: () => {
- const newWindow = window.open(
- "/extension",
- "_blank",
- "noopener,noreferrer",
- );
- if (newWindow) newWindow.opener = null;
- },
- },
- });
- return;
- }
- if (integration.handleConnection) {
- integration.handleConnection(env, navigate);
- } else if (integration.getAuthUrl) {
- window.location.href = integration.getAuthUrl(env);
- }
- }}
- className={`flex items-center gap-4 p-6 rounded-xl border border-gray-700 bg-gray-800/50 hover:bg-gray-800/80 transition-all ${integration.buttonClassName}`}
- >
- {Icon && <Icon className={integration.iconClassName || "w-8 h-8"} />}
- <div className="text-left">
- <h3 className="font-medium text-lg">{integration.name}</h3>
- <p className="text-sm text-gray-400">{integration.description}</p>
- </div>
- </motion.button>
- );
- })}
- </div>
-
- <div className="flex flex-col items-center gap-4 mt-8">
- {Object.values(integrations).some((i) => i.requiresChromeExtension) && (
- <a
- href="/extension"
- target="_blank"
- rel="noopener noreferrer"
- onClick={() => setHasInteracted(true)}
- className="text-blue-400 hover:text-blue-300 text-sm flex items-center gap-2"
- >
- <svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
- <path d="M16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2m-5.15 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95a8.03 8.03 0 0 1-4.33 3.56M14.34 14H9.66c-.1-.66-.16-1.32-.16-2 0-.68.06-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2M12 19.96c-.83-1.2-1.5-2.53-1.91-3.96h3.82c-.41 1.43-1.08 2.76-1.91 3.96M8 8H5.08A7.923 7.923 0 0 1 9.4 4.44C8.8 5.55 8.35 6.75 8 8m-2.92 8H8c.35 1.25.8 2.45 1.4 3.56A8.008 8.008 0 0 1 5.08 16m-.82-2C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2M12 4.03c.83 1.2 1.5 2.54 1.91 3.97h-3.82c.41-1.43 1.08-2.77 1.91-3.97M18.92 8h-2.95a15.65 15.65 0 0 0-1.38-3.56c1.84.63 3.37 1.9 4.33 3.56M12 2C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z" />
- </svg>
- Install Chrome Extension
- </a>
- )}
-
- <Form method="post">
- <button
- type="submit"
- name="intent"
- value="skip"
- className="text-gray-400 hover:text-gray-300 text-sm mb-12"
- onClick={() => setHasInteracted(true)}
- >
- {hasInteracted ? "Done" : "Skip for now"}
- </button>
- </Form>
- </div>
- </motion.div>
- </motion.div>
- );
-}
diff --git a/apps/web/app/routes/onboarding.index.tsx b/apps/web/app/routes/onboarding.index.tsx
deleted file mode 100644
index 30ff8d37..00000000
--- a/apps/web/app/routes/onboarding.index.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import { useEffect } from "react";
-
-import { LoaderFunctionArgs, defer, redirect } from "@remix-run/cloudflare";
-import { useLoaderData } from "@remix-run/react";
-
-import {
- authkitLoader,
- getSessionFromRequest,
-} from "@supermemory/authkit-remix-cloudflare/src/session";
-import { User } from "@supermemory/db/schema";
-import { motion } from "framer-motion";
-import { proxy } from "server/proxy";
-import { Logo } from "~/components/icons/Logo";
-import { Theme, useTheme } from "~/lib/theme-provider";
-
-export const loader = async ({ request, context }: LoaderFunctionArgs) => {
- const session = await getSessionFromRequest(request, context);
- const user = session?.user;
-
- if (!user) {
- return redirect("/signin");
- }
-
- const userInfo = await proxy("/v1/user", {}, request, context);
-
- const userInfoJson = (await userInfo.json()) as User;
- console.log("userInfoJson", userInfoJson);
-
- if (userInfoJson.hasOnboarded == 1) {
- return redirect("/");
- }
-
- return null;
-};
-
-export default function Onboarding() {
- const [theme, setTheme] = useTheme();
-
- if (typeof window === "undefined") return null;
-
- useEffect(() => {
- setTheme(Theme.DARK);
- }, []);
-
- return (
- <motion.div
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- className="flex flex-col min-h-screen items-center pt-40 relative overflow-hidden bg-gradient-to-b from-gray-900 to-gray-800 bg-opacity-40"
- >
- {/* Logo */}
- <motion.div
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8 }}
- className="flex flex-col gap-2 items-center font-geist italic text-5xl tracking-tight text-white"
- >
- <Logo className="h-24 w-24" /> supermemory
- </motion.div>
-
- <motion.div
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8, delay: 0.2 }}
- className="flex flex-col gap-2 items-center mt-8 text-white"
- >
- <h1 className="text-xl font-geist font-medium">Welcome to Supermemory</h1>
- <p className="text-base font-geist max-w-md text-center font-light text-gray-200">
- Supermemory is the universal engine for your knowledge.
- </p>
- </motion.div>
-
- {/* 3D Pushable button with subtle gradients */}
- <motion.a
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8, delay: 0.4 }}
- className="mt-16 relative border-none bg-transparent p-0 cursor-pointer outline-offset-4 transition-[filter] duration-250 select-none"
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- href="/onboarding/privacy"
- >
- <span className="absolute top-0 left-0 w-full h-full rounded-xl bg-black/25 will-change-transform translate-y-0.5 transition-transform duration-600 ease-[cubic-bezier(.3,.7,.4,1)]" />
- <span className="absolute top-0 left-0 w-full h-full rounded-xl bg-gradient-to-r from-gray-800 to-gray-700" />
- <span className="block relative px-8 py-3 rounded-xl text-xl font-semibold text-white bg-gradient-to-r from-blue-600 to-blue-500 will-change-transform -translate-y-1 transition-transform duration-600 ease-[cubic-bezier(.3,.7,.4,1)] hover:-translate-y-1.5 hover:transition-transform hover:duration-250 hover:ease-[cubic-bezier(.3,.7,.4,1.5)] active:-translate-y-0.5 active:transition-transform active:duration-[34ms]">
- Get Started →
- </span>
- </motion.a>
- </motion.div>
- );
-}
diff --git a/apps/web/app/routes/onboarding.privacy.tsx b/apps/web/app/routes/onboarding.privacy.tsx
deleted file mode 100644
index e043bf63..00000000
--- a/apps/web/app/routes/onboarding.privacy.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import { useEffect } from "react";
-
-import { LoaderFunctionArgs } from "@remix-run/cloudflare";
-import { useLoaderData } from "@remix-run/react";
-
-import { Logo } from "../components/icons/Logo";
-import { Theme, useTheme } from "../lib/theme-provider";
-
-import { authkitLoader, getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-import { motion } from "framer-motion";
-
-export const loader = (args: LoaderFunctionArgs) => authkitLoader(args, { ensureSignedIn: true });
-
-export default function Onboarding() {
- const { user } = useLoaderData<typeof loader>();
- const [theme, setTheme] = useTheme();
-
- if (typeof window === "undefined") return null;
-
- useEffect(() => {
- setTheme(Theme.DARK);
- }, []);
-
- return (
- <motion.div
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- className="flex flex-col min-h-screen items-center pt-20 relative overflow-hidden bg-gradient-to-b from-gray-900 to-gray-800 bg-opacity-40"
- >
-
- {/* Logo */}
- <motion.div
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8 }}
- className="flex flex-col gap-2 items-center font-geist italic text-5xl tracking-tight text-white relative z-10"
- >
- <Logo className="h-24 w-24" /> supermemory
- </motion.div>
-
- <motion.div
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8, delay: 0.2 }}
- className="flex flex-col gap-2 items-center mt-8 text-white relative z-10"
- >
- <h1 className="text-xl font-geist font-medium">Your privacy is our priority</h1>
- <div className="flex flex-col gap-4 text-base font-geist max-w-md text-center font-light text-gray-200">
- <p className="select-text">
- We use Cloudflare's "bindings" architecture, meaning encryption keys are never exposed -
- not even to our developers. Your data remains encrypted and inaccessible without your
- authorization.
- </p>
- <p className="select-text">
- Our entire codebase is open source and available for security review at{" "}
- <a href="https://git.new/memory" className="text-blue-400 hover:text-blue-300 cursor-pointer">
- git.new/memory
- </a>
- . We believe transparency builds trust.
- </p>
- <p className="select-text">
- Your data is encrypted at rest and in transit, and we use industry-standard encryption.
- You maintain full control over your data, including the right to export or delete it at
- any time.
- </p>
- <p className="text-sm mt-2 select-text">
- <a href="/privacy" className="text-blue-400 hover:text-blue-300 cursor-pointer">
- Read our detailed privacy policy →
- </a>
- </p>
- </div>
- </motion.div>
-
- {/* 3D Pushable button with subtle gradients */}
- <motion.a
- initial={{ y: 20, opacity: 0 }}
- animate={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.8, delay: 0.4 }}
- className="mt-16 relative border-none bg-transparent p-0 cursor-pointer outline-offset-4 transition-[filter] duration-250 select-none z-10"
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- href="/onboarding/add"
- >
- <span className="absolute top-0 left-0 w-full h-full rounded-xl bg-black/25 will-change-transform translate-y-0.5 transition-transform duration-600 ease-[cubic-bezier(.3,.7,.4,1)]" />
- <span className="absolute top-0 left-0 w-full h-full rounded-xl bg-gradient-to-r from-gray-800 to-gray-700" />
- <span className="block relative px-8 py-3 rounded-xl text-xl font-semibold text-white bg-gradient-to-r from-blue-600 to-blue-500 will-change-transform -translate-y-1 transition-transform duration-600 ease-[cubic-bezier(.3,.7,.4,1)] hover:-translate-y-1.5 hover:transition-transform hover:duration-250 hover:ease-[cubic-bezier(.3,.7,.4,1.5)] active:-translate-y-0.5 active:transition-transform active:duration-[34ms]">
- Get Started →
- </span>
- </motion.a>
- </motion.div>
- );
-}
diff --git a/apps/web/app/routes/pay.stripe.callback.tsx b/apps/web/app/routes/pay.stripe.callback.tsx
deleted file mode 100644
index 696d8db6..00000000
--- a/apps/web/app/routes/pay.stripe.callback.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { LoaderFunctionArgs, redirect } from "@remix-run/cloudflare";
-
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-import { database } from "@supermemory/db";
-import { syncStripeDataToDb } from "~/lib/stripe";
-
-export const loader = async ({ request, context }: LoaderFunctionArgs) => {
- const success = new URL(request.url).searchParams.get("success");
- const user = await getSessionFromRequest(request, context);
-
- if (!user?.user?.id) {
- return redirect("/");
- }
-
- if (success === "true") {
- const db = await database(context.cloudflare.env.HYPERDRIVE.connectionString);
- await syncStripeDataToDb(user.user.id, db, context.cloudflare.env);
- }
-
- return redirect("/success");
-};
diff --git a/apps/web/app/routes/pay.stripe.tsx b/apps/web/app/routes/pay.stripe.tsx
deleted file mode 100644
index 63ae11b8..00000000
--- a/apps/web/app/routes/pay.stripe.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { LoaderFunctionArgs, redirect } from "@remix-run/cloudflare";
-
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-import { database, eq } from "@supermemory/db";
-import { users } from "@supermemory/db/schema";
-import { Stripe } from "stripe";
-
-const productsMap = {
- development: {
- "tier-standard-monthly": "price_1Qh6XVP0SZYla2tY19VFeVr6",
- "tier-standard-yearly": "price_1Qh6Y3P0SZYla2tYWXsBH9cv",
- "tier-lifetime": "price_1Qh6YaP0SZYla2tYLfoFQ9Op",
- },
- production: {
- "tier-standard-monthly": "price_1Qh4ozP0SZYla2tYeQEgWirz",
- "tier-standard-yearly": "price_1Qh4ozP0SZYla2tYw4T75Vbn",
- "tier-lifetime": "price_1Qh4p0P0SZYla2tYvBxNomWS",
- },
-};
-
-export const loader = async ({ request, context }: LoaderFunctionArgs) => {
- const tier = new URL(request.url).searchParams.get("tier");
-
- const user = await getSessionFromRequest(request, context);
- const db = await database(context.cloudflare.env.HYPERDRIVE.connectionString);
- const stripe = new Stripe(context.cloudflare.env.STRIPE_CHECKOUT_KEY);
-
- let userInDb = await db.query.users.findFirst({
- where: eq(users.uuid, user?.user?.id ?? ""),
- });
-
- if (!userInDb) {
- return redirect("/");
- }
-
- if (!userInDb.stripeCustomerId) {
- // create one.
- const newCustomer = await stripe.customers.create({
- email: userInDb.email,
- metadata: {
- userId: userInDb.uuid,
- },
- });
- userInDb = (
- await db
- .update(users)
- .set({
- stripeCustomerId: newCustomer.id,
- })
- .where(eq(users.uuid, userInDb.uuid))
- .returning()
- )[0];
- }
-
- if (!userInDb || !userInDb.stripeCustomerId) {
- return redirect("/");
- }
-
- if (!tier || typeof tier == undefined || tier == null) {
- return {
- error: "No tier specified",
- status: 400,
- };
- }
-
- const validTiers = Object.keys(productsMap.development);
- if (!validTiers.includes(tier)) {
- return {
- error: "Invalid tier specified. Valid tiers are: " + validTiers.join(", "),
- status: 400,
- };
- }
-
- const isDev = context.cloudflare.env.NODE_ENV === "development";
- const redirectUrl = isDev ? "http://localhost:3000" : "https://supermemory.ai";
-
- const url = await stripe.checkout.sessions.create({
- success_url: redirectUrl + "/pay/stripe/callback?success=true",
- mode: tier.includes("lifetime") ? "payment" : "subscription",
- customer: userInDb.stripeCustomerId,
- line_items: [
- {
- quantity: 1,
- price:
- productsMap[isDev ? "development" : "production"][
- tier as keyof (typeof productsMap)["development"]
- ],
- },
- ],
- });
-
- return redirect(url.url ?? "error");
-};
diff --git a/apps/web/app/routes/pay.stripe.webhook.tsx b/apps/web/app/routes/pay.stripe.webhook.tsx
deleted file mode 100644
index fe8efcdc..00000000
--- a/apps/web/app/routes/pay.stripe.webhook.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { ActionFunctionArgs } from "@remix-run/cloudflare";
-import { database, eq } from "@supermemory/db";
-import { users } from "@supermemory/db/schema";
-import { Stripe } from "stripe";
-import { allowedEvents } from "~/lib/stripe.constants";
-import { syncStripeDataToDb } from "~/lib/stripe";
-
-export const action = async ({ request, context }: ActionFunctionArgs) => {
- const stripe = new Stripe(context.cloudflare.env.STRIPE_CHECKOUT_KEY);
- const db = await database(context.cloudflare.env.HYPERDRIVE.connectionString);
-
- const payload = await request.text();
- const signature = request.headers.get("stripe-signature");
-
- if (!signature) {
- return new Response(JSON.stringify({}), { status: 400 });
- }
-
- async function processEvent(event: Stripe.Event) {
- if (!allowedEvents.includes(event.type)) return;
-
- const { customer: customerId } = event.data.object as {
- customer: string;
- };
-
- if (typeof customerId !== "string") {
- throw new Error(
- `[STRIPE HOOK][ERROR] ID isn't string.\nEvent type: ${event.type}`
- );
- }
-
- const user = await db.query.users.findFirst({
- where: eq(users.stripeCustomerId, customerId),
- });
-
- if (!user) {
- throw new Error("[STRIPE HOOK] No user found for customer " + customerId);
- }
-
- await syncStripeDataToDb(user.uuid, db, context.cloudflare.env);
- }
-
- async function doEventProcessing() {
- if (typeof signature !== "string") {
- throw new Error("[STRIPE HOOK] Header isn't a string");
- }
-
- const event = stripe.webhooks.constructEvent(
- payload,
- signature,
- context.cloudflare.env.STRIPE_WEBHOOK_SECRET
- );
-
- await processEvent(event);
- }
-
- try {
- await doEventProcessing();
- } catch (error) {
- console.error("[STRIPE HOOK] Error processing event", error);
- }
-
- return new Response(JSON.stringify({ received: true }), {
- status: 200,
- headers: {
- "Content-Type": "application/json",
- },
- });
-};
diff --git a/apps/web/app/routes/pitch.index.tsx b/apps/web/app/routes/pitch.index.tsx
deleted file mode 100644
index 0e1e10f6..00000000
--- a/apps/web/app/routes/pitch.index.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import React, { useEffect } from "react";
-
-import posthog from "posthog-js";
-import { Logo } from "~/components/icons/Logo";
-import { Theme, useTheme } from "~/lib/theme-provider";
-
-function PitchPage1() {
- return (
- <div className="h-screen w-screen flex flex-col justify-center px-4 sm:px-8">
- <Logo className="w-[min(25vw,16rem)] h-[min(25vw,16rem)] mb-4" />
- <div className="w-full max-w-[90vw]">
- <h1 className="text-[min(10vw,12rem)] font-bold tracking-tight leading-none whitespace-nowrap">
- <span className="inline">super</span>
- <span className="inline">memory</span>
- </h1>
- <p className="text-[min(3vw,3rem)] font-medium tracking-tight leading-none mt-8">
- The second brain platform for everyone. <br />
- <span className="text-[min(1.5vw,1rem)] text-gray-500">dhravya shah draft</span>
- </p>
- </div>
- </div>
- );
-}
-
-function PitchPage2() {
- return (
- <div className="h-screen w-screen flex flex-col justify-center px-4 sm:px-8">
- <h2 className="text-[min(5vw,4rem)] font-bold tracking-tight leading-none mb-12">current problems</h2>
- <div className="grid grid-cols-4 gap-4 max-w-7xl mx-auto w-full h-[60vh] relative">
- <div className="absolute -right-24 top-1/2 -translate-y-1/2 w-48">
- <img
- src="https://www.harleytherapy.co.uk/counselling/wp-content/uploads/4624465693_115ce5fa02-400x300.jpg"
- alt="Messy desk with papers"
- className="rounded-lg shadow-lg"
- />
- <div className="font-handwritten text-lg text-blue-600 -rotate-12 mt-2 ml-4">
- current knowledgebase
- <svg className="w-12 h-12 -mt-2 ml-2 transform rotate-45" viewBox="0 0 24 24" fill="none" stroke="currentColor">
- <path d="M5 12h14M12 5l7 7-7 7" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"/>
- </svg>
- </div>
- </div>
-
- <div className="col-span-2 row-span-2 border rounded-xl p-8 hover:bg-gray-50 transition-colors transform hover:scale-105 hover:rotate-1">
- <h3 className="font-semibold text-3xl mb-4">Knowledge Management</h3>
- <p className="text-gray-600 text-lg">Information overload is real. Notes scattered everywhere, bookmarks lost in endless folders, important details buried in email threads. We're building this for Chakshu who's drowning in digital chaos.</p>
- </div>
- <div className="border rounded-xl p-6 hover:bg-gray-50 transition-colors transform hover:-rotate-2">
- <h3 className="font-semibold text-xl mb-2">Trust & Privacy</h3>
- <p className="text-gray-600 text-sm">Developer friends like Kshunya and Siddharth need their data private and secure</p>
- </div>
- <div className="border rounded-xl p-6 hover:bg-gray-50 transition-colors transform hover:rotate-2">
- <h3 className="font-semibold text-xl mb-2">Enterprise Search</h3>
- <p className="text-gray-600 text-sm">Cloudflare's internal knowledge is a maze of confusion</p>
- </div>
- <div className="col-span-2 border rounded-xl p-6 hover:bg-gray-50 transition-colors transform hover:-rotate-1">
- <h3 className="font-semibold text-2xl mb-3">Digital Chaos</h3>
- <p className="text-gray-600">Notes in Notion, bookmarks in Chrome, knowledge in Slack, wisdom in emails... it's everywhere and nowhere</p>
- </div>
- <div className="border rounded-xl p-6 hover:bg-gray-50 transition-colors transform hover:rotate-1">
- <h3 className="font-semibold text-xl mb-2">Information Anxiety</h3>
- <p className="text-gray-600 text-sm">Brent spends hours searching through old emails</p>
- </div>
- <div className="border rounded-xl p-6 hover:bg-gray-50 transition-colors transform hover:-rotate-1">
- <h3 className="font-semibold text-xl mb-2">Developer Cost</h3>
- <p className="text-gray-600 text-sm">Memory APIs are a costly maze of complexity</p>
- </div>
- </div>
- </div>
- );
-}
-
-function Pitch() {
- const [theme, setTheme] = useTheme();
- useEffect(() => {
- posthog.capture("pitch_viewed");
- setTheme(Theme.LIGHT);
- }, []);
-
- return (
- <div className="snap-y snap-mandatory h-screen w-screen overflow-y-auto">
- <div className="snap-start">
- <PitchPage1 />
- </div>
- <div className="snap-start">
- <PitchPage2 />
- </div>
- {/* Add more pages here with snap-start class */}
- </div>
- );
-}
-
-export default Pitch;
diff --git a/apps/web/app/routes/pricing.index.tsx b/apps/web/app/routes/pricing.index.tsx
deleted file mode 100644
index 721d9184..00000000
--- a/apps/web/app/routes/pricing.index.tsx
+++ /dev/null
@@ -1,253 +0,0 @@
-import { LoaderFunctionArgs } from "@remix-run/cloudflare";
-import { useLoaderData } from "@remix-run/react";
-
-import { authkitLoader } from "@supermemory/authkit-remix-cloudflare";
-import { CheckIcon, Sparkles } from "lucide-react";
-import Navbar from "~/components/Navbar";
-
-export const loader = (args: LoaderFunctionArgs) => authkitLoader(args);
-
-const tiers = [
- {
- name: "Free",
- id: "tier-free",
- href: "/signin",
- price: "$0",
- description: "Get started with the essentials and upgrade anytime.",
- features: [
- "1,000 memories",
- "3 spaces",
- "Basic search",
- "Limited API access",
- "Mobile-friendly interface",
- "Community support",
- ],
- featured: false,
- },
- {
- name: "Standard",
- id: "tier-standard",
- href: "/pay/stripe?tier=tier-standard",
- priceMonthly: "$12",
- priceYearly: "$100",
- description: "Perfect for power users who want to unlock their full potential.",
- features: [
- "10,000 memories",
- "100 spaces",
- "Images and videos",
- "Collaborate with teams",
- "Full API access",
- "Auto-assign space",
- "Early access to features",
- "Priority email support",
- "Canvas & Editor (Coming soon)",
- "Own your data & LLM (Coming soon)",
- ],
- featured: true,
- badge: "Most Popular",
- savings: "Save $44/year",
- },
- {
- name: "Early Supporter",
- id: "tier-lifetime",
- href: "/pay/stripe?tier=tier-lifetime",
- price: "$200",
- description: "One-time payment. Access forever. No subscription needed.",
- features: [
- "Unlimited memories",
- "Unlimited spaces",
- "Everything in Standard",
- "Full API access",
- "VIP email & chat support with founder",
- "Canvas & Note Editor",
- "Weekly Review Newsletter",
- "Access forever - no subscription",
- "All future updates included",
- ],
- featured: false,
- badge: "Best Value",
- },
-];
-
-function classNames(...classes: (string | undefined | false)[]) {
- return classes.filter(Boolean).join(" ");
-}
-
-export default function Pay() {
- const { user } = useLoaderData<typeof loader>();
-
- return (
- <div className="min-h-screen font-geist">
- <Navbar user={user ?? undefined} />
- <div className="relative isolate px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8">
- <div
- className="absolute inset-x-0 -top-3 -z-10 transform-gpu overflow-hidden px-36 blur-3xl"
- aria-hidden="true"
- >
- <div
- className="mx-auto aspect-[1155/678] w-[72.1875rem] bg-gradient-to-tr from-[#0ea5e9] to-[#8b5cf6] dark:from-[#0369a1] dark:to-[#6d28d9] opacity-30"
- style={{
- clipPath:
- "polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)",
- }}
- />
- </div>
-
- <div className="mx-auto max-w-4xl text-center">
- <h1 className="text-2xl sm:text-3xl font-semibold tracking-[-0.020em] text-neutral-900 dark:text-neutral-100">
- Supermemory Pricing
- </h1>
- <p className="mt-8 text-balance text-4xl sm:text-5xl font-semibold tracking-tight text-blue-500 dark:text-blue-400 sm:text-6xl">
- Your Intelligent Knowledge Platform for the AI Era
- </p>
- </div>
-
- <p className="mx-auto mt-6 max-w-2xl text-pretty text-center text-base sm:text-lg text-neutral-600 dark:text-neutral-400 sm:text-xl/8">
- Save bookmarks, notes, websites, tweets, and more in one place. Access your personal AI
- knowledge base anytime, anywhere.
- </p>
-
- <div className="mx-auto mt-12 sm:mt-16 grid max-w-lg grid-cols-1 items-stretch gap-y-10 sm:gap-y-0 lg:max-w-6xl lg:grid-cols-3">
- {tiers.map((tier, tierIdx) => (
- <div
- key={tier.id}
- className={classNames(
- tier.featured
- ? "relative bg-neutral-900 dark:bg-blue-950 shadow-2xl rounded-3xl z-10 lg:scale-110"
- : "bg-white/60 dark:bg-white/5 sm:mx-8 lg:mx-0",
- "rounded-3xl",
- "p-6 sm:p-8 ring-1 ring-neutral-900/10 dark:ring-white/10 backdrop-blur-sm sm:p-10",
- "flex flex-col justify-between",
- )}
- >
- {tier.badge && (
- <div className="absolute -top-5 left-0 right-0 mx-auto w-fit px-3 py-1 text-sm font-medium text-white bg-blue-500 rounded-full">
- {tier.badge}
- </div>
- )}
- <div>
- <div className="flex items-center gap-2">
- <h3
- id={tier.id}
- className={classNames(
- tier.featured ? "text-blue-400" : "text-blue-600 dark:text-blue-400",
- "text-base/7 font-semibold",
- )}
- >
- {tier.name}
- </h3>
- {tier.featured && <Sparkles className="h-4 w-4 text-blue-400" />}
- </div>
- <p className="mt-4 flex items-baseline gap-x-2">
- <span
- className={classNames(
- tier.featured ? "text-white" : "text-neutral-900 dark:text-white",
- "text-4xl sm:text-5xl font-semibold tracking-tight",
- )}
- >
- {tier.price || tier.priceMonthly}
- </span>
- {tier.priceMonthly && (
- <span
- className={classNames(
- tier.featured
- ? "text-neutral-300"
- : "text-neutral-500 dark:text-neutral-400",
- "text-sm sm:text-base",
- )}
- >
- /month
- </span>
- )}
- </p>
- {tier.priceYearly && (
- <p
- className={classNames(
- tier.featured ? "text-neutral-300" : "text-neutral-500 dark:text-neutral-400",
- "mt-1 text-sm flex items-center gap-2",
- )}
- >
- or {tier.priceYearly}/year
- {tier.savings && (
- <span className="text-green-500 font-medium">({tier.savings})</span>
- )}
- </p>
- )}
- <p
- className={classNames(
- tier.featured ? "text-neutral-300" : "text-neutral-600 dark:text-neutral-300",
- "mt-6 text-sm sm:text-base/7",
- )}
- >
- {tier.description}
- </p>
- <ul
- role="list"
- className={classNames(
- tier.featured ? "text-neutral-300" : "text-neutral-600 dark:text-neutral-300",
- "mt-6 sm:mt-8 space-y-3 text-sm/6",
- )}
- >
- {tier.features.map((feature) => (
- <li key={feature} className="flex gap-x-3">
- <CheckIcon
- aria-hidden="true"
- className={classNames(
- tier.featured ? "text-blue-400" : "text-blue-600 dark:text-blue-400",
- "h-5 w-5 flex-none",
- )}
- />
- {feature}
- </li>
- ))}
- </ul>
- </div>
- {tier.id === "tier-standard" ? (
- <div className="mt-6 sm:mt-8 flex flex-col sm:flex-row gap-2">
- <a
- href={`${tier.href}-monthly`}
- aria-describedby={tier.id}
- className="flex-1 text-white ring-1 ring-inset ring-blue-500 hover:bg-blue-500/10 focus-visible:outline-blue-500 rounded-md px-3 py-2.5 text-center text-sm font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-all duration-200"
- >
- Start Monthly Plan
- </a>
- <a
- href={`${tier.href}-yearly`}
- aria-describedby={tier.id}
- className="flex-1 bg-blue-500 text-white shadow-sm hover:bg-blue-400 focus-visible:outline-blue-500 rounded-md px-3 py-2.5 text-center text-sm font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-all duration-200"
- >
- Save 37% Yearly
- </a>
- </div>
- ) : (
- <a
- href={tier.href}
- aria-describedby={tier.id}
- className={classNames(
- tier.featured
- ? "bg-blue-500 text-white shadow-sm hover:bg-blue-400 focus-visible:outline-blue-500"
- : "text-blue-600 dark:text-white ring-1 ring-inset ring-blue-200 dark:ring-blue-500 hover:bg-blue-500/10 dark:hover:bg-blue-500/10 focus-visible:outline-blue-600",
- "mt-6 sm:mt-8 block rounded-md px-3 py-2.5 text-center text-sm font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-all duration-200",
- )}
- >
- {tier.price === "$0" ? "Try Supermemory Free" : "Get Lifetime Access"}
- </a>
- )}
- </div>
- ))}
- </div>
-
- <p className="text-center mt-12 sm:mt-16 text-sm sm:text-base text-neutral-600 dark:text-neutral-400">
- For enterprise plans and custom solutions, contact{" "}
- <a
- href="mailto:[email protected]"
- className="text-blue-500 hover:text-blue-400"
- >
- </a>
- </p>
- </div>
- {/* TODO: MORE MARKETING HERE */}
- </div>
- );
-}
diff --git a/apps/web/app/routes/privacy.tsx b/apps/web/app/routes/privacy.tsx
deleted file mode 100644
index 84676609..00000000
--- a/apps/web/app/routes/privacy.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import Markdown from "react-markdown";
-
-function Page() {
- return (
- <div className="flex flex-col items-center justify-center mt-8">
- <div className="max-w-3xl prose md:prose-lg dark:prose-invert">
- <Markdown>
- {`
- Privacy Policy for Supermemory.ai
-# Introduction
-This Privacy Policy provides detailed information on the handling, storage, and protection of your personal information by Supermemory.ai - A web app and a browser extension developed and owned by Dhravya Shah and Supermemory team in 2024. The extension is designed to enhance your browsing experience by providing contextual information based on the content of the web pages you visit. This policy outlines the types of data collected by Supermemory.ai, how it is used, and the measures we take to protect your privacy.
-
-
-Information Collection
-Supermemory.ai collects the following types of information:
-
-### In the web app
-- **Personal Information**:
-When you interact with Supermemory.ai, we may collect personal information including but not limited to your email address, session data, name, and profile picture. This information is collected to improve your user experience and to provide personalized services.
-
-- **Usage Data**:
-Supermemory.ai stores data related to your usage of the web app, including but not limited to the pages you visit, the features you use, and the time spent on the app. This data is used to improve the functionality of the app and to provide you with a better user experience.
-
-- **Saved content**:
-When you use the save feature, Supermemory.ai stores the content you save. This information is used to provide you with quick access to your saved content. This information is also stored in a vector database so that you can easily search for your saved content.
-
-- **Chat and Query Data**:
-When you use the chat feature or submit queries through the web app, we collect the data you provide. This information is used to provide a rich user experience with history of your past conversations.
-
-### In the extension
-- **Web Browsing Data**:
-The extension has the capability to see all websites that users visit. However, Supermemory.ai only stores data when the user actively clicks on the extension button while browsing. The browser history is not recorded, ensuring that your browsing activities remain private.
-
-- **Current page data**: Upon activation (click) by the user, Supermemory.ai stores data from the current HTML page. This data is used to provide relevant contextual information based on the content of the page you are viewing.
-
-## Data Storage and Security
-All collected data is securely stored in a Database hosted on [Hetzner](https://www.hetzner.com/).
-We employ industry-standard security measures to protect your information from unauthorized access, alteration, disclosure, or destruction. Despite our efforts, no method of transmission over the Internet or method of electronic storage is 100% secure. Therefore, while we strive to use commercially acceptable means to protect your personal information, we cannot guarantee its absolute security.
-
-When you chat with the app, your queries may be sent to OpenAI GPT-4 API, Google Gemini API or other third-party services to provide you with relevant information. These services may store your queries and responses for training purposes.
-
-## Use of Information
-Supermemory.ai uses the collected information for the following purposes:
-
-To provide and improve the functionality of the web app and the extension.
-To offer personalized user experiences.
-To communicate with users regarding updates, support, and promotional offers, if consented.
-To ensure the security of our services and to detect, prevent, or address technical issues.
-Sharing of Information
-Supermemory.ai does not sell, trade, or rent users' personal identification information to others. We may share generic aggregated demographic information not linked to any personal identification information regarding visitors and users with our business partners, trusted affiliates, and advertisers for the purposes outlined above.
-
-# Your Privacy Rights
-You have the right to access, update, or delete your personal information that we hold. If you wish to exercise these rights, please contact us at the details provided below.
-
-## Changes to This Privacy Policy
-Supermemory.ai reserves the right to update this privacy policy at any time. When we do, we will post a notification on our website and update the date at the top of this page. We encourage users to frequently check this page for any changes to stay informed about how we are protecting the personal information we collect. Your continued use of the service after the posting of changes to this policy will be deemed your acceptance of those changes.
-
-# Contact Us
-If you have any questions about this Privacy Policy, the practices of this site, or your dealings with this site, please contact us at:
-
-This document was last updated on February 14, 2025.
-
-`}
- </Markdown>
- </div>
- </div>
- );
-}
-
-export default Page; \ No newline at end of file
diff --git a/apps/web/app/routes/ref.tsx b/apps/web/app/routes/ref.tsx
deleted file mode 100644
index dbf74a1c..00000000
--- a/apps/web/app/routes/ref.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-// import { getSignInUrl, signOut, authkitLoader } from '@supermemory/authkit-remix-cloudflare';
-import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/cloudflare";
-import { json } from "@remix-run/cloudflare";
-import { Form, Link, useLoaderData } from "@remix-run/react";
-
-import { Theme, useTheme } from "~/lib/theme-provider";
-
-export const loader = (args: LoaderFunctionArgs) => {
- return json({});
-};
-
-export default function HomePage() {
- const [theme, setTheme] = useTheme();
-
- return (
- <div className="min-h-screen">
- <Form method="post">
- {/* <p>Welcome back {user?.firstName && `, ${user?.firstName}`}</p> */}
- <button type="submit">Sign out</button>
- </Form>
-
- {/* <button onClick={() => setTheme(theme === Theme.LIGHT ? Theme.DARK : Theme.LIGHT)}>{theme}</button> */}
- </div>
- );
-}
diff --git a/apps/web/app/routes/signin.tsx b/apps/web/app/routes/signin.tsx
deleted file mode 100644
index 97303b75..00000000
--- a/apps/web/app/routes/signin.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { LoaderFunctionArgs, redirect } from "@remix-run/cloudflare";
-import { getSignInUrl } from "@supermemory/authkit-remix-cloudflare";
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-
-export async function loader({ request, context }: LoaderFunctionArgs) {
-
- const signinUrl = await getSignInUrl(context);
- return redirect(signinUrl);
-}
diff --git a/apps/web/app/routes/space.$spaceId.tsx b/apps/web/app/routes/space.$spaceId.tsx
deleted file mode 100644
index a2a88caa..00000000
--- a/apps/web/app/routes/space.$spaceId.tsx
+++ /dev/null
@@ -1,388 +0,0 @@
-import { useEffect, useState } from "react";
-
-import { LoaderFunctionArgs, json } from "@remix-run/cloudflare";
-import { useLoaderData, useNavigate } from "@remix-run/react";
-
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-import { Clipboard, DeleteIcon, Pencil, Share, Star, Trash, UserPlus } from "lucide-react";
-import { proxy } from "server/proxy";
-import { toast } from "sonner";
-import Navbar from "~/components/Navbar";
-import MemoriesPage from "~/components/memories/MemoriesPage";
-import { Button } from "~/components/ui/button";
-import { Input } from "~/components/ui/input";
-import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "~/components/ui/select";
-
-interface Space {
- id: number;
- uuid: string;
- name: string;
- ownerId: string;
- isPublic: boolean;
- createdAt: string;
-}
-
-export async function loader({ params, request, context }: LoaderFunctionArgs) {
- const session = await getSessionFromRequest(request, context);
-
- const spaceId = params.spaceId?.split("---")[0];
-
- if (!spaceId) {
- throw new Response("Space not found", { status: 404 });
- }
-
- try {
- // Fetch space details and check access
- const response = await proxy(`/v1/spaces/${spaceId}`, { method: "GET" }, request, context);
-
- if (!response.ok) {
- if (response.status === 404) {
- throw new Response("Space not found", { status: 404 });
- }
- if (response.status === 403) {
- throw new Response("Access denied", { status: 403 });
- }
- throw new Response("Failed to load space", { status: 500 });
- }
-
- const space = (await response.json()) as Space & {
- permissions: { canRead: boolean; canEdit: boolean; isOwner: boolean };
- };
-
- return json({
- space,
- user: session?.user,
- });
- } catch (error) {
- console.error(
- "Error loading space:",
- error instanceof Error ? error.message : "Unknown error " + JSON.stringify(error),
- );
- throw new Response("Failed to load space", { status: 500 });
- }
-}
-
-export default function SpacePage() {
- const { space, user } = useLoaderData<typeof loader>();
- const navigate = useNavigate();
- const [email, setEmail] = useState("");
- const [accessType, setAccessType] = useState<"read" | "edit">("read");
- const [isInviting, setIsInviting] = useState(false);
- const [isFavorited, setIsFavorited] = useState(false);
- const [isEditing, setIsEditing] = useState(false);
- const [editedName, setEditedName] = useState(space.name);
- const [isUpdating, setIsUpdating] = useState(false);
-
- useEffect(() => {
- // Only update if we're on exactly /space/spaceid and not already in the correct format
- if (
- window.location.pathname === `/space/${space.uuid}` &&
- !window.location.pathname.includes("---")
- ) {
- // Convert space name to URL-friendly format
- const urlFriendlyName = space.name
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, "-")
- .replace(/^-+|-+$/g, "");
-
- const newPath = `/space/${space.uuid}---${urlFriendlyName}`;
- // Use push instead of replace to maintain history
- navigate(newPath);
- }
- }, [space, navigate]);
-
- const handleEditName = async () => {
- if (!editedName.trim() || editedName.trim() === space.name || isUpdating) return;
-
- setIsUpdating(true);
- try {
- const response = await fetch(`/backend/v1/spaces/${space.uuid}`, {
- method: "PATCH",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ name: editedName.trim() }),
- credentials: "include",
- });
-
- if (!response.ok) {
- const data = (await response.json()) as { error: string };
- throw new Error(data.error || "Failed to update space name");
- }
-
- toast.success("Space name updated successfully");
- // Update URL with new name
- const urlFriendlyName = editedName
- .trim()
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, "-")
- .replace(/^-+|-+$/g, "");
- navigate(`/space/${space.uuid}---${urlFriendlyName}`);
- } catch (error) {
- toast.error(error instanceof Error ? error.message : "Failed to update space name");
- setEditedName(space.name); // Reset to original name on error
- } finally {
- setIsUpdating(false);
- setIsEditing(false);
- }
- };
-
- const handleInvite = async () => {
- if (!email) return;
-
- setIsInviting(true);
- try {
- const response = await fetch(`/backend/v1/spaces/${space.uuid}/invite`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ email, accessType }),
- credentials: "include",
- });
-
- if (!response.ok) {
- const data = (await response.json()) as { error: string };
- throw new Error(data.error || "Failed to send invite");
- }
-
- toast.success(`Invitation sent to ${email}`);
- setEmail("");
- } catch (error) {
- toast.error(error instanceof Error ? error.message : "Failed to send invite");
- } finally {
- setIsInviting(false);
- }
- };
-
- const handleFavorite = async () => {
- try {
- const response = await fetch(`/backend/v1/spaces/${space.uuid}/favorite`, {
- method: isFavorited ? "DELETE" : "POST",
- headers: { "Content-Type": "application/json" },
- credentials: "include",
- });
-
- if (!response.ok) {
- const data = (await response.json()) as { error: string };
- toast.error(data.error || "Failed to update favorite status");
- throw new Error("Failed to update favorite status");
- }
-
- setIsFavorited(!isFavorited);
- toast.success(isFavorited ? "Removed from favorites" : "Added to favorites");
- } catch (error) {
- toast.error("Failed to update favorite status");
- }
- };
-
- const handleShare = async () => {
- const shareUrl = window.location.href;
-
- if (navigator.share) {
- try {
- await navigator.share({
- title: `${space.name} - Supermemory Space`,
- text: `Check out this space on Supermemory: ${space.name}`,
- url: shareUrl,
- });
- } catch (err) {
- // Fallback to clipboard if share fails or is cancelled
- await navigator.clipboard.writeText(shareUrl);
- }
- } else {
- await navigator.clipboard.writeText(shareUrl);
- }
- };
-
- const handleCopyLink = async () => {
- const shareUrl = window.location.href;
- await navigator.clipboard.writeText(shareUrl);
- toast.success("Link copied to clipboard!");
- };
-
- const handleDelete = async () => {
- const response = await fetch(`/backend/v1/spaces/${space.uuid}`, {
- method: "DELETE",
- credentials: "include",
- });
-
- if (!response.ok) {
- toast.error("Failed to delete space");
- } else {
- navigate("/");
- toast.success("Space deleted successfully");
- }
- };
-
- return (
- <div>
- <Navbar user={user} />
- <div className="p-4 font-geist md:p-24 md:pt-16">
- <div className="flex flex-col gap-8">
- <div className="flex flex-col gap-2">
- <div className="flex flex-col md:flex-row items-start md:items-center gap-3 md:justify-between">
- <div className="flex flex-col md:flex-row items-start md:items-center gap-3">
- {isEditing && space.permissions.isOwner ? (
- <div className="flex items-center gap-2">
- <Input
- value={editedName}
- onChange={(e) => setEditedName(e.target.value)}
- className="text-3xl font-semibold h-auto py-1"
- autoFocus
- onKeyDown={(e) => {
- if (e.key === "Enter") {
- handleEditName();
- } else if (e.key === "Escape") {
- setIsEditing(false);
- setEditedName(space.name);
- }
- }}
- />
- <Button
- variant="ghost"
- size="sm"
- onClick={() => {
- setIsEditing(false);
- setEditedName(space.name);
- }}
- className="text-muted-foreground"
- >
- Cancel
- </Button>
- <Button
- variant="default"
- size="sm"
- onClick={handleEditName}
- disabled={
- isUpdating || !editedName.trim() || editedName.trim() === space.name
- }
- >
- {isUpdating ? "Saving..." : "Save"}
- </Button>
- </div>
- ) : (
- <div className="flex items-center gap-2">
- <h1 className="font-geist text-3xl font-semibold dark:text-neutral-100 text-neutral-700 tracking-[-0.020em]">
- {space.name}
- </h1>
- {space.permissions.isOwner && (
- <Button
- variant="ghost"
- size="sm"
- onClick={() => setIsEditing(true)}
- className="text-muted-foreground"
- >
- <Pencil className="h-4 w-4" />
- </Button>
- )}
- </div>
- )}
- {space.permissions.canEdit && (
- <span className="px-2 py-0.5 text-sm bg-emerald-100 dark:bg-emerald-900 text-emerald-700 dark:text-emerald-300 rounded-md">
- Can Edit
- </span>
- )}
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
- <div
- className={`w-2 h-2 rounded-full ${space.isPublic ? "bg-green-500" : "bg-yellow-500"}`}
- />
- <span>{space.isPublic ? "Public Space" : "Private Space"}</span>
- </div>
- </div>
- <div className="flex items-center gap-2">
- {space.permissions.isOwner && (
- <Popover>
- <PopoverTrigger asChild>
- <Button variant="outline" size="sm">
- <UserPlus className="h-4 w-4" />
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-80">
- <div className="grid gap-4">
- <div className="space-y-2">
- <h4 className="font-medium leading-none">Invite to Space</h4>
- <p className="text-sm text-muted-foreground">
- Invite users to collaborate in this space
- </p>
- </div>
- <div className="grid gap-2">
- <Input
- type="email"
- placeholder="Email address"
- value={email}
- onChange={(e) => setEmail(e.target.value)}
- />
- <Select
- value={accessType}
- onValueChange={(value: "read" | "edit") => setAccessType(value)}
- >
- <SelectTrigger>
- <SelectValue placeholder="Select permission" />
- </SelectTrigger>
- <SelectContent>
- {!space.isPublic && <SelectItem value="read">Can View</SelectItem>}
- <SelectItem value="edit">Can Edit</SelectItem>
- </SelectContent>
- </Select>
- <Button onClick={handleInvite} disabled={isInviting}>
- {isInviting ? "Sending..." : "Send Invite"}
- </Button>
- </div>
- </div>
- </PopoverContent>
- </Popover>
- )}
- <Button variant="outline" size="sm" onClick={handleShare}>
- <Share className="h-4 w-4" />
- </Button>
- <Button variant="outline" size="sm" onClick={handleCopyLink}>
- <Clipboard className="h-4 w-4" />
- </Button>
- <Button variant="outline" size="sm" onClick={handleDelete}>
- <Trash className="h-4 w-4" />
- </Button>
-
- {space.isPublic &&
- user &&
- !space.permissions.isOwner &&
- !space.permissions.canEdit &&
- !space.permissions.canRead && (
- <Button
- variant="ghost"
- size="sm"
- onClick={handleFavorite}
- className={`ml-2 ${isFavorited ? "text-yellow-500" : "text-gray-500"}`}
- >
- <Star className="h-4 w-4" fill={isFavorited ? "currentColor" : "none"} />
- </Button>
- )}
- </div>
- </div>
- </div>
-
- <div className="w-full">
- <MemoriesPage showAddButtons={space.permissions.canEdit} isSpace={true} />
- </div>
- </div>
- </div>
- </div>
- );
-}
-
-// Error boundary component
-export function ErrorBoundary() {
- return (
- <div className="flex min-h-screen items-center justify-center">
- <div className="text-center">
- <h1 className="text-2xl font-bold">Oops!</h1>
- <p className="text-muted-foreground">
- We couldn't load this space. It might not exist or you might not have access to it.
- </p>
- </div>
- </div>
- );
-}
diff --git a/apps/web/app/routes/space.($spaceId).invitation.tsx b/apps/web/app/routes/space.($spaceId).invitation.tsx
deleted file mode 100644
index 26b3b2df..00000000
--- a/apps/web/app/routes/space.($spaceId).invitation.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-// invitation page. user should be logged in to see this page.
-import { useEffect } from "react";
-
-import { LoaderFunctionArgs, json, redirect } from "@remix-run/cloudflare";
-import { useLoaderData, useNavigate } from "@remix-run/react";
-
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-import { Space } from "@supermemory/db/schema";
-import posthog from "posthog-js";
-import { proxy } from "server/proxy";
-import { Button } from "~/components/ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
-
-export async function loader({ request, params, context }: LoaderFunctionArgs) {
- const user = await getSessionFromRequest(request, context);
- if (!user) {
- return redirect("/signin");
- }
-
- const spaceId = params.spaceId?.split("---")[0];
-
- if (!spaceId) {
- return redirect("/");
- }
-
- try {
- // Check if user has pending invitation
- const response = await proxy(`/v1/spaces/${spaceId}/invitation`, {}, request, context);
-
- const myJson = await response.json();
-
- console.log(myJson);
-
- const invitation = myJson as {
- space: Space;
- accessType: "read" | "edit";
- };
- return json({ invitation, user });
- } catch (error) {
- // If 403, there's no invitation found
- if (error instanceof Error && error.message.includes("403")) {
- return redirect("/");
- }
- throw error;
- }
-}
-
-export default function SpaceInvitation() {
- const { invitation, user } = useLoaderData<typeof loader>();
- console.log(invitation);
- const navigate = useNavigate();
-
- async function handleInviteResponse(action: "accept" | "reject") {
- const response = await fetch(`/backend/v1/spaces/invites/${action}`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- spaceId: invitation.space.uuid,
- }),
- credentials: "include",
- });
-
- if (response.ok) {
- // Redirect to space page after accepting/rejecting
- navigate(`/space/${invitation.space.uuid}`);
- } else {
- console.log("Error accepting/rejecting invitation", response);
- }
- }
-
- return (
- <div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8 flex items-center justify-center">
- <Card className="w-full max-w-md">
- <CardHeader className="text-center">
- <div className="mx-auto w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center mb-4">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- className="h-6 w-6 text-blue-600 dark:text-blue-300"
- viewBox="0 0 20 20"
- fill="currentColor"
- >
- <path d="M8 9a3 3 0 100-6 3 3 0 000 6zM8 11a6 6 0 016 6H2a6 6 0 016-6zM16 7a1 1 0 10-2 0v1h-1a1 1 0 100 2h1v1a1 1 0 102 0v-1h1a1 1 0 100-2h-1V7z" />
- </svg>
- </div>
- <CardTitle className="text-2xl font-bold">Space Invitation</CardTitle>
- <CardDescription className="mt-2">
- You've been invited to join{" "}
- <span className="font-medium text-blue-600 dark:text-blue-400">
- {invitation.space.name}
- </span>
- </CardDescription>
- </CardHeader>
- <CardContent>
- <div className="space-y-4">
- <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
- <p className="text-sm text-gray-600 dark:text-gray-300">
- Access Level:{" "}
- <span className="font-medium capitalize">{invitation.accessType}</span>
- </p>
- </div>
- <div className="flex flex-col sm:flex-row gap-3">
- <Button
- onClick={() => handleInviteResponse("accept")}
- className="flex-1 bg-blue-600 hover:bg-blue-700"
- >
- Accept Invitation
- </Button>
- <Button
- onClick={() => handleInviteResponse("reject")}
- variant="outline"
- className="flex-1 border-red-200 text-red-600 hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-900/20"
- >
- Decline
- </Button>
- </div>
- </div>
- </CardContent>
- </Card>
- </div>
- );
-}
diff --git a/apps/web/app/routes/tos.tsx b/apps/web/app/routes/tos.tsx
deleted file mode 100644
index 32d73e36..00000000
--- a/apps/web/app/routes/tos.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import React from "react";
-import Markdown from "react-markdown";
-
-function Page() {
- return (
- <div className="flex flex-col items-center justify-center mt-8">
- <div className="max-w-3xl prose prose-invert">
- <Markdown>
- {`
- Terms of Service for Supermemory.ai
-
-**Effective Date:** July 4, 2024
-
-Welcome to Supermemory! By using our app, you agree to the following terms and conditions. Please read them carefully.
-
-### 1. Acceptance of Terms
-By accessing or using the Supermemory app, you agree to be bound by these Terms of Service and our Privacy Policy. If you do not agree with any part of these terms, you must not use the app.
-
-### 2. Description of Service
-Supermemory allows users to save information from various sources on the internet, organize it, and interact with it using AI. The service includes features such as:
-- **Knowledge Canvas:** Arrange your saved information in a 2D canvas.
-- **Writing Assistant:** Use our markdown editor to create content with AI assistance.
-- **Data Collection:** Collect data from any website and bring it into your second brain.
-- **Powerful Search:** Quickly find any saved information.
-
-### 3. User Responsibilities
-- **Account Security:** You are responsible for maintaining the confidentiality of your account and password.
-- **Content:** You must ensure that any content you save or upload does not violate any laws or third-party rights.
-- **Usage:** You agree not to misuse the app, including but not limited to, attempting to gain unauthorized access to the service or its related systems.
-
-### 4. Intellectual Property
-All content, trademarks, and data on the Supermemory app are the property of Supermemory or its licensors. You may not use any of this content without permission.
-
-### 5. Privacy
-Your use of the app is also governed by our Privacy Policy, which explains how we collect, use, and protect your information.
-
-### 6. Termination
-We reserve the right to suspend or terminate your access to the app at our discretion, without notice, for conduct that we believe violates these Terms of Service or is harmful to other users.
-
-### 7. Limitation of Liability
-Supermemory is provided "as is" without any warranties of any kind. We do not guarantee that the service will be uninterrupted or error-free. To the fullest extent permitted by law, Supermemory will not be liable for any damages arising from the use of the app.
-
-### 8. Changes to Terms
-We may update these Terms of Service from time to time. We will notify you of any changes by posting the new terms on our app. Your continued use of the app after such changes constitutes your acceptance of the new terms.
-
-### 9. Contact Information
-If you have any questions about these Terms of Service, please contact us at [email protected].
-
-**By using Supermemory, you acknowledge that you have read, understood, and agree to be bound by these Terms of Service.**
-`}
- </Markdown>
- </div>
- </div>
- );
-}
-
-export default Page; \ No newline at end of file
diff --git a/apps/web/app/sonner.css b/apps/web/app/sonner.css
deleted file mode 100644
index 1c6d856e..00000000
--- a/apps/web/app/sonner.css
+++ /dev/null
@@ -1,656 +0,0 @@
-:where(html[dir='ltr']),
-:where([data-sonner-toaster][dir='ltr']) {
- --toast-icon-margin-start: -3px;
- --toast-icon-margin-end: 4px;
- --toast-svg-margin-start: -1px;
- --toast-svg-margin-end: 0px;
- --toast-button-margin-start: auto;
- --toast-button-margin-end: 0;
- --toast-close-button-start: 0;
- --toast-close-button-end: unset;
- --toast-close-button-transform: translate(-35%, -35%);
-}
-
-:where(html[dir='rtl']),
-:where([data-sonner-toaster][dir='rtl']) {
- --toast-icon-margin-start: 4px;
- --toast-icon-margin-end: -3px;
- --toast-svg-margin-start: 0px;
- --toast-svg-margin-end: -1px;
- --toast-button-margin-start: 0;
- --toast-button-margin-end: auto;
- --toast-close-button-start: unset;
- --toast-close-button-end: 0;
- --toast-close-button-transform: translate(35%, -35%);
-}
-
-:where([data-sonner-toaster]) {
- position: fixed;
- width: var(--width);
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial,
- Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
- --gray1: hsl(0, 0%, 99%);
- --gray2: hsl(0, 0%, 97.3%);
- --gray3: hsl(0, 0%, 95.1%);
- --gray4: hsl(0, 0%, 93%);
- --gray5: hsl(0, 0%, 90.9%);
- --gray6: hsl(0, 0%, 88.7%);
- --gray7: hsl(0, 0%, 85.8%);
- --gray8: hsl(0, 0%, 78%);
- --gray9: hsl(0, 0%, 56.1%);
- --gray10: hsl(0, 0%, 52.3%);
- --gray11: hsl(0, 0%, 43.5%);
- --gray12: hsl(0, 0%, 9%);
- --border-radius: 8px;
- box-sizing: border-box;
- padding: 0;
- margin: 0;
- list-style: none;
- outline: none;
- z-index: 999999999;
- transition: transform 400ms ease;
-}
-
-:where([data-sonner-toaster][data-lifted='true']) {
- transform: translateY(-10px);
-}
-
-@media (hover: none) and (pointer: coarse) {
- :where([data-sonner-toaster][data-lifted='true']) {
- transform: none;
- }
-}
-
-:where([data-sonner-toaster][data-x-position='right']) {
- right: max(var(--offset), env(safe-area-inset-right));
-}
-
-:where([data-sonner-toaster][data-x-position='left']) {
- left: max(var(--offset), env(safe-area-inset-left));
-}
-
-:where([data-sonner-toaster][data-x-position='center']) {
- left: 50%;
- transform: translateX(-50%);
-}
-
-:where([data-sonner-toaster][data-y-position='top']) {
- top: max(var(--offset), env(safe-area-inset-top));
-}
-
-:where([data-sonner-toaster][data-y-position='bottom']) {
- bottom: max(var(--offset), env(safe-area-inset-bottom));
-}
-
-:where([data-sonner-toast]) {
- --y: translateY(100%);
- --lift-amount: calc(var(--lift) * var(--gap));
- z-index: var(--z-index);
- position: absolute;
- opacity: 0;
- transform: var(--y);
- filter: blur(0);
- /* https://stackoverflow.com/questions/48124372/pointermove-event-not-working-with-touch-why-not */
- touch-action: none;
- transition: transform 400ms, opacity 400ms, height 400ms, box-shadow 200ms;
- box-sizing: border-box;
- outline: none;
- overflow-wrap: anywhere;
-}
-
-:where([data-sonner-toast][data-styled='true']) {
- padding: 16px;
- background: var(--normal-bg);
- border: 1px solid var(--normal-border);
- color: var(--normal-text);
- border-radius: var(--border-radius);
- box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
- width: var(--width);
- font-size: 13px;
- display: flex;
- align-items: center;
- gap: 6px;
-}
-
-:where([data-sonner-toast]:focus-visible) {
- box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2);
-}
-
-:where([data-sonner-toast][data-y-position='top']) {
- top: 0;
- --y: translateY(-100%);
- --lift: 1;
- --lift-amount: calc(1 * var(--gap));
-}
-
-:where([data-sonner-toast][data-y-position='bottom']) {
- bottom: 0;
- --y: translateY(100%);
- --lift: -1;
- --lift-amount: calc(var(--lift) * var(--gap));
-}
-
-:where([data-sonner-toast]) :where([data-description]) {
- font-weight: 400;
- line-height: 1.4;
- color: inherit;
-}
-
-:where([data-sonner-toast]) :where([data-title]) {
- font-weight: 500;
- line-height: 1.5;
- color: inherit;
-}
-
-:where([data-sonner-toast]) :where([data-icon]) {
- display: flex;
- height: 16px;
- width: 16px;
- position: relative;
- justify-content: flex-start;
- align-items: center;
- flex-shrink: 0;
- margin-left: var(--toast-icon-margin-start);
- margin-right: var(--toast-icon-margin-end);
-}
-
-:where([data-sonner-toast][data-promise='true']) :where([data-icon]) > svg {
- opacity: 0;
- transform: scale(0.8);
- transform-origin: center;
- animation: sonner-fade-in 300ms ease forwards;
-}
-
-:where([data-sonner-toast]) :where([data-icon]) > * {
- flex-shrink: 0;
-}
-
-:where([data-sonner-toast]) :where([data-icon]) svg {
- margin-left: var(--toast-svg-margin-start);
- margin-right: var(--toast-svg-margin-end);
-}
-
-:where([data-sonner-toast]) :where([data-content]) {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-[data-sonner-toast][data-styled='true'] [data-button] {
- border-radius: 4px;
- padding-left: 8px;
- padding-right: 8px;
- height: 24px;
- font-size: 12px;
- color: var(--normal-bg);
- background: var(--normal-text);
- margin-left: var(--toast-button-margin-start);
- margin-right: var(--toast-button-margin-end);
- border: none;
- cursor: pointer;
- outline: none;
- display: flex;
- align-items: center;
- flex-shrink: 0;
- transition: opacity 400ms, box-shadow 200ms;
-}
-
-:where([data-sonner-toast]) :where([data-button]):focus-visible {
- box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4);
-}
-
-:where([data-sonner-toast]) :where([data-button]):first-of-type {
- margin-left: var(--toast-button-margin-start);
- margin-right: var(--toast-button-margin-end);
-}
-
-:where([data-sonner-toast]) :where([data-cancel]) {
- color: var(--normal-text);
- background: rgba(0, 0, 0, 0.08);
-}
-
-:where([data-sonner-toast][data-theme='dark']) :where([data-cancel]) {
- background: rgba(255, 255, 255, 0.3);
-}
-
-:where([data-sonner-toast]) :where([data-close-button]) {
- position: absolute;
- left: var(--toast-close-button-start);
- right: var(--toast-close-button-end);
- top: 0;
- height: 20px;
- width: 20px;
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 0;
- color: var(--gray12);
- border: 1px solid var(--gray4);
- transform: var(--toast-close-button-transform);
- border-radius: 50%;
- cursor: pointer;
- z-index: 1;
- transition: opacity 100ms, background 200ms, border-color 200ms;
-}
-
-[data-sonner-toast] [data-close-button] {
- background: var(--gray1);
-}
-
-:where([data-sonner-toast]) :where([data-close-button]):focus-visible {
- box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2);
-}
-
-:where([data-sonner-toast]) :where([data-disabled='true']) {
- cursor: not-allowed;
-}
-
-:where([data-sonner-toast]):hover :where([data-close-button]):hover {
- background: var(--gray2);
- border-color: var(--gray5);
-}
-
-/* Leave a ghost div to avoid setting hover to false when swiping out */
-:where([data-sonner-toast][data-swiping='true'])::before {
- content: '';
- position: absolute;
- left: 0;
- right: 0;
- height: 100%;
- z-index: -1;
-}
-
-:where([data-sonner-toast][data-y-position='top'][data-swiping='true'])::before {
- /* y 50% needed to distribute height additional height evenly */
- bottom: 50%;
- transform: scaleY(3) translateY(50%);
-}
-
-:where([data-sonner-toast][data-y-position='bottom'][data-swiping='true'])::before {
- /* y -50% needed to distribute height additional height evenly */
- top: 50%;
- transform: scaleY(3) translateY(-50%);
-}
-
-/* Leave a ghost div to avoid setting hover to false when transitioning out */
-:where([data-sonner-toast][data-swiping='false'][data-removed='true'])::before {
- content: '';
- position: absolute;
- inset: 0;
- transform: scaleY(2);
-}
-
-/* Needed to avoid setting hover to false when inbetween toasts */
-:where([data-sonner-toast])::after {
- content: '';
- position: absolute;
- left: 0;
- height: calc(var(--gap) + 1px);
- bottom: 100%;
- width: 100%;
-}
-
-:where([data-sonner-toast][data-mounted='true']) {
- --y: translateY(0);
- opacity: 1;
-}
-
-:where([data-sonner-toast][data-expanded='false'][data-front='false']) {
- --scale: var(--toasts-before) * 0.05 + 1;
- --y: translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale)));
- height: var(--front-toast-height);
-}
-
-:where([data-sonner-toast]) > * {
- transition: opacity 400ms;
-}
-
-:where([data-sonner-toast][data-expanded='false'][data-front='false'][data-styled='true']) > * {
- opacity: 0;
-}
-
-:where([data-sonner-toast][data-visible='false']) {
- opacity: 0;
- pointer-events: none;
-}
-
-:where([data-sonner-toast][data-mounted='true'][data-expanded='true']) {
- --y: translateY(calc(var(--lift) * var(--offset)));
- height: var(--initial-height);
-}
-
-:where([data-sonner-toast][data-removed='true'][data-front='true'][data-swipe-out='false']) {
- --y: translateY(calc(var(--lift) * -100%));
- opacity: 0;
-}
-
-:where([data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='true']) {
- --y: translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%));
- opacity: 0;
-}
-
-:where([data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='false']) {
- --y: translateY(40%);
- opacity: 0;
- transition: transform 500ms, opacity 200ms;
-}
-
-/* Bump up the height to make sure hover state doesn't get set to false */
-:where([data-sonner-toast][data-removed='true'][data-front='false'])::before {
- height: calc(var(--initial-height) + 20%);
-}
-
-[data-sonner-toast][data-swiping='true'] {
- transform: var(--y) translateY(var(--swipe-amount, 0px));
- transition: none;
-}
-
-[data-sonner-toast][data-swiped='true'] {
- user-select: none;
-}
-
-[data-sonner-toast][data-swipe-out='true'][data-y-position='bottom'],
-[data-sonner-toast][data-swipe-out='true'][data-y-position='top'] {
- animation: swipe-out 200ms ease-out forwards;
-}
-
-@keyframes swipe-out {
- from {
- transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount)));
- opacity: 1;
- }
-
- to {
- transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount) + var(--lift) * -100%));
- opacity: 0;
- }
-}
-
-@media (max-width: 600px) {
- [data-sonner-toaster] {
- position: fixed;
- --mobile-offset: 16px;
- right: var(--mobile-offset);
- left: var(--mobile-offset);
- width: 100%;
- }
-
- [data-sonner-toaster][dir='rtl'] {
- left: calc(var(--mobile-offset) * -1);
- }
-
- [data-sonner-toaster] [data-sonner-toast] {
- left: 0;
- right: 0;
- width: calc(100% - var(--mobile-offset) * 2);
- }
-
- [data-sonner-toaster][data-x-position='left'] {
- left: var(--mobile-offset);
- }
-
- [data-sonner-toaster][data-y-position='bottom'] {
- bottom: 20px;
- }
-
- [data-sonner-toaster][data-y-position='top'] {
- top: 20px;
- }
-
- [data-sonner-toaster][data-x-position='center'] {
- left: var(--mobile-offset);
- right: var(--mobile-offset);
- transform: none;
- }
-}
-
-[data-sonner-toaster][data-theme='light'] {
- --normal-bg: #fff;
- --normal-border: var(--gray4);
- --normal-text: var(--gray12);
-
- --success-bg: hsl(143, 85%, 96%);
- --success-border: hsl(145, 92%, 91%);
- --success-text: hsl(140, 100%, 27%);
-
- --info-bg: hsl(208, 100%, 97%);
- --info-border: hsl(221, 91%, 91%);
- --info-text: hsl(210, 92%, 45%);
-
- --warning-bg: hsl(49, 100%, 97%);
- --warning-border: hsl(49, 91%, 91%);
- --warning-text: hsl(31, 92%, 45%);
-
- --error-bg: hsl(359, 100%, 97%);
- --error-border: hsl(359, 100%, 94%);
- --error-text: hsl(360, 100%, 45%);
-}
-
-[data-sonner-toaster][data-theme='light'] [data-sonner-toast][data-invert='true'] {
- --normal-bg: #000;
- --normal-border: hsl(0, 0%, 20%);
- --normal-text: var(--gray1);
-}
-
-[data-sonner-toaster][data-theme='dark'] [data-sonner-toast][data-invert='true'] {
- --normal-bg: #fff;
- --normal-border: var(--gray3);
- --normal-text: var(--gray12);
-}
-
-[data-sonner-toaster][data-theme='dark'] {
- --normal-bg: #000;
- --normal-border: hsl(0, 0%, 20%);
- --normal-text: var(--gray1);
-
- --success-bg: hsl(150, 100%, 6%);
- --success-border: hsl(147, 100%, 12%);
- --success-text: hsl(150, 86%, 65%);
-
- --info-bg: hsl(215, 100%, 6%);
- --info-border: hsl(223, 100%, 12%);
- --info-text: hsl(216, 87%, 65%);
-
- --warning-bg: hsl(64, 100%, 6%);
- --warning-border: hsl(60, 100%, 12%);
- --warning-text: hsl(46, 87%, 65%);
-
- --error-bg: hsl(358, 76%, 10%);
- --error-border: hsl(357, 89%, 16%);
- --error-text: hsl(358, 100%, 81%);
-}
-
-[data-rich-colors='true'][data-sonner-toast][data-type='success'] {
- background: var(--success-bg);
- border-color: var(--success-border);
- color: var(--success-text);
-}
-
-[data-rich-colors='true'][data-sonner-toast][data-type='success'] [data-close-button] {
- background: var(--success-bg);
- border-color: var(--success-border);
- color: var(--success-text);
-}
-
-[data-rich-colors='true'][data-sonner-toast][data-type='info'] {
- background: var(--info-bg);
- border-color: var(--info-border);
- color: var(--info-text);
-}
-
-[data-rich-colors='true'][data-sonner-toast][data-type='info'] [data-close-button] {
- background: var(--info-bg);
- border-color: var(--info-border);
- color: var(--info-text);
-}
-
-[data-rich-colors='true'][data-sonner-toast][data-type='warning'] {
- background: var(--warning-bg);
- border-color: var(--warning-border);
- color: var(--warning-text);
-}
-
-[data-rich-colors='true'][data-sonner-toast][data-type='warning'] [data-close-button] {
- background: var(--warning-bg);
- border-color: var(--warning-border);
- color: var(--warning-text);
-}
-
-[data-rich-colors='true'][data-sonner-toast][data-type='error'] {
- background: var(--error-bg);
- border-color: var(--error-border);
- color: var(--error-text);
-}
-
-[data-rich-colors='true'][data-sonner-toast][data-type='error'] [data-close-button] {
- background: var(--error-bg);
- border-color: var(--error-border);
- color: var(--error-text);
-}
-
-.sonner-loading-wrapper {
- --size: 16px;
- height: var(--size);
- width: var(--size);
- position: absolute;
- inset: 0;
- z-index: 10;
-}
-
-.sonner-loading-wrapper[data-visible='false'] {
- transform-origin: center;
- animation: sonner-fade-out 0.2s ease forwards;
-}
-
-.sonner-spinner {
- position: relative;
- top: 50%;
- left: 50%;
- height: var(--size);
- width: var(--size);
-}
-
-.sonner-loading-bar {
- animation: sonner-spin 1.2s linear infinite;
- background: var(--gray11);
- border-radius: 6px;
- height: 8%;
- left: -10%;
- position: absolute;
- top: -3.9%;
- width: 24%;
-}
-
-.sonner-loading-bar:nth-child(1) {
- animation-delay: -1.2s;
- transform: rotate(0.0001deg) translate(146%);
-}
-
-.sonner-loading-bar:nth-child(2) {
- animation-delay: -1.1s;
- transform: rotate(30deg) translate(146%);
-}
-
-.sonner-loading-bar:nth-child(3) {
- animation-delay: -1s;
- transform: rotate(60deg) translate(146%);
-}
-
-.sonner-loading-bar:nth-child(4) {
- animation-delay: -0.9s;
- transform: rotate(90deg) translate(146%);
-}
-
-.sonner-loading-bar:nth-child(5) {
- animation-delay: -0.8s;
- transform: rotate(120deg) translate(146%);
-}
-
-.sonner-loading-bar:nth-child(6) {
- animation-delay: -0.7s;
- transform: rotate(150deg) translate(146%);
-}
-
-.sonner-loading-bar:nth-child(7) {
- animation-delay: -0.6s;
- transform: rotate(180deg) translate(146%);
-}
-
-.sonner-loading-bar:nth-child(8) {
- animation-delay: -0.5s;
- transform: rotate(210deg) translate(146%);
-}
-
-.sonner-loading-bar:nth-child(9) {
- animation-delay: -0.4s;
- transform: rotate(240deg) translate(146%);
-}
-
-.sonner-loading-bar:nth-child(10) {
- animation-delay: -0.3s;
- transform: rotate(270deg) translate(146%);
-}
-
-.sonner-loading-bar:nth-child(11) {
- animation-delay: -0.2s;
- transform: rotate(300deg) translate(146%);
-}
-
-.sonner-loading-bar:nth-child(12) {
- animation-delay: -0.1s;
- transform: rotate(330deg) translate(146%);
-}
-
-@keyframes sonner-fade-in {
- 0% {
- opacity: 0;
- transform: scale(0.8);
- }
- 100% {
- opacity: 1;
- transform: scale(1);
- }
-}
-
-@keyframes sonner-fade-out {
- 0% {
- opacity: 1;
- transform: scale(1);
- }
- 100% {
- opacity: 0;
- transform: scale(0.8);
- }
-}
-
-@keyframes sonner-spin {
- 0% {
- opacity: 1;
- }
- 100% {
- opacity: 0.15;
- }
-}
-
-@media (prefers-reduced-motion) {
- [data-sonner-toast],
- [data-sonner-toast] > *,
- .sonner-loading-bar {
- transition: none !important;
- animation: none !important;
- }
-}
-
-.sonner-loader {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- transform-origin: center;
- transition: opacity 200ms, transform 200ms;
-}
-
-.sonner-loader[data-visible='false'] {
- opacity: 0;
- transform: scale(0.8) translate(-50%, -50%);
-} \ No newline at end of file
diff --git a/apps/web/app/tailwind.css b/apps/web/app/tailwind.css
deleted file mode 100644
index b6c362b0..00000000
--- a/apps/web/app/tailwind.css
+++ /dev/null
@@ -1,253 +0,0 @@
-@import "@radix-ui/colors/gray-dark";
-@import "@radix-ui/colors/gray";
-
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-@layer base {
- :root {
- --background: 0 0% 100%;
-
- --foreground: 20 14.3% 4.1%;
-
- --card: 0 0% 100%;
-
- --card-foreground: 20 14.3% 4.1%;
-
- --popover: 0 0% 100%;
-
- --popover-foreground: 20 14.3% 4.1%;
-
- --primary: 24 9.8% 10%;
-
- --primary-foreground: 60 9.1% 97.8%;
-
- --secondary: 60 4.8% 95.9%;
-
- --secondary-foreground: 24 9.8% 10%;
-
- --muted: 60 4.8% 95.9%;
-
- --muted-foreground: 25 5.3% 44.7%;
-
- --accent: 60 4.8% 95.9%;
-
- --accent-foreground: 24 9.8% 10%;
-
- --destructive: 0 84.2% 60.2%;
-
- --destructive-foreground: 60 9.1% 97.8%;
-
- --border: 20 5.9% 90%;
-
- --input: 20 5.9% 90%;
-
- --ring: 20 14.3% 4.1%;
-
- --chart-1: 12 76% 61%;
-
- --chart-2: 173 58% 39%;
-
- --chart-3: 197 37% 24%;
-
- --chart-4: 43 74% 66%;
-
- --chart-5: 27 87% 67%;
-
- --radius: 0.5rem;
- }
- .dark {
- --background: 0, 0%, 17%;
-
- --foreground: 60 9.1% 97.8%;
-
- --card: 0, 0%, 20%;
-
- --card-foreground: 60 9.1% 97.8%;
-
- --popover: 0, 0%, 25%;
-
- --popover-foreground: 60 9.1% 98.5%;
-
- --primary: 60 9.1% 97.8%;
-
- --primary-foreground: 24 9.8% 10%;
-
- --secondary: 12 6.5% 15.1%;
-
- --secondary-foreground: 60 9.1% 97.8%;
-
- --muted: 12 6.5% 15.1%;
-
- --muted-foreground: 24 5.4% 63.9%;
-
- --accent: 12 6.5% 15.1%;
-
- --accent-foreground: 60 9.1% 97.8%;
-
- --destructive: 0 62.8% 30.6%;
-
- --destructive-foreground: 60 9.1% 97.8%;
-
- --border: 20 5.9% 25%;
-
- --input: 12 6.5% 15.1%;
-
- --ring: 24 5.7% 82.9%;
-
- --chart-1: 220 70% 50%;
-
- --chart-2: 160 60% 45%;
-
- --chart-3: 30 80% 55%;
-
- --chart-4: 280 65% 60%;
-
- --chart-5: 340 75% 55%;
- }
- [data-registry="plate"] {
- --background: 0 0% 100%;
- --foreground: 240 10% 3.9%;
- --card: 0 0% 100%;
- --card-foreground: 240 10% 3.9%;
- --popover: 0 0% 100%;
- --popover-foreground: 240 10% 3.9%;
- --primary: 240 5.9% 10%;
- --primary-foreground: 0 0% 98%;
- --secondary: 240 4.8% 95.9%;
- --secondary-foreground: 240 5.9% 10%;
- --muted: 240 4.8% 95.9%;
- --muted-foreground: 240 3.8% 46.1%;
- --accent: 240 4.8% 95.9%;
- --accent-foreground: 240 5.9% 10%;
- --destructive: 0 84.2% 60.2%;
- --destructive-foreground: 0 0% 98%;
- --border: 240 5.9% 90%;
- --input: 240 5.9% 90%;
- --ring: 240 10% 3.9%;
- --chart-1: 12 76% 61%;
- --chart-2: 173 58% 39%;
- --chart-3: 197 37% 24%;
- --chart-4: 43 74% 66%;
- --chart-5: 27 87% 67%;
- --radius: 0.5rem;
- --brand: 217.2 91.2% 59.8%;
- --highlight: 47.9 95.8% 53.1%;
- }
- [data-registry="plate"].dark {
- --background: 240 10% 3.9%;
- --foreground: 0 0% 98%;
- --card: 240 10% 3.9%;
- --card-foreground: 0 0% 98%;
- --popover: 240 10% 3.9%;
- --popover-foreground: 0 0% 98%;
- --primary: 0 0% 98%;
- --primary-foreground: 240 5.9% 10%;
- --secondary: 240 3.7% 15.9%;
- --secondary-foreground: 0 0% 98%;
- --muted: 240 3.7% 15.9%;
- --muted-foreground: 240 5% 64.9%;
- --accent: 240 3.7% 15.9%;
- --accent-foreground: 0 0% 98%;
- --destructive: 0 62.8% 30.6%;
- --destructive-foreground: 0 0% 98%;
- --border: 240 3.7% 15.9%;
- --input: 240 3.7% 15.9%;
- --ring: 240 4.9% 83.9%;
- --chart-1: 220 70% 50%;
- --chart-2: 160 60% 45%;
- --chart-3: 30 80% 55%;
- --chart-4: 280 65% 60%;
- --chart-5: 340 75% 55%;
- --brand: 213.3 93.9% 67.8%;
- --highlight: 48 96% 53%;
- }
-}
-
-@layer base {
- * {
- @apply border-border;
- &:has(html[data-theme]) {
- transition-property: color, background-color, border-color;
- transition-duration: 200ms;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- }
- }
- body {
- @apply bg-background text-foreground;
- }
-}
-
-@layer components {
- .fade-overlay {
- @apply absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-card to-transparent;
- }
-}
-
-/* Base state - scrollbar always present but invisible */
-.md\:overflow-y-scroll::-webkit-scrollbar {
- width: 4px; /* Thin scrollbar */
- background-color: transparent;
- opacity: 0;
- transition: opacity 0.2s ease;
-}
-
-.md\:overflow-y-scroll::-webkit-scrollbar-track {
- background-color: rgba(240, 240, 240, 0.1); /* Very subtle light track in light mode */
-}
-
-.md\:overflow-y-scroll::-webkit-scrollbar-thumb {
- background-color: rgba(155, 155, 155, 0.5); /* Light gray thumb */
- border-radius: 10px;
- opacity: 0;
- transition: opacity 0.2s ease;
-}
-
-/* Show scrollbar on hover */
-.md\:overflow-y-scroll:hover::-webkit-scrollbar,
-.md\:overflow-y-scroll:hover::-webkit-scrollbar-thumb {
- opacity: 1;
-}
-
-/* Dark mode adjustments */
-@media (prefers-color-scheme: dark) {
- .md\:overflow-y-scroll::-webkit-scrollbar-track {
- background-color: rgba(50, 50, 50, 0.1); /* Subtle dark track in dark mode */
- }
-
- .md\:overflow-y-scroll::-webkit-scrollbar-thumb {
- background-color: rgba(180, 180, 180, 0.5); /* Slightly lighter thumb for dark mode */
- }
-}
-
-/* For Firefox */
-.md\:overflow-y-scroll {
- scrollbar-width: thin;
- scrollbar-color: rgba(155, 155, 155, 0) transparent; /* Start with opacity 0 */
- transition: scrollbar-color 0.2s ease;
-}
-
-.md\:overflow-y-scroll:hover {
- scrollbar-color: rgba(155, 155, 155, 0.5) rgba(240, 240, 240, 0.1); /* Light mode colors */
-}
-
-/* Dark mode for Firefox */
-@media (prefers-color-scheme: dark) {
- .md\:overflow-y-scroll:hover {
- scrollbar-color: rgba(180, 180, 180, 0.5) rgba(50, 50, 50, 0.1); /* Dark mode colors */
- }
-}
-
-/* For your specific dark mode class */
-.dark .md\:overflow-y-scroll::-webkit-scrollbar-track {
- background-color: rgba(50, 50, 50, 0.1);
-}
-
-.dark .md\:overflow-y-scroll::-webkit-scrollbar-thumb {
- background-color: rgba(180, 180, 180, 0.5);
-}
-
-.dark .md\:overflow-y-scroll:hover {
- scrollbar-color: rgba(180, 180, 180, 0.5) rgba(50, 50, 50, 0.1);
-}
diff --git a/apps/web/app/types/css.d.ts b/apps/web/app/types/css.d.ts
deleted file mode 100644
index 57039670..00000000
--- a/apps/web/app/types/css.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-declare module "*.module.css" {
- const classes: { [key: string]: string };
- export default classes;
-}
-
-declare module "*.css" {
- const content: string;
- export default content;
-}
diff --git a/apps/web/app/types/stripe.d.ts b/apps/web/app/types/stripe.d.ts
deleted file mode 100644
index 6450ec1b..00000000
--- a/apps/web/app/types/stripe.d.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import * as React from 'react';
-
-// If using TypeScript, add the following snippet to your file as well.
-declare global {
- namespace JSX {
- interface IntrinsicElements {
- 'stripe-pricing-table': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
- }
- }
-} \ No newline at end of file
diff --git a/apps/web/app/waitlist/page.tsx b/apps/web/app/waitlist/page.tsx
new file mode 100644
index 00000000..b493c813
--- /dev/null
+++ b/apps/web/app/waitlist/page.tsx
@@ -0,0 +1,218 @@
+"use client"
+
+import { $fetch } from "@lib/api"
+import { authClient } from "@lib/auth"
+import { useAuth } from "@lib/auth-context"
+import { fetchConsumerProProduct } from "@lib/queries"
+import { Button } from "@ui/components/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@ui/components/card"
+import { useCustomer } from "autumn-js/react"
+import { Clock, LoaderIcon, SkipForwardIcon, LogOut } from "lucide-react"
+import Link from "next/link"
+import { useRouter, useSearchParams } from "next/navigation"
+import { useEffect, useState } from "react"
+import { toast } from "sonner"
+
+export default function WaitlistPage() {
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const referralCode = searchParams.get('ref')
+ const { user } = useAuth()
+ const [isChecking, setIsChecking] = useState(true)
+ const [isSkippingWaitlist, setIsSkippingWaitlist] = useState(false)
+ const [waitlistStatus, setWaitlistStatus] = useState<{
+ inWaitlist: boolean
+ accessGranted: boolean
+ createdAt: string
+ } | null>(null)
+ const autumn = useCustomer()
+
+ // @ts-ignore
+ const { data: earlyAccess } = fetchConsumerProProduct(autumn)
+
+ const handleSkipWaitlist = async () => {
+ setIsSkippingWaitlist(true)
+ try {
+ const res = await autumn.attach({
+ productId: "consumer_pro",
+ forceCheckout: true,
+ successUrl: "https://app.supermemory.ai/",
+ })
+ if (res.data && "checkout_url" in res.data && res.data.checkout_url) {
+ router.push(res.data.checkout_url)
+ }
+ } catch (error) {
+ console.error("Error skipping waitlist:", error)
+ } finally {
+ setIsSkippingWaitlist(false)
+ }
+ }
+
+ const handleLogout = async () => {
+ try {
+ await authClient.signOut()
+ router.push("/")
+ } catch (error) {
+ console.error("Error signing out:", error)
+ toast.error("Failed to sign out")
+ }
+ }
+
+ useEffect(() => {
+ async function checkAccess() {
+ if (!user) {
+ router.push("/")
+ return
+ }
+
+ // Anonymous users should sign in first
+ if (user.isAnonymous) {
+ authClient.signOut()
+ router.push("/")
+ return
+ }
+
+ try {
+ // Check waitlist status using the new endpoint
+ const response = await $fetch("@get/waitlist/status")
+
+ if (response.data) {
+ setWaitlistStatus(response.data)
+
+ if (!response.data.inWaitlist) {
+ authClient.signOut()
+ router.push("/login")
+ }
+
+ // If user has access, redirect to home
+ if (response.data.accessGranted) {
+ router.push("/")
+ }
+ }
+ } catch (error) {
+ console.error("Error checking waitlist status:", error)
+ // If there's an error, assume user is on waitlist
+ setWaitlistStatus({
+ inWaitlist: true,
+ accessGranted: false,
+ createdAt: new Date().toISOString(),
+ })
+ } finally {
+ setIsChecking(false)
+ }
+ }
+
+ checkAccess()
+ }, [user, router])
+
+ if (isChecking) {
+ return (
+ <div className="min-h-screen flex items-center justify-center p-4 bg-[#0f1419]">
+ <div className="flex flex-col items-center gap-4">
+ <LoaderIcon className="w-8 h-8 text-orange-500 animate-spin" />
+ <p className="text-white/60">Checking access...</p>
+ </div>
+ </div>
+ )
+ }
+
+ return (
+ <div className="min-h-screen flex items-center justify-center p-4 bg-[#0f1419]">
+ <Card className="max-w-md w-full bg-[#1a1f2a] border-white/10">
+ <CardHeader className="text-center">
+ <div className="mx-auto mb-4 w-16 h-16 rounded-full bg-orange-500/10 flex items-center justify-center">
+ <Clock className="w-8 h-8 text-orange-500" />
+ </div>
+ <CardTitle className="text-2xl font-bold text-white">
+ You're on the waitlist!
+ </CardTitle>
+ <CardDescription className="text-white/60 mt-2">
+ {referralCode
+ ? "Thanks for joining through a friend's invitation! You've been added to the waitlist with priority access."
+ : "Thanks for your interest in supermemory. We'll notify you as soon as we're ready for you."
+ }
+ </CardDescription>
+ {referralCode && (
+ <div className="mt-3 px-3 py-2 bg-orange-500/10 rounded-lg border border-orange-500/20">
+ <p className="text-orange-400 text-sm font-medium">
+ 🎉 Referred by a friend! You'll get priority access.
+ </p>
+ </div>
+ )}
+ </CardHeader>
+ <CardContent>
+ <div className="flex flex-col gap-4">
+ {!earlyAccess?.allowed && (
+ <Button
+ disabled={isSkippingWaitlist}
+ onClick={handleSkipWaitlist}
+ >
+ {isSkippingWaitlist ? (
+ <LoaderIcon className="w-4 h-4 animate-spin" />
+ ) : (
+ <SkipForwardIcon className="w-4 h-4" />
+ )}
+ {isSkippingWaitlist
+ ? "Processing..."
+ : "Skip the waitlist for $15"}
+ </Button>
+ )}
+ <div className="pt-4 border-t border-white/10">
+ <p className="text-white/60 text-sm">
+ We're working hard to bring you the best experience. In the
+ meantime, you can:
+ </p>
+ <ul className="mt-3 space-y-2 text-sm">
+ <li className="flex items-center gap-2 text-white/80">
+ <span className="text-orange-500">•</span>
+ <a
+ className="hover:text-white transition-colors underline"
+ href="https://x.com/supermemoryai"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ Follow our X for updates
+ </a>
+ </li>
+ <li className="flex items-center gap-2 text-white/80">
+ <span className="text-orange-500">•</span>
+ <a
+ className="hover:text-white transition-colors underline"
+ href="https://supermemory.link/discord"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ Join our community Discord
+ </a>
+ </li>
+ </ul>
+ </div>
+
+ {user && (
+ <div className="pt-4 text-center space-y-3">
+ <p className="text-white/50 text-xs">
+ Signed in as {user.email}
+ </p>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleLogout}
+ className="border-white/20 hover:bg-white/5"
+ >
+ <LogOut className="w-4 h-4 mr-2" />
+ Sign out
+ </Button>
+ </div>
+ )}
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ )
+}
diff --git a/apps/web/assets/logo.svg b/apps/web/assets/logo.svg
deleted file mode 100644
index 5ca15922..00000000
--- a/apps/web/assets/logo.svg
+++ /dev/null
@@ -1,11 +0,0 @@
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="42"
- height="42"
- viewBox="0 0 42 42"
- >
- <path
- className="fill-gray-700 dark:fill-gray-100"
- d="M19.0357 8C20.5531 8 21 9.27461 21 10.8438V16.3281H23.5536V14.2212C23.5536 13.1976 23.9468 12.216 24.6467 11.4922L25.0529 11.0721C24.9729 10.8772 24.9286 10.6627 24.9286 10.4375C24.9286 9.54004 25.6321 8.8125 26.5 8.8125C27.3679 8.8125 28.0714 9.54004 28.0714 10.4375C28.0714 11.335 27.3679 12.0625 26.5 12.0625C26.2822 12.0625 26.0748 12.0167 25.8863 11.9339L25.4801 12.354C25.0012 12.8492 24.7321 13.5209 24.7321 14.2212V16.3281H28.9714C29.2045 15.7326 29.7691 15.3125 30.4286 15.3125C31.2964 15.3125 32 16.04 32 16.9375C32 17.835 31.2964 18.5625 30.4286 18.5625C29.7691 18.5625 29.2045 18.1424 28.9714 17.5469H21V21.2031H25.0428C25.2759 20.6076 25.8405 20.1875 26.5 20.1875C27.3679 20.1875 28.0714 20.915 28.0714 21.8125C28.0714 22.71 27.3679 23.4375 26.5 23.4375C25.8405 23.4375 25.2759 23.0174 25.0428 22.4219H21V26.0781H24.4125C25.4023 26.0781 26.3516 26.4847 27.0515 27.2085L29.0292 29.2536C29.2177 29.1708 29.4251 29.125 29.6429 29.125C30.5107 29.125 31.2143 29.8525 31.2143 30.75C31.2143 31.6475 30.5107 32.375 29.6429 32.375C28.775 32.375 28.0714 31.6475 28.0714 30.75C28.0714 30.5248 28.1157 30.3103 28.1958 30.1154L26.2181 28.0703C25.7392 27.5751 25.0897 27.2969 24.4125 27.2969H21V31.1562C21 32.7254 20.5531 34 19.0357 34C17.6165 34 16.4478 32.8879 16.3004 31.4559C16.0451 31.527 15.775 31.5625 15.5 31.5625C13.7665 31.5625 12.3571 30.1051 12.3571 28.3125C12.3571 27.9367 12.421 27.5711 12.5339 27.2359C11.0509 26.657 10 25.1742 10 23.4375C10 21.8176 10.9183 20.416 12.2491 19.766C11.8219 19.2125 11.5714 18.5117 11.5714 17.75C11.5714 16.191 12.6321 14.891 14.0464 14.5711C13.9679 14.2918 13.9286 13.9922 13.9286 13.6875C13.9286 12.1691 14.9402 10.8895 16.3004 10.534C16.4478 9.11211 17.6165 8 19.0357 8Z"
- />
- </svg> \ No newline at end of file
diff --git a/apps/web/biome.json b/apps/web/biome.json
new file mode 100644
index 00000000..79f38fb5
--- /dev/null
+++ b/apps/web/biome.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
+ "extends": "//",
+ "linter": {
+ "domains": {
+ "next": "recommended",
+ "react": "recommended"
+ }
+ }
+}
diff --git a/apps/web/button.tsx b/apps/web/button.tsx
new file mode 100644
index 00000000..99d10e10
--- /dev/null
+++ b/apps/web/button.tsx
@@ -0,0 +1,58 @@
+import { cn } from "@lib/utils"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+import type * as React from "react"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps<typeof buttonVariants> & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ <Comp
+ className={cn(buttonVariants({ variant, size, className }))}
+ data-slot="button"
+ {...props}
+ />
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/apps/web/components.json b/apps/web/components.json
index e7346c66..d42a2c76 100644
--- a/apps/web/components.json
+++ b/apps/web/components.json
@@ -1,20 +1,20 @@
{
- "$schema": "https://ui.shadcn.com/schema.json",
- "aliases": {
- "components": "~/components",
- "hooks": "~/hooks",
- "lib": "~/lib",
- "ui": "~/components/ui",
- "utils": "~/lib/utils"
- },
- "rsc": false,
- "style": "new-york",
- "tailwind": {
- "baseColor": "stone",
- "config": "tailwind.config.ts",
- "css": "app/tailwind.css",
- "cssVariables": true,
- "prefix": ""
- },
- "tsx": true
-} \ No newline at end of file
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "../../packages/ui/globals.css",
+ "baseColor": "zinc",
+ "cssVariables": true
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@ui/components",
+ "hooks": "@/hooks",
+ "lib": "@/lib",
+ "utils": "@lib/utils",
+ "ui": "@ui/components"
+ }
+}
diff --git a/apps/web/components/connect-ai-modal.tsx b/apps/web/components/connect-ai-modal.tsx
new file mode 100644
index 00000000..ca5240cd
--- /dev/null
+++ b/apps/web/components/connect-ai-modal.tsx
@@ -0,0 +1,219 @@
+"use client"
+
+import { useIsMobile } from "@hooks/use-mobile"
+import { Button } from "@ui/components/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@ui/components/dialog"
+import { Input } from "@ui/components/input"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@ui/components/select"
+import { CopyableCell } from "@ui/copyable-cell"
+import { CopyIcon, ExternalLink } from "lucide-react"
+import Image from "next/image"
+import { useState } from "react"
+import { toast } from "sonner"
+
+const clients = {
+ cursor: "Cursor",
+ claude: "Claude Desktop",
+ vscode: "VSCode",
+ cline: "Cline",
+ "gemini-cli": "Gemini CLI",
+ "claude-code": "Claude Code",
+ "roo-cline": "Roo Cline",
+ witsy: "Witsy",
+ enconvo: "Enconvo",
+} as const
+
+interface ConnectAIModalProps {
+ children: React.ReactNode
+}
+
+export function ConnectAIModal({ children }: ConnectAIModalProps) {
+ const [client, setClient] = useState<keyof typeof clients>("cursor")
+ const [isOpen, setIsOpen] = useState(false)
+ const [showAllTools, setShowAllTools] = useState(false)
+ const isMobile = useIsMobile()
+ const installCommand = `npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${client} --oauth=yes`
+
+ const copyToClipboard = () => {
+ navigator.clipboard.writeText(installCommand)
+ toast.success("Copied to clipboard!")
+ }
+
+ return (
+ <Dialog open={isOpen} onOpenChange={setIsOpen}>
+ <DialogTrigger asChild>{children}</DialogTrigger>
+ <DialogContent className="sm:max-w-4xl">
+ <DialogHeader>
+ <DialogTitle>Connect Supermemory to Your AI</DialogTitle>
+ <DialogDescription>
+ Connect supermemory to your favorite AI tools using the Model Context Protocol (MCP).
+ This allows your AI assistant to create, search, and access your memories directly.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="mb-6 block md:hidden">
+ <label
+ className="text-sm font-medium text-white/80 block mb-2"
+ htmlFor="mcp-server-url"
+ >
+ MCP Server URL
+ </label>
+ <div className="p-3 bg-white/5 rounded border border-white/10">
+ <CopyableCell
+ className="font-mono text-sm text-blue-400"
+ value="https://api.supermemory.ai/mcp"
+ />
+ </div>
+ <p className="text-xs text-white/50 mt-2">
+ Click URL to copy to clipboard. Use this URL to configure supermemory in your AI assistant.
+ </p>
+ </div>
+
+ <div className="space-y-6">
+ <div className="hidden md:block">
+ <h3 className="text-sm font-medium mb-3">Supported AI Tools</h3>
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-3 text-sm">
+ {Object.entries(clients)
+ .slice(0, showAllTools ? undefined : isMobile ? 4 : 6)
+ .map(([key, clientName]) => (
+ <div
+ key={clientName}
+ className="flex items-center gap-3 px-3 py-3 bg-muted rounded-md"
+ >
+ <div className="w-8 h-8 relative flex-shrink-0 flex items-center justify-center">
+ <Image
+ src={`/mcp-supported-tools/${key == "claude-code" ? "claude" : key}.png`}
+ alt={clientName}
+ width={isMobile ? 20 : 32}
+ height={isMobile ? 20 : 32}
+ className={"rounded object-contain"}
+ onError={(e) => {
+ const target = e.target as HTMLImageElement;
+ target.style.display = 'none';
+ const parent = target.parentElement;
+ if (parent && !parent.querySelector('.fallback-text')) {
+ const fallback = document.createElement('span');
+ fallback.className = 'fallback-text text-xs font-bold text-muted-foreground';
+ fallback.textContent = clientName.substring(0, 2).toUpperCase();
+ parent.appendChild(fallback);
+ }
+ }}
+ />
+ </div>
+ <span className="text-sm font-medium">{clientName}</span>
+ </div>
+ ))}
+ </div>
+ {Object.entries(clients).length > 6 && (
+ <div className="mt-3 text-center">
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => setShowAllTools(!showAllTools)}
+ className="text-xs text-muted-foreground hover:text-foreground"
+ >
+ {showAllTools ? "Show Less" : `Show All`}
+ </Button>
+ </div>
+ )}
+ </div>
+
+ <div className="hidden md:block">
+ <h3 className="text-sm font-medium mb-3">Quick Installation</h3>
+ <div className="space-y-3 flex gap-2 items-center justify-between">
+ <Select
+ value={client}
+ onValueChange={(value) => setClient(value as keyof typeof clients)}
+ >
+ <SelectTrigger className="w-48 mb-0">
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ {Object.entries(clients).map(([key, value]) => (
+ <SelectItem key={key} value={key}>
+ <div className="flex items-center gap-2">
+ <div className="w-4 h-4 relative flex-shrink-0 flex items-center justify-center">
+ <Image
+ src={`/mcp-supported-tools/${key == "claude-code" ? "claude" : key}.png`}
+ alt={value}
+ width={16}
+ height={16}
+ className="rounded object-contain"
+ onError={(e) => {
+ const target = e.target as HTMLImageElement;
+ target.style.display = 'none';
+ const parent = target.parentElement;
+ if (parent && !parent.querySelector('.fallback-text')) {
+ const fallback = document.createElement('span');
+ fallback.className = 'fallback-text text-xs font-bold text-muted-foreground';
+ fallback.textContent = value.substring(0, 1).toUpperCase();
+ parent.appendChild(fallback);
+ }
+ }}
+ />
+ </div>
+ {value}
+ </div>
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+
+ <div className="relative w-full flex items-center">
+ <Input
+ className="font-mono text-xs w-full pr-10"
+ readOnly
+ value={installCommand}
+ />
+
+ <Button
+ onClick={copyToClipboard}
+ className="absolute right-0 cursor-pointer"
+ variant="ghost"
+ >
+ <CopyIcon className="size-4" />
+ </Button>
+ </div>
+ </div>
+ </div>
+
+ <div>
+ <h3 className="text-sm font-medium mb-3">What You Can Do</h3>
+ <ul className="space-y-2 text-sm text-muted-foreground">
+ <li>• Ask your AI to save important information as memories</li>
+ <li>• Search through your saved memories during conversations</li>
+ <li>• Get contextual information from your knowledge base</li>
+ <li>• Seamlessly integrate with your existing AI workflow</li>
+ </ul>
+ </div>
+
+ <div className="flex justify-between items-center pt-4 border-t">
+ <Button
+ variant="outline"
+ onClick={() => window.open("https://docs.supermemory.ai/supermemory-mcp/introduction", "_blank")}
+ >
+ <ExternalLink className="w-4 h-4 mr-2" />
+ Learn More
+ </Button>
+ <Button onClick={() => setIsOpen(false)}>
+ Done
+ </Button>
+ </div>
+ </div>
+ </DialogContent>
+ </Dialog>
+ )
+}
diff --git a/apps/web/components/create-project-dialog.tsx b/apps/web/components/create-project-dialog.tsx
new file mode 100644
index 00000000..904c3f2d
--- /dev/null
+++ b/apps/web/components/create-project-dialog.tsx
@@ -0,0 +1,119 @@
+"use client"
+
+import { Button } from "@repo/ui/components/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@repo/ui/components/dialog"
+import { Input } from "@repo/ui/components/input"
+import { Label } from "@repo/ui/components/label"
+import { Loader2 } from "lucide-react"
+import { motion, AnimatePresence } from "motion/react"
+import { useState } from "react"
+import { useProjectMutations } from "@/hooks/use-project-mutations"
+
+interface CreateProjectDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+}
+
+export function CreateProjectDialog({ open, onOpenChange }: CreateProjectDialogProps) {
+ const [projectName, setProjectName] = useState("")
+ const { createProjectMutation } = useProjectMutations()
+
+ const handleClose = () => {
+ onOpenChange(false)
+ setProjectName("")
+ }
+
+ const handleCreate = () => {
+ if (projectName.trim()) {
+ createProjectMutation.mutate(projectName, {
+ onSuccess: () => {
+ handleClose()
+ }
+ })
+ }
+ }
+
+ return (
+ <AnimatePresence>
+ {open && (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="sm:max-w-2xl bg-[#0f1419] backdrop-blur-xl border-white/10 text-white">
+ <motion.div
+ initial={{ opacity: 0, scale: 0.95 }}
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.95 }}
+ >
+ <DialogHeader>
+ <DialogTitle>Create New Project</DialogTitle>
+ <DialogDescription className="text-white/60">
+ Give your project a unique name
+ </DialogDescription>
+ </DialogHeader>
+ <div className="grid gap-4 py-4">
+ <motion.div
+ initial={{ opacity: 0, y: 10 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.1 }}
+ className="flex flex-col gap-2"
+ >
+ <Label htmlFor="projectName">Project Name</Label>
+ <Input
+ id="projectName"
+ className="bg-white/5 border-white/10 text-white"
+ placeholder="My Awesome Project"
+ value={projectName}
+ onChange={(e) => setProjectName(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && projectName.trim()) {
+ handleCreate()
+ }
+ }}
+ />
+ <p className="text-xs text-white/50">
+ This will help you organize your memories
+ </p>
+ </motion.div>
+ </div>
+ <DialogFooter>
+ <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
+ <Button
+ type="button"
+ variant="outline"
+ className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
+ onClick={handleClose}
+ >
+ Cancel
+ </Button>
+ </motion.div>
+ <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
+ <Button
+ type="button"
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ disabled={createProjectMutation.isPending || !projectName.trim()}
+ onClick={handleCreate}
+ >
+ {createProjectMutation.isPending ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ Creating...
+ </>
+ ) : (
+ "Create Project"
+ )}
+ </Button>
+ </motion.div>
+ </DialogFooter>
+ </motion.div>
+ </DialogContent>
+ </Dialog>
+ )}
+ </AnimatePresence>
+ )
+}
diff --git a/apps/web/components/glass-menu-effect.tsx b/apps/web/components/glass-menu-effect.tsx
new file mode 100644
index 00000000..9d4d4b68
--- /dev/null
+++ b/apps/web/components/glass-menu-effect.tsx
@@ -0,0 +1,37 @@
+import { motion } from "motion/react"
+
+interface GlassMenuEffectProps {
+ rounded?: string
+ className?: string
+}
+
+export function GlassMenuEffect({
+ rounded = "rounded-[28px]",
+ className = "",
+}: GlassMenuEffectProps) {
+ return (
+ <motion.div
+ className={`absolute inset-0 ${className}`}
+ layout
+ style={{
+ transform: "translateZ(0)",
+ willChange: "auto",
+ }}
+ transition={{
+ layout: {
+ type: "spring",
+ damping: 35,
+ stiffness: 180,
+ },
+ }}
+ >
+ <div
+ className={`absolute inset-0 backdrop-blur-md bg-white/5 border border-white/10 ${rounded}`}
+ style={{
+ transform: "translateZ(0)",
+ willChange: "transform",
+ }}
+ />
+ </motion.div>
+ )
+}
diff --git a/apps/web/components/install-prompt.tsx b/apps/web/components/install-prompt.tsx
new file mode 100644
index 00000000..cde987c4
--- /dev/null
+++ b/apps/web/components/install-prompt.tsx
@@ -0,0 +1,118 @@
+import { useEffect, useState } from "react"
+import { motion, AnimatePresence } from "motion/react"
+import { X, Download, Share } from "lucide-react"
+import { Button } from "@repo/ui/components/button"
+
+export function InstallPrompt() {
+ const [isIOS, setIsIOS] = useState(false)
+ const [showPrompt, setShowPrompt] = useState(false)
+ const [deferredPrompt, setDeferredPrompt] = useState<any>(null)
+
+ useEffect(() => {
+ const isIOSDevice = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
+ const isInStandaloneMode = window.matchMedia('(display-mode: standalone)').matches
+ const hasSeenPrompt = localStorage.getItem('install-prompt-dismissed') === 'true'
+
+ setIsIOS(isIOSDevice)
+
+ const isDevelopment = process.env.NODE_ENV === 'development'
+ setShowPrompt(!hasSeenPrompt && (isDevelopment || (!isInStandaloneMode && (isIOSDevice || 'serviceWorker' in navigator))))
+
+ const handleBeforeInstallPrompt = (e: Event) => {
+ e.preventDefault()
+ setDeferredPrompt(e)
+ if (!hasSeenPrompt) {
+ setShowPrompt(true)
+ }
+ }
+
+ window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
+
+ return () => {
+ window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
+ }
+ }, [])
+
+ const handleInstall = async () => {
+ if (deferredPrompt) {
+ deferredPrompt.prompt()
+ const { outcome } = await deferredPrompt.userChoice
+ if (outcome === 'accepted') {
+ localStorage.setItem('install-prompt-dismissed', 'true')
+ setShowPrompt(false)
+ }
+ setDeferredPrompt(null)
+ }
+ }
+
+ const handleDismiss = () => {
+ localStorage.setItem('install-prompt-dismissed', 'true')
+ setShowPrompt(false)
+ }
+
+ if (!showPrompt) {
+ return null
+ }
+
+ return (
+ <AnimatePresence>
+ <motion.div
+ animate={{ y: 0, opacity: 1 }}
+ exit={{ y: 100, opacity: 0 }}
+ initial={{ y: 100, opacity: 0 }}
+ className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-sm md:hidden"
+ >
+ <div className="bg-black/90 backdrop-blur-md text-white rounded-2xl p-4 shadow-2xl border border-white/10">
+ <div className="flex items-start justify-between mb-3">
+ <div className="flex items-center gap-2">
+ <div className="w-8 h-8 bg-[#0f1419] rounded-lg flex items-center justify-center">
+ <Download className="w-4 h-4" />
+ </div>
+ <h3 className="font-semibold text-sm">Install Supermemory</h3>
+ </div>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={handleDismiss}
+ className="text-white/60 hover:text-white h-6 w-6 p-0"
+ >
+ <X className="w-4 h-4" />
+ </Button>
+ </div>
+
+ <p className="text-white/80 text-xs mb-4 leading-relaxed">
+ Add Supermemory to your home screen for quick access and a better experience.
+ </p>
+
+ {isIOS ? (
+ <div className="space-y-3">
+ <p className="text-white/70 text-xs flex items-center gap-1">
+ 1. Tap the <Share className="w-3 h-3 inline" /> Share button in Safari
+ </p>
+ <p className="text-white/70 text-xs">
+ 2. Select "Add to Home Screen" ➕
+ </p>
+ <Button
+ variant="secondary"
+ size="sm"
+ onClick={handleDismiss}
+ className="w-full text-xs"
+ >
+ Got it
+ </Button>
+ </div>
+ ) : (
+ <Button
+ onClick={handleInstall}
+ size="sm"
+ className="w-full bg-[#0f1419] hover:bg-[#1a1f2a] text-white text-xs"
+ >
+ <Download className="w-3 h-3 mr-1" />
+ Add to Home Screen
+ </Button>
+ )}
+ </div>
+ </motion.div>
+ </AnimatePresence>
+ )
+}
diff --git a/apps/web/components/memory-list-view.tsx b/apps/web/components/memory-list-view.tsx
new file mode 100644
index 00000000..654a3ad1
--- /dev/null
+++ b/apps/web/components/memory-list-view.tsx
@@ -0,0 +1,802 @@
+"use client"
+
+import {
+ GoogleDocs,
+ GoogleDrive,
+ GoogleSheets,
+ GoogleSlides,
+ MicrosoftExcel,
+ MicrosoftOneNote,
+ MicrosoftPowerpoint,
+ MicrosoftWord,
+ NotionDoc,
+ OneDrive,
+ PDF,
+} from "@repo/ui/assets/icons"
+import { Badge } from "@repo/ui/components/badge"
+import { Card, CardContent, CardHeader } from "@repo/ui/components/card"
+
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+} from "@repo/ui/components/sheet"
+import {
+ Drawer,
+ DrawerContent,
+ DrawerHeader,
+ DrawerTitle,
+} from "@repo/ui/components/drawer"
+import { colors } from "@repo/ui/memory-graph/constants"
+import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
+import { Label1Regular } from "@ui/text/label/label-1-regular"
+import { Brain, Calendar, ExternalLink, FileText, Sparkles } from "lucide-react"
+import { useVirtualizer } from "@tanstack/react-virtual"
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
+import type { z } from "zod"
+import { analytics } from "@/lib/analytics"
+import useResizeObserver from "@/hooks/use-resize-observer"
+import { useIsMobile } from "@hooks/use-mobile"
+import { cn } from "@lib/utils"
+
+type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
+type DocumentWithMemories = DocumentsResponse["documents"][0]
+type MemoryEntry = DocumentWithMemories["memoryEntries"][0]
+
+interface MemoryListViewProps {
+ children?: React.ReactNode
+ documents: DocumentWithMemories[]
+ isLoading: boolean
+ isLoadingMore: boolean
+ error: Error | null
+ totalLoaded: number
+ hasMore: boolean
+ loadMoreDocuments: () => Promise<void>
+}
+
+const GreetingMessage = memo(() => {
+ const getGreeting = () => {
+ const hour = new Date().getHours()
+ if (hour < 12) return "Good morning"
+ if (hour < 17) return "Good afternoon"
+ return "Good evening"
+ }
+
+ return (
+ <div className="flex items-center gap-3 mb-3 px-4 md:mb-6 md:mt-3">
+ <div>
+ <h1
+ className="text-lg md:text-xl font-semibold"
+ style={{ color: colors.text.primary }}
+ >
+ {getGreeting()}!
+ </h1>
+ <p
+ className="text-xs md:text-sm"
+ style={{ color: colors.text.muted }}
+ >
+ Welcome back to your memory collection
+ </p>
+ </div>
+ </div>
+ )
+})
+
+const formatDate = (date: string | Date) => {
+ const dateObj = new Date(date)
+ const now = new Date()
+ const currentYear = now.getFullYear()
+ const dateYear = dateObj.getFullYear()
+
+ const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+ const month = monthNames[dateObj.getMonth()]
+ const day = dateObj.getDate()
+
+ const getOrdinalSuffix = (n: number) => {
+ const s = ["th", "st", "nd", "rd"]
+ const v = n % 100
+ return n + (s[(v - 20) % 10] || s[v] || s[0]!)
+ }
+
+ const formattedDay = getOrdinalSuffix(day)
+
+ if (dateYear !== currentYear) {
+ return `${month} ${formattedDay}, ${dateYear}`
+ }
+
+ return `${month} ${formattedDay}`
+}
+
+const formatDocumentType = (type: string) => {
+ // Special case for PDF
+ if (type.toLowerCase() === "pdf") return "PDF"
+
+ // Replace underscores with spaces and capitalize each word
+ return type
+ .split("_")
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
+ .join(" ")
+}
+
+const getDocumentIcon = (type: string, className: string) => {
+ const iconProps = {
+ className,
+ style: { color: colors.text.muted },
+ }
+
+ switch (type) {
+ case "google_doc":
+ return <GoogleDocs {...iconProps} />
+ case "google_sheet":
+ return <GoogleSheets {...iconProps} />
+ case "google_slide":
+ return <GoogleSlides {...iconProps} />
+ case "google_drive":
+ return <GoogleDrive {...iconProps} />
+ case "notion":
+ case "notion_doc":
+ return <NotionDoc {...iconProps} />
+ case "word":
+ case "microsoft_word":
+ return <MicrosoftWord {...iconProps} />
+ case "excel":
+ case "microsoft_excel":
+ return <MicrosoftExcel {...iconProps} />
+ case "powerpoint":
+ case "microsoft_powerpoint":
+ return <MicrosoftPowerpoint {...iconProps} />
+ case "onenote":
+ case "microsoft_onenote":
+ return <MicrosoftOneNote {...iconProps} />
+ case "onedrive":
+ return <OneDrive {...iconProps} />
+ case "pdf":
+ return <PDF {...iconProps} />
+ default:
+ return <FileText {...iconProps} />
+ }
+}
+
+const getSourceUrl = (document: DocumentWithMemories) => {
+ if (document.type === "google_doc" && document.customId) {
+ return `https://docs.google.com/document/d/${document.customId}`
+ }
+ if (document.type === "google_sheet" && document.customId) {
+ return `https://docs.google.com/spreadsheets/d/${document.customId}`
+ }
+ if (document.type === "google_slide" && document.customId) {
+ return `https://docs.google.com/presentation/d/${document.customId}`
+ }
+ // Fallback to existing URL for all other document types
+ return document.url
+}
+
+const MemoryDetailItem = memo(({ memory }: { memory: MemoryEntry }) => {
+ return (
+ <button
+ className="p-4 rounded-lg border transition-all relative overflow-hidden cursor-pointer"
+ style={{
+ backgroundColor: memory.isLatest
+ ? colors.memory.primary
+ : "rgba(255, 255, 255, 0.02)",
+ borderColor: memory.isLatest
+ ? colors.memory.border
+ : "rgba(255, 255, 255, 0.1)",
+ backdropFilter: "blur(8px)",
+ WebkitBackdropFilter: "blur(8px)",
+ }}
+ tabIndex={0}
+ type="button"
+ >
+ <div className="flex items-start gap-2 relative z-10">
+ <div
+ className="p-1 rounded"
+ style={{
+ backgroundColor: memory.isLatest
+ ? colors.memory.secondary
+ : "transparent",
+ }}
+ >
+ <Brain
+ className={`w-4 h-4 flex-shrink-0 transition-all ${memory.isLatest ? "text-blue-400" : "text-blue-400/50"
+ }`}
+ />
+ </div>
+ <div className="flex-1 space-y-2">
+ <Label1Regular
+ className="text-sm leading-relaxed text-left"
+ style={{ color: colors.text.primary }}
+ >
+ {memory.memory}
+ </Label1Regular>
+ <div className="flex items-center gap-2 flex-wrap">
+ {memory.isForgotten && (
+ <Badge
+ className="text-xs border-red-500/30 backdrop-blur-sm"
+ style={{
+ backgroundColor: colors.status.forgotten,
+ color: "#dc2626",
+ backdropFilter: "blur(4px)",
+ WebkitBackdropFilter: "blur(4px)",
+ }}
+ variant="destructive"
+ >
+ Forgotten
+ </Badge>
+ )}
+ {memory.isLatest && (
+ <Badge
+ className="text-xs border-blue-400/30 backdrop-blur-sm"
+ style={{
+ backgroundColor: colors.memory.secondary,
+ color: colors.accent.primary,
+ backdropFilter: "blur(4px)",
+ WebkitBackdropFilter: "blur(4px)",
+ }}
+ variant="default"
+ >
+ Latest
+ </Badge>
+ )}
+ {memory.forgetAfter && (
+ <Badge
+ className="text-xs backdrop-blur-sm"
+ style={{
+ borderColor: colors.status.expiring,
+ color: colors.status.expiring,
+ backgroundColor: "rgba(251, 165, 36, 0.1)",
+ backdropFilter: "blur(4px)",
+ WebkitBackdropFilter: "blur(4px)",
+ }}
+ variant="outline"
+ >
+ Expires: {formatDate(memory.forgetAfter)}
+ </Badge>
+ )}
+ </div>
+ <div
+ className="flex items-center gap-4 text-xs"
+ style={{ color: colors.text.muted }}
+ >
+ <span className="flex items-center gap-1">
+ <Calendar className="w-3 h-3" />
+ {formatDate(memory.createdAt)}
+ </span>
+ <span className="font-mono">v{memory.version}</span>
+ {memory.sourceRelevanceScore && (
+ <span
+ className="flex items-center gap-1"
+ style={{
+ color:
+ memory.sourceRelevanceScore > 70
+ ? colors.accent.emerald
+ : colors.text.muted,
+ }}
+ >
+ <Sparkles className="w-3 h-3" />
+ {memory.sourceRelevanceScore}%
+ </span>
+ )}
+ </div>
+ </div>
+ </div>
+ </button>
+ )
+})
+
+const DocumentCard = memo(
+ ({
+ document,
+ onOpenDetails,
+ }: {
+ document: DocumentWithMemories
+ onOpenDetails: (document: DocumentWithMemories) => void
+ }) => {
+ const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten)
+ const forgottenMemories = document.memoryEntries.filter(
+ (m) => m.isForgotten,
+ )
+
+ return (
+ <Card
+ className="h-full mx-4 p-4 transition-all cursor-pointer group relative overflow-hidden border-0 gap-2 md:w-full"
+ onClick={() => {
+ analytics.documentCardClicked()
+ onOpenDetails(document)
+ }}
+ style={{
+ backgroundColor: colors.document.primary,
+ }}
+ >
+ <CardHeader className="relative z-10 px-0">
+ <div className="flex items-center justify-between gap-2">
+ <div className="flex items-center gap-1">
+ {getDocumentIcon(document.type, "w-4 h-4 flex-shrink-0")}
+ <p className={cn("text-sm font-medium line-clamp-1", document.url ? "max-w-[190px]" : "max-w-[200px]")}>{document.title || "Untitled Document"}</p>
+ </div>
+ {document.url && (
+ <button
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded"
+ onClick={(e) => {
+ e.stopPropagation()
+ const sourceUrl = getSourceUrl(document)
+ window.open(sourceUrl ?? undefined, "_blank")
+ }}
+ style={{
+ backgroundColor: "rgba(255, 255, 255, 0.05)",
+ color: colors.text.secondary,
+ }}
+ type="button"
+ >
+ <ExternalLink className="w-3 h-3" />
+ </button>
+ )}
+ <div className="flex items-center gap-2 text-[10px] text-muted-foreground">
+ <span>{formatDate(document.createdAt)}</span>
+ </div>
+ </div>
+ </CardHeader>
+ <CardContent className="relative z-10 px-0">
+ {document.summary && (
+ <p
+ className="text-xs line-clamp-2 mb-3"
+ style={{ color: colors.text.muted }}
+ >
+ {document.summary}
+ </p>
+ )}
+ <div className="flex items-center gap-2 flex-wrap">
+ {activeMemories.length > 0 && (
+ <Badge
+ className="text-xs text-accent-foreground"
+ style={{
+ backgroundColor: colors.memory.secondary,
+ }}
+ variant="secondary"
+ >
+ <Brain className="w-3 h-3 mr-1" />
+ {activeMemories.length}{" "}
+ {activeMemories.length === 1 ? "memory" : "memories"}
+ </Badge>
+ )}
+ {forgottenMemories.length > 0 && (
+ <Badge
+ className="text-xs"
+ style={{
+ borderColor: "rgba(255, 255, 255, 0.2)",
+ color: colors.text.muted,
+ }}
+ variant="outline"
+ >
+ {forgottenMemories.length} forgotten
+ </Badge>
+ )}
+ </div>
+ </CardContent>
+ </Card>
+ )
+ },
+)
+
+const DocumentDetailSheet = memo(
+ ({
+ document,
+ isOpen,
+ onClose,
+ isMobile,
+ }: {
+ document: DocumentWithMemories | null
+ isOpen: boolean
+ onClose: () => void
+ isMobile: boolean
+ }) => {
+ if (!document) return null
+
+ const [isSummaryExpanded, setIsSummaryExpanded] = useState(false)
+ const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten)
+ const forgottenMemories = document.memoryEntries.filter(
+ (m) => m.isForgotten,
+ )
+
+ const HeaderContent = ({ TitleComponent }: { TitleComponent: typeof SheetTitle | typeof DrawerTitle }) => (
+ <div className="flex items-start justify-between gap-2">
+ <div className="flex items-start gap-3 flex-1">
+ <div
+ className="p-2 rounded-lg"
+ style={{
+ backgroundColor: colors.document.secondary,
+ border: `1px solid ${colors.document.border}`,
+ }}
+ >
+ {getDocumentIcon(document.type, "w-5 h-5")}
+ </div>
+ <div className="flex-1">
+ <TitleComponent style={{ color: colors.text.primary }}>
+ {document.title || "Untitled Document"}
+ </TitleComponent>
+ <div
+ className="flex items-center gap-2 mt-1 text-xs"
+ style={{ color: colors.text.muted }}
+ >
+ <span>{formatDocumentType(document.type)}</span>
+ <span>•</span>
+ <span>
+ {formatDate(document.createdAt)}
+ </span>
+ {document.url && (
+ <>
+ <span>•</span>
+ <button
+ className="flex items-center gap-1 transition-all hover:gap-2"
+ onClick={() => {
+ const sourceUrl = getSourceUrl(document)
+ window.open(sourceUrl ?? undefined, "_blank")
+ }}
+ style={{ color: colors.accent.primary }}
+ type="button"
+ >
+ View source
+ <ExternalLink className="w-3 h-3" />
+ </button>
+ </>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+
+ const SummarySection = () => {
+ if (!document.summary) return null
+
+ const shouldShowToggle = document.summary.length > 200 // Show toggle for longer summaries
+
+ return (
+ <div
+ className="mt-4 p-3 rounded-lg"
+ style={{
+ backgroundColor: "rgba(255, 255, 255, 0.03)",
+ border: "1px solid rgba(255, 255, 255, 0.08)",
+ }}
+ >
+ <p
+ className={`text-sm ${!isSummaryExpanded ? 'line-clamp-3' : ''}`}
+ style={{ color: colors.text.muted }}
+ >
+ {document.summary}
+ </p>
+ {shouldShowToggle && (
+ <button
+ onClick={() => setIsSummaryExpanded(!isSummaryExpanded)}
+ className="mt-2 text-xs hover:underline transition-all"
+ style={{ color: colors.accent.primary }}
+ type="button"
+ >
+ {isSummaryExpanded ? 'Show less' : 'Show more'}
+ </button>
+ )}
+ </div>
+ )
+ }
+
+ const MemoryContent = () => (
+ <div className="p-6 space-y-6">
+ {activeMemories.length > 0 && (
+ <div>
+ <div
+ className="text-sm font-medium mb-4 flex items-start gap-2 px-3 py-2 rounded-lg"
+ style={{
+ color: colors.text.secondary,
+ backgroundColor: colors.memory.primary,
+ border: `1px solid ${colors.memory.border}`,
+ }}
+ >
+ <Brain className="w-4 h-4 text-blue-400" />
+ Active Memories ({activeMemories.length})
+ </div>
+ <div className="space-y-3">
+ {activeMemories.map((memory, index) => (
+ <div
+ className="animate-in fade-in slide-in-from-right-2"
+ key={memory.id}
+ style={{ animationDelay: `${index * 50}ms` }}
+ >
+ <MemoryDetailItem memory={memory} />
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {forgottenMemories.length > 0 && (
+ <div>
+ <div
+ className="text-sm font-medium mb-4 px-3 py-2 rounded-lg opacity-60"
+ style={{
+ color: colors.text.muted,
+ backgroundColor: "rgba(255, 255, 255, 0.02)",
+ border: "1px solid rgba(255, 255, 255, 0.08)",
+ }}
+ >
+ Forgotten Memories ({forgottenMemories.length})
+ </div>
+ <div className="space-y-3 opacity-40">
+ {forgottenMemories.map((memory) => (
+ <MemoryDetailItem key={memory.id} memory={memory} />
+ ))}
+ </div>
+ </div>
+ )}
+
+ {activeMemories.length === 0 &&
+ forgottenMemories.length === 0 && (
+ <div
+ className="text-center py-12 rounded-lg"
+ style={{
+ backgroundColor: "rgba(255, 255, 255, 0.02)",
+ border: "1px solid rgba(255, 255, 255, 0.08)",
+ }}
+ >
+ <Brain
+ className="w-12 h-12 mx-auto mb-4 opacity-30"
+ style={{ color: colors.text.muted }}
+ />
+ <p style={{ color: colors.text.muted }}>
+ No memories found for this document
+ </p>
+ </div>
+ )}
+ </div>
+ )
+
+ if (isMobile) {
+ return (
+ <Drawer onOpenChange={onClose} open={isOpen}>
+ <DrawerContent
+ className="border-0 p-0 overflow-hidden max-h-[90vh]"
+ style={{
+ backgroundColor: colors.background.secondary,
+ borderTop: `1px solid ${colors.document.border}`,
+ backdropFilter: "blur(20px)",
+ WebkitBackdropFilter: "blur(20px)",
+ }}
+ >
+ {/* Header section with glass effect */}
+ <div
+ className="p-4 relative border-b"
+ style={{
+ backgroundColor: "rgba(255, 255, 255, 0.02)",
+ borderBottom: `1px solid ${colors.document.border}`,
+ }}
+ >
+ <DrawerHeader className="pb-0 px-0 text-left">
+ <HeaderContent TitleComponent={DrawerTitle} />
+ </DrawerHeader>
+
+ <SummarySection />
+ </div>
+
+ <div className="flex-1 memory-drawer-scroll overflow-y-auto">
+ <MemoryContent />
+ </div>
+ </DrawerContent>
+ </Drawer>
+ )
+ }
+
+ return (
+ <Sheet onOpenChange={onClose} open={isOpen}>
+ <SheetContent
+ className="w-full sm:max-w-2xl border-0 p-0 overflow-hidden"
+ style={{
+ backgroundColor: colors.background.secondary,
+ borderLeft: `1px solid ${colors.document.border}`,
+ backdropFilter: "blur(20px)",
+ WebkitBackdropFilter: "blur(20px)",
+ }}
+ >
+ {/* Header section with glass effect */}
+ <div
+ className="p-6 relative"
+ style={{
+ backgroundColor: "rgba(255, 255, 255, 0.02)",
+ borderBottom: `1px solid ${colors.document.border}`,
+ }}
+ >
+ <SheetHeader className="pb-0">
+ <HeaderContent TitleComponent={SheetTitle} />
+ </SheetHeader>
+
+ <SummarySection />
+ </div>
+
+ <div className="h-[calc(100vh-200px)] memory-sheet-scroll overflow-y-auto">
+ <MemoryContent />
+ </div>
+ </SheetContent>
+ </Sheet>
+ )
+ },
+)
+
+export const MemoryListView = ({
+ children,
+ documents,
+ isLoading,
+ isLoadingMore,
+ error,
+ hasMore,
+ loadMoreDocuments,
+}: MemoryListViewProps) => {
+ const [selectedSpace, _] = useState<string>("all")
+ const [selectedDocument, setSelectedDocument] =
+ useState<DocumentWithMemories | null>(null)
+ const [isDetailOpen, setIsDetailOpen] = useState(false)
+ const parentRef = useRef<HTMLDivElement>(null)
+ const containerRef = useRef<HTMLDivElement>(null)
+ const isMobile = useIsMobile()
+
+ const gap = 14
+
+ const { width: containerWidth } = useResizeObserver(containerRef);
+ const columnWidth = isMobile ? containerWidth : 320
+ const columns = Math.max(1, Math.floor((containerWidth + gap) / (columnWidth + gap)))
+
+ // Filter documents based on selected space
+ const filteredDocuments = useMemo(() => {
+ if (!documents) return []
+
+ if (selectedSpace === "all") {
+ return documents
+ }
+
+ return documents
+ .map((doc) => ({
+ ...doc,
+ memoryEntries: doc.memoryEntries.filter(
+ (memory) =>
+ (memory.spaceContainerTag ?? memory.spaceId) === selectedSpace,
+ ),
+ }))
+ .filter((doc) => doc.memoryEntries.length > 0)
+ }, [documents, selectedSpace])
+
+ const handleOpenDetails = useCallback((document: DocumentWithMemories) => {
+ analytics.memoryDetailOpened()
+ setSelectedDocument(document)
+ setIsDetailOpen(true)
+ }, [])
+
+ const handleCloseDetails = useCallback(() => {
+ setIsDetailOpen(false)
+ setTimeout(() => setSelectedDocument(null), 300)
+ }, [])
+
+ const virtualItems = useMemo(() => {
+ const items = []
+ for (let i = 0; i < filteredDocuments.length; i += columns) {
+ items.push(filteredDocuments.slice(i, i + columns))
+ }
+ return items
+ }, [filteredDocuments, columns])
+
+ const virtualizer = useVirtualizer({
+ count: virtualItems.length,
+ getScrollElement: () => parentRef.current,
+ overscan: 5,
+ estimateSize: () => 200,
+ })
+
+ useEffect(() => {
+ const [lastItem] = [...virtualizer.getVirtualItems()].reverse()
+
+ if (!lastItem || !hasMore || isLoadingMore) {
+ return
+ }
+
+ if (lastItem.index >= virtualItems.length - 1) {
+ loadMoreDocuments()
+ }
+ }, [hasMore, isLoadingMore, loadMoreDocuments, virtualizer.getVirtualItems(), virtualItems.length])
+
+ // Always render with consistent structure
+ return (
+ <>
+ <div
+ className="h-full overflow-hidden relative pb-20"
+ style={{ backgroundColor: colors.background.primary }}
+ ref={containerRef}
+ >
+ {error ? (
+ <div className="h-full flex items-center justify-center p-4">
+ <div className="rounded-xl overflow-hidden">
+ <div
+ className="relative z-10 px-6 py-4"
+ style={{ color: colors.text.primary }}
+ >
+ Error loading documents: {error.message}
+ </div>
+ </div>
+ </div>
+ ) : isLoading ? (
+ <div className="h-full flex items-center justify-center p-4">
+ <div className="rounded-xl overflow-hidden">
+ <div
+ className="relative z-10 px-6 py-4"
+ style={{ color: colors.text.primary }}
+ >
+ <div className="flex items-center gap-2">
+ <Sparkles className="w-4 h-4 animate-spin text-blue-400" />
+ <span>Loading memory list...</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ ) : filteredDocuments.length === 0 && !isLoading ? (
+ <div className="h-full flex items-center justify-center p-4">
+ {children}
+ </div>
+ ) : (
+ <div
+ ref={parentRef}
+ className="h-full overflow-auto mt-20 custom-scrollbar"
+ >
+ <GreetingMessage />
+
+ <div
+ className="w-full relative"
+ style={{ height: `${virtualizer.getTotalSize() + (virtualItems.length * gap)}px` }}
+ >
+ {virtualizer.getVirtualItems().map((virtualRow) => {
+ const rowItems = virtualItems[virtualRow.index]
+ if (!rowItems) return null
+
+ return (
+ <div
+ key={virtualRow.key}
+ data-index={virtualRow.index}
+ ref={virtualizer.measureElement}
+ className="absolute top-0 left-0 w-full"
+ style={{ transform: `translateY(${virtualRow.start + (virtualRow.index * gap)}px)` }}
+ >
+ <div
+ className="grid justify-start"
+ style={{ gridTemplateColumns: `repeat(${columns}, ${columnWidth}px)`, gap: `${gap}px` }}
+ >
+ {rowItems.map((document) => (
+ <DocumentCard
+ key={document.id}
+ document={document}
+ onOpenDetails={handleOpenDetails}
+ />
+ ))}
+ </div>
+ </div>
+ )
+ })}
+ </div>
+
+ {isLoadingMore && (
+ <div className="py-8 flex items-center justify-center">
+ <div className="flex items-center gap-2">
+ <Sparkles className="w-4 h-4 animate-spin text-blue-400" />
+ <span style={{ color: colors.text.primary }}>
+ Loading more memories...
+ </span>
+ </div>
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+
+ <DocumentDetailSheet
+ document={selectedDocument}
+ isOpen={isDetailOpen}
+ onClose={handleCloseDetails}
+ isMobile={isMobile}
+ />
+ </>
+ )
+}
diff --git a/apps/web/components/menu.tsx b/apps/web/components/menu.tsx
new file mode 100644
index 00000000..622b94b1
--- /dev/null
+++ b/apps/web/components/menu.tsx
@@ -0,0 +1,618 @@
+"use client"
+
+import { useIsMobile } from "@hooks/use-mobile"
+import {
+ fetchConsumerProProduct,
+ fetchMemoriesFeature,
+} from "@repo/lib/queries"
+import { Button } from "@repo/ui/components/button"
+import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold"
+import { GlassMenuEffect } from "@ui/other/glass-effect"
+import { useCustomer } from "autumn-js/react"
+import {
+ MessageSquareMore,
+ Plus,
+ User,
+ X,
+} from "lucide-react"
+import { AnimatePresence, LayoutGroup, motion } from "motion/react"
+import { useEffect, useState } from "react"
+import { useMobilePanel } from "@/lib/mobile-panel-context"
+import { TOUR_STEP_IDS } from "@/lib/tour-constants"
+import { ProjectSelector } from "./project-selector"
+import { useTour } from "./tour"
+import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory"
+import { MCPView } from "./views/mcp"
+import { ProfileView } from "./views/profile"
+import { useChatOpen } from "@/stores"
+import { Drawer } from "vaul"
+
+const MCPIcon = ({ className }: { className?: string }) => {
+ return (
+ <svg
+ className={className}
+ fill="currentColor"
+ fillRule="evenodd"
+ viewBox="0 0 24 24"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <title>ModelContextProtocol</title>
+ <path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" />
+ <path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" />
+ </svg>
+ )
+}
+
+function Menu({ id }: { id?: string }) {
+ const [isHovered, setIsHovered] = useState(false)
+ const [expandedView, setExpandedView] = useState<
+ "addUrl" | "mcp" | "projects" | "profile" | null
+ >(null)
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
+ const [isCollapsing, setIsCollapsing] = useState(false)
+ const [showAddMemoryView, setShowAddMemoryView] = useState(false)
+ const isMobile = useIsMobile()
+ const { activePanel, setActivePanel } = useMobilePanel()
+ const { setMenuExpanded } = useTour()
+ const autumn = useCustomer()
+ const { setIsOpen } = useChatOpen()
+
+ const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any)
+
+ const memoriesUsed = memoriesCheck?.usage ?? 0
+ const memoriesLimit = memoriesCheck?.included_usage ?? 0
+
+ const { data: proCheck } = fetchConsumerProProduct(autumn as any)
+
+ useEffect(() => {
+ if (memoriesCheck) {
+ console.log({ memoriesCheck })
+ }
+
+ if (proCheck) {
+ console.log({ proCheck })
+ }
+ }, [memoriesCheck, proCheck])
+
+ const isProUser = proCheck?.allowed ?? false
+
+ const shouldShowLimitWarning =
+ !isProUser && memoriesUsed >= memoriesLimit * 0.8
+
+ // Map menu item keys to tour IDs
+ const menuItemTourIds: Record<string, string> = {
+ addUrl: TOUR_STEP_IDS.MENU_ADD_MEMORY,
+ projects: TOUR_STEP_IDS.MENU_PROJECTS,
+ mcp: TOUR_STEP_IDS.MENU_MCP,
+ }
+
+ const menuItems = [
+ {
+ icon: Plus,
+ text: "Add Memory",
+ key: "addUrl" as const,
+ disabled: false,
+ },
+ {
+ icon: MessageSquareMore,
+ text: "Chat",
+ key: "chat" as const,
+ disabled: false,
+ },
+ {
+ icon: MCPIcon,
+ text: "MCP",
+ key: "mcp" as const,
+ disabled: false,
+ },
+ {
+ icon: User,
+ text: "Profile",
+ key: "profile" as const,
+ disabled: false,
+ },
+ ]
+
+ const handleMenuItemClick = (
+ key: "chat" | "addUrl" | "mcp" | "projects" | "profile",
+ ) => {
+ if (key === "chat") {
+ setIsOpen(true)
+ setIsMobileMenuOpen(false)
+ if (isMobile) {
+ setActivePanel("chat")
+ }
+ } else {
+ if (expandedView === key) {
+ setIsCollapsing(true)
+ setExpandedView(null)
+ } else if (key === "addUrl") {
+ setShowAddMemoryView(true)
+ setExpandedView(null)
+ } else {
+ setExpandedView(key)
+ }
+ if (isMobile) {
+ setActivePanel("menu")
+ }
+ }
+ }
+
+ // Watch for active panel changes on mobile
+ useEffect(() => {
+ if (isMobile && activePanel !== "menu" && activePanel !== null) {
+ // Another panel became active, close the menu
+ setIsMobileMenuOpen(false)
+ setExpandedView(null)
+ }
+ }, [isMobile, activePanel])
+
+ // Notify tour provider about expansion state changes
+ useEffect(() => {
+ const isExpanded = isMobile
+ ? isMobileMenuOpen || !!expandedView
+ : isHovered || !!expandedView
+ setMenuExpanded(isExpanded)
+ }, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded])
+
+ // Calculate width based on state
+ const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56
+
+ // Dynamic z-index for mobile based on active panel
+ const mobileZIndex = isMobile && activePanel === "menu" ? "z-[70]" : "z-[100]"
+
+ return (
+ <>
+ {/* Desktop Menu */}
+ {!isMobile && (
+ <LayoutGroup>
+ <div className="fixed h-screen w-full p-4 items-center top-0 left-0 pointer-events-none z-[60] flex">
+ <motion.nav
+ animate={{
+ width: menuWidth,
+ scale: 1,
+ }}
+ className="pointer-events-auto group relative flex text-sm font-medium flex-col items-start overflow-hidden rounded-3xl shadow-2xl"
+ id={id}
+ initial={{ width: 56, scale: 0.95 }}
+ layout
+ onMouseEnter={() => !expandedView && setIsHovered(true)}
+ onMouseLeave={() => !expandedView && setIsHovered(false)}
+ transition={{
+ width: {
+ duration: 0.2,
+ ease: [0.4, 0, 0.2, 1],
+ },
+ scale: {
+ duration: 0.5,
+ ease: [0.4, 0, 0.2, 1],
+ },
+ layout: {
+ duration: 0.2,
+ ease: [0.4, 0, 0.2, 1],
+ },
+ }}
+ >
+ {/* Glass effect background */}
+ <motion.div className="absolute inset-0" layout>
+ <GlassMenuEffect />
+ </motion.div>
+
+ {/* Menu content */}
+ <motion.div
+ className="relative z-20 flex flex-col gap-6 w-full"
+ layout
+ >
+ <AnimatePresence
+ initial={false}
+ mode="wait"
+ onExitComplete={() => setIsCollapsing(false)}
+ >
+ {!expandedView ? (
+ <motion.div
+ animate={{
+ opacity: 1,
+ }}
+ className="w-full flex flex-col gap-6 p-4"
+ exit={{
+ opacity: 0,
+ transition: {
+ duration: 0.2,
+ ease: "easeOut",
+ },
+ }}
+ initial={{
+ opacity: 0,
+ }}
+ key="menu-items"
+ layout
+ style={{
+ transform: "translateZ(0)",
+ willChange: "opacity",
+ }}
+ transition={{
+ opacity: {
+ duration: 0.15,
+ ease: "easeInOut",
+ },
+ }}
+ >
+ <div className="flex flex-col gap-6">
+ {menuItems.map((item, index) => (
+ <div key={item.key}>
+ <motion.button
+ animate={{
+ opacity: 1,
+ y: 0,
+ scale: 1,
+ transition: {
+ duration: 0.1,
+ },
+ }}
+ className={`flex w-full items-center text-white/80 transition-colors duration-100 hover:text-white cursor-pointer relative ${isHovered || expandedView ? "px-1" : ""}`}
+ id={menuItemTourIds[item.key]}
+ initial={{ opacity: 0, y: 20, scale: 0.95 }}
+ layout
+ onClick={() => handleMenuItemClick(item.key)}
+ type="button"
+ whileHover={{
+ scale: 1.02,
+ transition: { duration: 0.1 },
+ }}
+ whileTap={{ scale: 0.98 }}
+ >
+ <motion.div
+ animate={{
+ scale: 1,
+ transition: {
+ delay: expandedView === null ? 0.15 : 0,
+ duration: 0.1,
+ },
+ }}
+ initial={{ scale: 0.8 }}
+ layout="position"
+ >
+ <item.icon className="duration-200 h-6 w-6 drop-shadow-lg flex-shrink-0" />
+ </motion.div>
+ <motion.p
+ animate={{
+ opacity: isHovered ? 1 : 0,
+ x: isHovered ? 0 : -10,
+ }}
+ className="drop-shadow-lg absolute left-10 whitespace-nowrap flex items-center gap-2"
+ initial={{ opacity: 0, x: -10 }}
+ style={{
+ transform: "translateZ(0)",
+ }}
+ transition={{
+ duration: 0.3,
+ delay: index * 0.03,
+ ease: [0.4, 0, 0.2, 1],
+ }}
+ >
+ {item.text}
+ {/* Show warning indicator for Add Memory when limits approached */}
+ {shouldShowLimitWarning &&
+ item.key === "addUrl" && (
+ <span className="text-xs bg-yellow-500/20 text-yellow-400 px-1.5 py-0.5 rounded">
+ {memoriesLimit - memoriesUsed} left
+ </span>
+ )}
+ </motion.p>
+ </motion.button>
+ {index === 0 && (
+ <motion.div
+ animate={{
+ opacity: 1,
+ scaleX: 1,
+ }}
+ className="w-full h-px bg-white/20 mt-3 origin-left"
+ initial={{ opacity: 0, scaleX: 0 }}
+ transition={{
+ duration: 0.3,
+ delay: 0.1,
+ ease: [0.4, 0, 0.2, 1],
+ }}
+ />
+ )}
+ </div>
+ ))}
+ </div>
+ </motion.div>
+ ) : (
+ <motion.div
+ animate={{
+ opacity: 1,
+ }}
+ className="w-full p-4"
+ exit={{
+ opacity: 0,
+ transition: {
+ duration: 0.2,
+ ease: "easeOut",
+ },
+ }}
+ initial={{
+ opacity: 0,
+ }}
+ key="expanded-view"
+ layout
+ style={{
+ transform: "translateZ(0)",
+ willChange: "opacity, transform",
+ }}
+ transition={{
+ opacity: {
+ duration: 0.15,
+ ease: "easeInOut",
+ },
+ }}
+ >
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex items-center justify-between mb-4"
+ initial={{ opacity: 0, y: -10 }}
+ layout
+ transition={{
+ delay: 0.05,
+ duration: 0.2,
+ ease: [0.4, 0, 0.2, 1],
+ }}
+ >
+ <HeadingH2Bold className="text-white">
+ {expandedView === "mcp" && "Model Context Protocol"}
+ {expandedView === "profile" && "Profile"}
+ </HeadingH2Bold>
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ initial={{ opacity: 0, scale: 0.8 }}
+ transition={{
+ delay: 0.08,
+ duration: 0.2,
+ }}
+ >
+ <Button
+ className="text-white/70 hover:text-white transition-colors duration-200"
+ onClick={() => {
+ setIsCollapsing(true)
+ setExpandedView(null)
+ }}
+ size="icon"
+ variant="ghost"
+ >
+ <X className="h-5 w-5" />
+ </Button>
+ </motion.div>
+ </motion.div>
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="max-h-[70vh] overflow-y-auto pr-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{
+ delay: 0.1,
+ duration: 0.25,
+ ease: [0.4, 0, 0.2, 1],
+ }}
+ >
+ {expandedView === "mcp" && <MCPView />}
+ {expandedView === "profile" && <ProfileView />}
+ </motion.div>
+ </motion.div>
+ )}
+ </AnimatePresence>
+ </motion.div>
+ </motion.nav>
+ </div>
+ </LayoutGroup>
+ )}
+
+ {/* Mobile Menu with Vaul Drawer */}
+ {isMobile && (
+ <Drawer.Root
+ open={isMobileMenuOpen || !!expandedView}
+ onOpenChange={(open) => {
+ if (!open) {
+ setIsMobileMenuOpen(false)
+ setExpandedView(null)
+ setActivePanel(null)
+ }
+ }}
+ >
+ {/* Menu Trigger Button */}
+ {!isMobileMenuOpen && !expandedView && (
+ <Drawer.Trigger asChild>
+ <div className={`fixed bottom-8 right-6 z-100 ${mobileZIndex}`}>
+ <motion.button
+ animate={{ scale: 1, opacity: 1 }}
+ className="w-14 h-14 flex items-center justify-center text-white rounded-full shadow-2xl"
+ initial={{ scale: 0.8, opacity: 0 }}
+ onClick={() => {
+ setIsMobileMenuOpen(true)
+ setActivePanel("menu")
+ }}
+ transition={{
+ duration: 0.3,
+ ease: [0.4, 0, 0.2, 1],
+ }}
+ type="button"
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ {/* Glass effect background */}
+ <div className="absolute inset-0 rounded-full">
+ <GlassMenuEffect rounded="rounded-full" />
+ </div>
+ <svg
+ className="h-6 w-6 relative z-10"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth={2}
+ viewBox="0 0 24 24"
+ >
+ <title>Open menu</title>
+ <path
+ d="M4 6h16M4 12h16M4 18h16"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ />
+ </svg>
+ </motion.button>
+ </div>
+ </Drawer.Trigger>
+ )}
+
+ <Drawer.Portal>
+ <Drawer.Overlay className="fixed inset-0 bg-black/40 z-[60]" />
+ <Drawer.Content className="bg-transparent fixed bottom-0 left-0 right-0 z-[70] outline-none">
+ <div className="w-full flex flex-col text-sm font-medium shadow-2xl relative overflow-hidden rounded-t-3xl max-h-[80vh]">
+ {/* Glass effect background */}
+ <div className="absolute inset-0 rounded-t-3xl">
+ <GlassMenuEffect rounded="rounded-t-3xl" />
+ </div>
+
+ {/* Drag Handle */}
+ <div className="relative z-20 flex justify-center py-3">
+ <div className="w-12 h-1 bg-white/30 rounded-full" />
+ </div>
+
+ {/* Menu content */}
+ <div className="relative z-20 flex flex-col w-full px-6 pb-8">
+ <AnimatePresence
+ initial={false}
+ mode="wait"
+ onExitComplete={() => setIsCollapsing(false)}
+ >
+ {!expandedView ? (
+ <motion.div
+ animate={{ opacity: 1 }}
+ className="w-full flex flex-col gap-6"
+ exit={{ opacity: 0 }}
+ initial={{ opacity: 0 }}
+ key="menu-items-mobile"
+ layout
+ >
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ initial={{ opacity: 0, y: -10 }}
+ transition={{ delay: 0.08 }}
+ >
+ <ProjectSelector />
+ </motion.div>
+
+ {/* Menu Items */}
+ <div className="flex flex-col gap-3">
+ {menuItems.map((item, index) => (
+ <div key={item.key}>
+ <motion.button
+ animate={{
+ opacity: 1,
+ y: 0,
+ transition: {
+ delay: 0.1 + index * 0.05,
+ duration: 0.3,
+ ease: "easeOut",
+ },
+ }}
+ className="flex w-full items-center gap-3 px-2 py-2 text-white/90 hover:text-white hover:bg-white/10 rounded-lg cursor-pointer relative"
+ id={menuItemTourIds[item.key]}
+ initial={{ opacity: 0, y: 10 }}
+ layout
+ onClick={() => {
+ handleMenuItemClick(item.key)
+ if (item.key !== "mcp" && item.key !== "profile") {
+ setIsMobileMenuOpen(false)
+ }
+ }}
+ type="button"
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <item.icon className="h-5 w-5 drop-shadow-lg flex-shrink-0" />
+ <span className="drop-shadow-lg text-sm font-medium flex-1 text-left">
+ {item.text}
+ </span>
+ {/* Show warning indicator for Add Memory when limits approached */}
+ {shouldShowLimitWarning &&
+ item.key === "addUrl" && (
+ <span className="text-xs bg-yellow-500/20 text-yellow-400 px-1.5 py-0.5 rounded">
+ {memoriesLimit - memoriesUsed} left
+ </span>
+ )}
+ </motion.button>
+ {/* Add horizontal line after first item */}
+ {index === 0 && (
+ <motion.div
+ animate={{
+ opacity: 1,
+ scaleX: 1,
+ }}
+ className="w-full h-px bg-white/20 mt-2 origin-left"
+ initial={{ opacity: 0, scaleX: 0 }}
+ transition={{
+ duration: 0.3,
+ delay: 0.15 + index * 0.05,
+ ease: [0.4, 0, 0.2, 1],
+ }}
+ />
+ )}
+ </div>
+ ))}
+ </div>
+ </motion.div>
+ ) : (
+ <motion.div
+ animate={{ opacity: 1 }}
+ className="w-full p-2 flex flex-col"
+ exit={{ opacity: 0 }}
+ initial={{ opacity: 0 }}
+ key="expanded-view-mobile"
+ layout
+ >
+ <div className="flex-1">
+ <motion.div className="mb-4 flex items-center justify-between" layout>
+ <HeadingH2Bold className="text-white">
+ {expandedView === "addUrl" && "Add Memory"}
+ {expandedView === "mcp" && "Model Context Protocol"}
+ {expandedView === "profile" && "Profile"}
+ </HeadingH2Bold>
+ <Button
+ className="text-white/70 hover:text-white transition-colors duration-200"
+ onClick={() => {
+ setIsCollapsing(true)
+ setExpandedView(null)
+ }}
+ size="icon"
+ variant="ghost"
+ >
+ <X className="h-5 w-5" />
+ </Button>
+ </motion.div>
+ <div className="max-h-[60vh] overflow-y-auto pr-1">
+ {expandedView === "addUrl" && (
+ <AddMemoryExpandedView />
+ )}
+ {expandedView === "mcp" && <MCPView />}
+ {expandedView === "profile" && <ProfileView />}
+ </div>
+ </div>
+ </motion.div>
+ )}
+ </AnimatePresence>
+ </div>
+ </div>
+ </Drawer.Content>
+ </Drawer.Portal>
+ </Drawer.Root>
+ )}
+
+ {showAddMemoryView && (
+ <AddMemoryView
+ initialTab="note"
+ onClose={() => setShowAddMemoryView(false)}
+ />
+ )}
+ </>
+ )
+}
+
+export default Menu
diff --git a/apps/web/components/project-selector.tsx b/apps/web/components/project-selector.tsx
new file mode 100644
index 00000000..bb62d8ca
--- /dev/null
+++ b/apps/web/components/project-selector.tsx
@@ -0,0 +1,566 @@
+"use client"
+
+import { $fetch } from "@repo/lib/api"
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
+import { ChevronDown, FolderIcon, Plus, Trash2, Loader2, MoreVertical, MoreHorizontal } from "lucide-react"
+import { motion, AnimatePresence } from "motion/react"
+import { useState } from "react"
+import { toast } from "sonner"
+import { CreateProjectDialog } from "./create-project-dialog"
+import { useProject } from "@/stores"
+import { useProjectName } from "@/hooks/use-project-name"
+import { useProjectMutations } from "@/hooks/use-project-mutations"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@repo/ui/components/dropdown-menu"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@repo/ui/components/dialog"
+import { Button } from "@repo/ui/components/button"
+import { Label } from "@repo/ui/components/label"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@repo/ui/components/select"
+import { DEFAULT_PROJECT_ID } from "@repo/lib/constants"
+
+interface Project {
+ id: string
+ name: string
+ containerTag: string
+ createdAt: string
+ updatedAt: string
+ isExperimental?: boolean
+}
+
+export function ProjectSelector() {
+ const queryClient = useQueryClient()
+ const [isOpen, setIsOpen] = useState(false)
+ const [showCreateDialog, setShowCreateDialog] = useState(false)
+ const { selectedProject } = useProject()
+ const projectName = useProjectName()
+ const { switchProject, deleteProjectMutation } = useProjectMutations()
+ const [deleteDialog, setDeleteDialog] = useState<{
+ open: boolean
+ project: null | { id: string; name: string; containerTag: string }
+ action: "move" | "delete"
+ targetProjectId: string
+ }>({
+ open: false,
+ project: null,
+ action: "move",
+ targetProjectId: DEFAULT_PROJECT_ID,
+ })
+ const [expDialog, setExpDialog] = useState<{
+ open: boolean
+ projectId: string
+ }>({
+ open: false,
+ projectId: "",
+ })
+
+ const { data: projects = [], isLoading } = useQuery({
+ queryKey: ["projects"],
+ queryFn: async () => {
+ const response = await $fetch("@get/projects")
+
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to load projects")
+ }
+
+ return response.data?.projects || []
+ },
+ staleTime: 30 * 1000,
+ })
+
+ const enableExperimentalMutation = useMutation({
+ mutationFn: async (projectId: string) => {
+ const response = await $fetch(`@post/projects/${projectId}/enable-experimental`)
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to enable experimental mode")
+ }
+ return response.data
+ },
+ onSuccess: () => {
+ toast.success("Experimental mode enabled for project")
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
+ setExpDialog({ open: false, projectId: "" })
+ },
+ onError: (error) => {
+ toast.error("Failed to enable experimental mode", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ })
+
+ const handleProjectSelect = (containerTag: string) => {
+ switchProject(containerTag)
+ setIsOpen(false)
+ }
+
+ const handleCreateNewProject = () => {
+ setIsOpen(false)
+ setShowCreateDialog(true)
+ }
+
+ return (
+ <div className="relative">
+ <motion.button
+ className="flex items-center gap-1.5 px-2 py-1.5 rounded-md bg-white/5 hover:bg-white/10 transition-colors"
+ onClick={() => setIsOpen(!isOpen)}
+ whileHover={{ scale: 1.01 }}
+ whileTap={{ scale: 0.99 }}
+ >
+ <FolderIcon className="h-3.5 w-3.5 text-white/70" />
+ <span className="text-xs font-medium text-white/90 max-w-32 truncate">
+ {isLoading ? "..." : projectName}
+ </span>
+ <motion.div
+ animate={{ rotate: isOpen ? 180 : 0 }}
+ transition={{ duration: 0.15 }}
+ >
+ <ChevronDown className="h-3 w-3 text-white/50" />
+ </motion.div>
+ </motion.button>
+
+ <AnimatePresence>
+ {isOpen && (
+ <>
+ <motion.div
+ className="fixed inset-0 z-40"
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ exit={{ opacity: 0 }}
+ onClick={() => setIsOpen(false)}
+ />
+
+ <motion.div
+ className="absolute top-full left-0 mt-1 w-56 bg-[#0f1419] backdrop-blur-xl border border-white/10 rounded-md shadow-xl z-50 overflow-hidden"
+ initial={{ opacity: 0, y: -5, scale: 0.98 }}
+ animate={{ opacity: 1, y: 0, scale: 1 }}
+ exit={{ opacity: 0, y: -5, scale: 0.98 }}
+ transition={{ duration: 0.15 }}
+ >
+ <div className="p-1.5 max-h-64 overflow-y-auto">
+ {/* Default Project */}
+ <motion.div
+ className={`flex items-center justify-between p-2 rounded-md transition-colors cursor-pointer ${
+ selectedProject === DEFAULT_PROJECT_ID
+ ? "bg-white/15"
+ : "hover:bg-white/8"
+ }`}
+ onClick={() => handleProjectSelect(DEFAULT_PROJECT_ID)}
+ >
+ <div className="flex items-center gap-2">
+ <FolderIcon className="h-3.5 w-3.5 text-white/70" />
+ <span className="text-xs font-medium text-white">Default</span>
+ </div>
+ </motion.div>
+
+ {/* User Projects */}
+ {projects
+ .filter((p: Project) => p.containerTag !== DEFAULT_PROJECT_ID)
+ .map((project: Project, index: number) => (
+ <motion.div
+ key={project.id}
+ className={`flex items-center justify-between p-2 rounded-md transition-colors group ${
+ selectedProject === project.containerTag
+ ? "bg-white/15"
+ : "hover:bg-white/8"
+ }`}
+ initial={{ opacity: 0, x: -5 }}
+ animate={{ opacity: 1, x: 0 }}
+ transition={{ delay: index * 0.03 }}
+ >
+ <div
+ className="flex items-center gap-2 flex-1 cursor-pointer"
+ onClick={() => handleProjectSelect(project.containerTag)}
+ >
+ <FolderIcon className="h-3.5 w-3.5 text-white/70" />
+ <span className="text-xs font-medium text-white truncate max-w-32">
+ {project.name}
+ </span>
+ </div>
+ <div className="flex items-center gap-1">
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <motion.button
+ className="p-1 hover:bg-white/10 rounded transition-all"
+ onClick={(e) => e.stopPropagation()}
+ whileHover={{ scale: 1.1 }}
+ whileTap={{ scale: 0.9 }}
+ >
+ <MoreHorizontal className="h-3 w-3 text-white/50" />
+ </motion.button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent
+ align="end"
+ className="bg-black/90 border-white/10"
+ >
+ {/* Show experimental toggle only if NOT experimental and NOT default project */}
+ {!project.isExperimental &&
+ project.containerTag !== DEFAULT_PROJECT_ID && (
+ <DropdownMenuItem
+ className="text-blue-400 hover:text-blue-300 cursor-pointer text-xs"
+ onClick={(e) => {
+ e.stopPropagation()
+ setExpDialog({
+ open: true,
+ projectId: project.id,
+ })
+ setIsOpen(false)
+ }}
+ >
+ <div className="h-3 w-3 mr-2 rounded border border-blue-400" />
+ Enable Experimental Mode
+ </DropdownMenuItem>
+ )}
+ {project.isExperimental && (
+ <DropdownMenuItem
+ className="text-blue-300/50 text-xs"
+ disabled
+ >
+ <div className="h-3 w-3 mr-2 rounded bg-blue-400" />
+ Experimental Mode Active
+ </DropdownMenuItem>
+ )}
+ <DropdownMenuItem
+ className="text-red-400 hover:text-red-300 cursor-pointer text-xs"
+ onClick={(e) => {
+ e.stopPropagation()
+ setDeleteDialog({
+ open: true,
+ project: {
+ id: project.id,
+ name: project.name,
+ containerTag: project.containerTag,
+ },
+ action: "move",
+ targetProjectId: "",
+ })
+ setIsOpen(false)
+ }}
+ >
+ <Trash2 className="h-3 w-3 mr-2" />
+ Delete Project
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ </div>
+ </motion.div>
+ ))}
+
+ <motion.div
+ className="flex items-center gap-2 p-2 rounded-md hover:bg-white/8 transition-colors cursor-pointer border-t border-white/10 mt-1"
+ onClick={handleCreateNewProject}
+ whileHover={{ x: 1 }}
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ transition={{ delay: (projects.length + 1) * 0.03 }}
+ >
+ <Plus className="h-3.5 w-3.5 text-white/70" />
+ <span className="text-xs font-medium text-white/80">New Project</span>
+ </motion.div>
+ </div>
+ </motion.div>
+ </>
+ )}
+ </AnimatePresence>
+
+ <CreateProjectDialog
+ open={showCreateDialog}
+ onOpenChange={setShowCreateDialog}
+ />
+
+ {/* Delete Project Dialog */}
+ <AnimatePresence>
+ {deleteDialog.open && deleteDialog.project && (
+ <Dialog
+ onOpenChange={(open) =>
+ setDeleteDialog((prev) => ({ ...prev, open }))
+ }
+ open={deleteDialog.open}
+ >
+ <DialogContent className="sm:max-w-2xl bg-black/90 backdrop-blur-xl border-white/10 text-white">
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.95 }}
+ initial={{ opacity: 0, scale: 0.95 }}
+ >
+ <DialogHeader>
+ <DialogTitle>Delete Project</DialogTitle>
+ <DialogDescription className="text-white/60">
+ Are you sure you want to delete "{deleteDialog.project.name}"?
+ Choose what to do with the documents in this project.
+ </DialogDescription>
+ </DialogHeader>
+ <div className="grid gap-4 py-4">
+ <div className="space-y-4">
+ <div className="flex items-center space-x-2">
+ <input
+ checked={deleteDialog.action === "move"}
+ className="w-4 h-4"
+ id="move"
+ name="action"
+ onChange={() =>
+ setDeleteDialog((prev) => ({
+ ...prev,
+ action: "move",
+ }))
+ }
+ type="radio"
+ />
+ <Label
+ className="text-white cursor-pointer text-sm"
+ htmlFor="move"
+ >
+ Move documents to another project
+ </Label>
+ </div>
+ {deleteDialog.action === "move" && (
+ <motion.div
+ animate={{ opacity: 1, height: "auto" }}
+ className="ml-6"
+ exit={{ opacity: 0, height: 0 }}
+ initial={{ opacity: 0, height: 0 }}
+ >
+ <Select
+ onValueChange={(value) =>
+ setDeleteDialog((prev) => ({
+ ...prev,
+ targetProjectId: value,
+ }))
+ }
+ value={deleteDialog.targetProjectId}
+ >
+ <SelectTrigger className="w-full bg-white/5 border-white/10 text-white">
+ <SelectValue placeholder="Select target project..." />
+ </SelectTrigger>
+ <SelectContent className="bg-black/90 backdrop-blur-xl border-white/10">
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ value={DEFAULT_PROJECT_ID}
+ >
+ Default Project
+ </SelectItem>
+ {projects
+ .filter(
+ (p: Project) =>
+ p.id !== deleteDialog.project?.id &&
+ p.containerTag !== DEFAULT_PROJECT_ID,
+ )
+ .map((project: Project) => (
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ key={project.id}
+ value={project.id}
+ >
+ {project.name}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </motion.div>
+ )}
+ <div className="flex items-center space-x-2">
+ <input
+ checked={deleteDialog.action === "delete"}
+ className="w-4 h-4"
+ id="delete"
+ name="action"
+ onChange={() =>
+ setDeleteDialog((prev) => ({
+ ...prev,
+ action: "delete",
+ }))
+ }
+ type="radio"
+ />
+ <Label
+ className="text-white cursor-pointer text-sm"
+ htmlFor="delete"
+ >
+ Delete all documents in this project
+ </Label>
+ </div>
+ {deleteDialog.action === "delete" && (
+ <motion.p
+ animate={{ opacity: 1 }}
+ className="text-sm text-red-400 ml-6"
+ initial={{ opacity: 0 }}
+ >
+ ⚠️ This action cannot be undone. All documents will be
+ permanently deleted.
+ </motion.p>
+ )}
+ </div>
+ </div>
+ <DialogFooter>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
+ onClick={() =>
+ setDeleteDialog({
+ open: false,
+ project: null,
+ action: "move",
+ targetProjectId: "",
+ })
+ }
+ type="button"
+ variant="outline"
+ >
+ Cancel
+ </Button>
+ </motion.div>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className={`${deleteDialog.action === "delete"
+ ? "bg-red-600 hover:bg-red-700"
+ : "bg-white/10 hover:bg-white/20"
+ } text-white border-white/20`}
+ disabled={
+ deleteProjectMutation.isPending ||
+ (deleteDialog.action === "move" &&
+ !deleteDialog.targetProjectId)
+ }
+ onClick={() => {
+ if (deleteDialog.project) {
+ deleteProjectMutation.mutate({
+ projectId: deleteDialog.project.id,
+ action: deleteDialog.action,
+ targetProjectId:
+ deleteDialog.action === "move"
+ ? deleteDialog.targetProjectId
+ : undefined,
+ }, {
+ onSuccess: () => {
+ setDeleteDialog({
+ open: false,
+ project: null,
+ action: "move",
+ targetProjectId: "",
+ })
+ }
+ })
+ }
+ }}
+ type="button"
+ >
+ {deleteProjectMutation.isPending ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ {deleteDialog.action === "move"
+ ? "Moving..."
+ : "Deleting..."}
+ </>
+ ) : deleteDialog.action === "move" ? (
+ "Move & Delete Project"
+ ) : (
+ "Delete Everything"
+ )}
+ </Button>
+ </motion.div>
+ </DialogFooter>
+ </motion.div>
+ </DialogContent>
+ </Dialog>
+ )}
+ </AnimatePresence>
+
+ {/* Experimental Mode Confirmation Dialog */}
+ <AnimatePresence>
+ {expDialog.open && (
+ <Dialog
+ onOpenChange={(open) => setExpDialog({ ...expDialog, open })}
+ open={expDialog.open}
+ >
+ <DialogContent className="sm:max-w-lg bg-black/90 backdrop-blur-xl border-white/10 text-white">
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ className="flex flex-col gap-4"
+ exit={{ opacity: 0, scale: 0.95 }}
+ initial={{ opacity: 0, scale: 0.95 }}
+ >
+ <DialogHeader>
+ <DialogTitle className="text-white">
+ Enable Experimental Mode?
+ </DialogTitle>
+ <DialogDescription className="text-white/60">
+ Experimental mode enables beta features and advanced memory
+ relationships for this project.
+ <br />
+ <br />
+ <span className="text-yellow-400 font-medium">
+ Warning:
+ </span>{" "}
+ This action is{" "}
+ <span className="text-red-400 font-bold">irreversible</span>
+ . Once enabled, you cannot return to regular mode for this
+ project.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
+ onClick={() =>
+ setExpDialog({ open: false, projectId: "" })
+ }
+ type="button"
+ variant="outline"
+ >
+ Cancel
+ </Button>
+ </motion.div>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-blue-600 hover:bg-blue-700 text-white"
+ disabled={enableExperimentalMutation.isPending}
+ onClick={() =>
+ enableExperimentalMutation.mutate(expDialog.projectId)
+ }
+ type="button"
+ >
+ {enableExperimentalMutation.isPending ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ Enabling...
+ </>
+ ) : (
+ "Enable Experimental Mode"
+ )}
+ </Button>
+ </motion.div>
+ </DialogFooter>
+ </motion.div>
+ </DialogContent>
+ </Dialog>
+ )}
+ </AnimatePresence>
+ </div>
+ )
+}
diff --git a/apps/web/components/referral-upgrade-modal.tsx b/apps/web/components/referral-upgrade-modal.tsx
new file mode 100644
index 00000000..2ad1b18a
--- /dev/null
+++ b/apps/web/components/referral-upgrade-modal.tsx
@@ -0,0 +1,290 @@
+"use client"
+
+import { useAuth } from "@lib/auth-context"
+import {
+ fetchMemoriesFeature,
+ fetchSubscriptionStatus,
+} from "@lib/queries"
+import { Button } from "@repo/ui/components/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle
+} from "@repo/ui/components/dialog"
+import { Input } from "@repo/ui/components/input"
+import { Label } from "@repo/ui/components/label"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@repo/ui/components/tabs"
+import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold"
+import { useCustomer } from "autumn-js/react"
+import {
+ CheckCircle,
+ Copy,
+ CreditCard,
+ Gift,
+ LoaderIcon,
+ Share2,
+ Users
+} from "lucide-react"
+import { motion } from "motion/react"
+import Link from "next/link"
+import { useState } from "react"
+
+interface ReferralUpgradeModalProps {
+ isOpen: boolean
+ onClose: () => void
+}
+
+export function ReferralUpgradeModal({ isOpen, onClose }: ReferralUpgradeModalProps) {
+ const { user } = useAuth()
+ const autumn = useCustomer()
+ const [isLoading, setIsLoading] = useState(false)
+ const [copied, setCopied] = useState(false)
+
+ const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any)
+ const memoriesUsed = memoriesCheck?.usage ?? 0
+ const memoriesLimit = memoriesCheck?.included_usage ?? 0
+
+ // Fetch subscription status
+ const {
+ data: status = {
+ consumer_pro: null,
+ },
+ isLoading: isCheckingStatus,
+ } = fetchSubscriptionStatus(autumn as any)
+
+ const isPro = status.consumer_pro
+
+ // Handle upgrade
+ const handleUpgrade = async () => {
+ setIsLoading(true)
+ try {
+ await autumn.attach({
+ productId: "consumer_pro",
+ successUrl: "https://app.supermemory.ai/",
+ })
+ window.location.reload()
+ } catch (error) {
+ console.error(error)
+ setIsLoading(false)
+ }
+ }
+
+ // Generate referral link (you'll need to implement this based on your referral system)
+ const referralLink = `https://app.supermemory.ai/ref/${user?.id || 'user'}`
+
+ const handleCopyReferralLink = async () => {
+ try {
+ await navigator.clipboard.writeText(referralLink)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ } catch (error) {
+ console.error('Failed to copy:', error)
+ }
+ }
+
+ const handleShare = async () => {
+ if (navigator.share) {
+ try {
+ await navigator.share({
+ title: 'Join Supermemory',
+ text: 'Check out Supermemory - the best way to organize and search your digital memories!',
+ url: referralLink,
+ })
+ } catch (error) {
+ console.error('Error sharing:', error)
+ }
+ } else {
+ handleCopyReferralLink()
+ }
+ }
+
+ if (user?.isAnonymous) {
+ return (
+ <Dialog open={isOpen} onOpenChange={onClose}>
+ <DialogContent className="sm:max-w-md bg-[#0f1419] backdrop-blur-xl border-white/10 text-white">
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.95 }}
+ initial={{ opacity: 0, scale: 0.95 }}
+ >
+ <DialogHeader>
+ <DialogTitle>Get More Memories</DialogTitle>
+ <DialogDescription className="text-white/60">
+ Sign in to access referrals and upgrade options
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="text-center py-6">
+ <Button
+ asChild
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ >
+ <Link href="/login">Sign in</Link>
+ </Button>
+ </div>
+ </motion.div>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
+ return (
+ <Dialog open={isOpen} onOpenChange={onClose}>
+ <DialogContent className="sm:max-w-lg bg-[#0f1419] backdrop-blur-xl border-white/10 text-white">
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.95 }}
+ initial={{ opacity: 0, scale: 0.95 }}
+ >
+ <DialogHeader className="mb-4">
+ <DialogTitle>Get More Memories</DialogTitle>
+ <DialogDescription className="text-white/60">
+ Expand your memory capacity through referrals or upgrades
+ </DialogDescription>
+ </DialogHeader>
+
+ {/* Current Usage */}
+ <div className="bg-white/5 rounded-lg p-4 mb-6">
+ <div className="flex justify-between items-center mb-2">
+ <span className="text-sm text-white/70">Current Usage</span>
+ <span className={`text-sm ${memoriesUsed >= memoriesLimit ? "text-red-400" : "text-white/90"}`}>
+ {memoriesUsed} / {memoriesLimit} memories
+ </span>
+ </div>
+ <div className="w-full bg-white/10 rounded-full h-2">
+ <div
+ className={`h-2 rounded-full transition-all ${
+ memoriesUsed >= memoriesLimit ? "bg-red-500" : isPro ? "bg-green-500" : "bg-blue-500"
+ }`}
+ style={{
+ width: `${Math.min((memoriesUsed / memoriesLimit) * 100, 100)}%`,
+ }}
+ />
+ </div>
+ </div>
+
+ {/* Tabs */}
+ <Tabs defaultValue="refer" className="w-full">
+ <TabsList className="grid w-full grid-cols-2 bg-white/5">
+ <TabsTrigger value="refer" className="flex items-center gap-2">
+ <Users className="w-4 h-4" />
+ Refer Friends
+ </TabsTrigger>
+ {!isPro && (
+ <TabsTrigger value="upgrade" className="flex items-center gap-2">
+ <CreditCard className="w-4 h-4" />
+ Upgrade Plan
+ </TabsTrigger>
+ )}
+ </TabsList>
+
+ <TabsContent value="refer" className="space-y-4 mt-6">
+ <div className="text-center">
+ <Gift className="w-12 h-12 text-blue-400 mx-auto mb-3" />
+ <HeadingH3Bold className="text-white mb-2">
+ Invite Friends, Get More Memories
+ </HeadingH3Bold>
+ <p className="text-white/70 text-sm">
+ For every friend who joins, you both get +5 extra memories!
+ </p>
+ </div>
+
+ <div className="bg-white/5 rounded-lg p-4">
+ <Label className="text-sm text-white/70 mb-2 block">
+ Your Referral Link
+ </Label>
+ <div className="flex gap-2">
+ <Input
+ className="flex-1 bg-white/10 border-white/20 text-white"
+ readOnly
+ value={referralLink}
+ />
+ <Button
+ className="bg-white/5 hover:bg-white/10 text-white border-white/20"
+ onClick={handleCopyReferralLink}
+ size="sm"
+ variant="outline"
+ >
+ {copied ? (
+ <CheckCircle className="w-4 h-4" />
+ ) : (
+ <Copy className="w-4 h-4" />
+ )}
+ </Button>
+ </div>
+ </div>
+
+ <Button
+ className="w-full bg-white/5 hover:bg-white/10 text-white border-white/20"
+ onClick={handleShare}
+ variant="outline"
+ >
+ <Share2 className="w-4 h-4 mr-2" />
+ Share Link
+ </Button>
+ </TabsContent>
+
+ {!isPro && (
+ <TabsContent value="upgrade" className="space-y-4 mt-6">
+ <div className="text-center">
+ <CreditCard className="w-12 h-12 text-purple-400 mx-auto mb-3" />
+ <HeadingH3Bold className="text-white mb-2">
+ Upgrade to Pro
+ </HeadingH3Bold>
+ <p className="text-white/70 text-sm">
+ Get unlimited memories and advanced features
+ </p>
+ </div>
+
+ <div className="bg-white/5 rounded-lg border border-white/10 p-4">
+ <h4 className="font-medium text-white mb-3">
+ Pro Plan Benefits
+ </h4>
+ <ul className="space-y-2">
+ <li className="flex items-center gap-2 text-sm text-white/90">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ 500 memories (vs {memoriesLimit} free)
+ </li>
+ <li className="flex items-center gap-2 text-sm text-white/90">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ 10 connections
+ </li>
+ <li className="flex items-center gap-2 text-sm text-white/90">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ Advanced search
+ </li>
+ <li className="flex items-center gap-2 text-sm text-white/90">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ Priority support
+ </li>
+ </ul>
+ </div>
+
+ <Button
+ className="w-full bg-blue-600 hover:bg-blue-700 text-white"
+ disabled={isLoading || isCheckingStatus}
+ onClick={handleUpgrade}
+ >
+ {isLoading || isCheckingStatus ? (
+ <>
+ <LoaderIcon className="h-4 w-4 animate-spin mr-2" />
+ Upgrading...
+ </>
+ ) : (
+ "Upgrade to Pro - $15/month"
+ )}
+ </Button>
+
+ <p className="text-xs text-white/50 text-center">
+ Cancel anytime. No questions asked.
+ </p>
+ </TabsContent>
+ )}
+ </Tabs>
+ </motion.div>
+ </DialogContent>
+ </Dialog>
+ )
+}
diff --git a/apps/web/components/spinner.tsx b/apps/web/components/spinner.tsx
new file mode 100644
index 00000000..aad15e33
--- /dev/null
+++ b/apps/web/components/spinner.tsx
@@ -0,0 +1,8 @@
+import { cn } from "@lib/utils";
+import { Loader2 } from "lucide-react";
+
+export function Spinner({ className }: { className?: string }) {
+ return (
+ <Loader2 className={cn("size-4 animate-spin", className)} />
+ )
+} \ No newline at end of file
diff --git a/apps/web/components/text-shimmer.tsx b/apps/web/components/text-shimmer.tsx
new file mode 100644
index 00000000..bef0e7a9
--- /dev/null
+++ b/apps/web/components/text-shimmer.tsx
@@ -0,0 +1,57 @@
+'use client';
+import React, { useMemo, type JSX } from 'react';
+import { motion } from 'motion/react';
+import { cn } from '@lib/utils';
+
+export type TextShimmerProps = {
+ children: string;
+ as?: React.ElementType;
+ className?: string;
+ duration?: number;
+ spread?: number;
+};
+
+function TextShimmerComponent({
+ children,
+ as: Component = 'p',
+ className,
+ duration = 2,
+ spread = 2,
+}: TextShimmerProps) {
+ const MotionComponent = motion.create(
+ Component as keyof JSX.IntrinsicElements
+ );
+
+ const dynamicSpread = useMemo(() => {
+ return children.length * spread;
+ }, [children, spread]);
+
+ return (
+ <MotionComponent
+ className={cn(
+ 'relative inline-block bg-[length:250%_100%,auto] bg-clip-text',
+ 'text-transparent [--base-color:#a1a1aa] [--base-gradient-color:#000]',
+ '[background-repeat:no-repeat,padding-box] [--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--base-gradient-color),#0000_calc(50%+var(--spread)))]',
+ 'dark:[--base-color:#71717a] dark:[--base-gradient-color:#ffffff] dark:[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--base-gradient-color),#0000_calc(50%+var(--spread)))]',
+ className
+ )}
+ initial={{ backgroundPosition: '100% center' }}
+ animate={{ backgroundPosition: '0% center' }}
+ transition={{
+ repeat: Infinity,
+ duration,
+ ease: 'linear',
+ }}
+ style={
+ {
+ '--spread': `${dynamicSpread}px`,
+ backgroundImage: `var(--bg), linear-gradient(var(--base-color), var(--base-color))`,
+ } as React.CSSProperties
+ }
+ >
+ {children}
+ </MotionComponent>
+ );
+}
+
+export const TextShimmer = React.memo(TextShimmerComponent);
diff --git a/apps/web/components/tour.tsx b/apps/web/components/tour.tsx
new file mode 100644
index 00000000..981effbc
--- /dev/null
+++ b/apps/web/components/tour.tsx
@@ -0,0 +1,413 @@
+"use client"
+
+import { Button } from "@repo/ui/components/button"
+import { GlassMenuEffect } from "@repo/ui/other/glass-effect"
+import { AnimatePresence, motion } from "motion/react"
+import * as React from "react"
+import { analytics } from "@/lib/analytics"
+
+// Types
+export interface TourStep {
+ content: React.ReactNode
+ selectorId: string
+ position?: "top" | "bottom" | "left" | "right" | "center"
+ onClickWithinArea?: () => void
+}
+
+interface TourContextType {
+ currentStep: number
+ totalSteps: number
+ nextStep: () => void
+ previousStep: () => void
+ endTour: () => void
+ isActive: boolean
+ isPaused: boolean
+ startTour: () => void
+ setSteps: (steps: TourStep[]) => void
+ steps: TourStep[]
+ isTourCompleted: boolean
+ setIsTourCompleted: (completed: boolean) => void
+ // Expansion state tracking
+ setMenuExpanded: (expanded: boolean) => void
+ setChatExpanded: (expanded: boolean) => void
+}
+
+// Context
+const TourContext = React.createContext<TourContextType | undefined>(undefined)
+
+export function useTour() {
+ const context = React.useContext(TourContext)
+ if (!context) {
+ throw new Error("useTour must be used within a TourProvider")
+ }
+ return context
+}
+
+// Provider
+interface TourProviderProps {
+ children: React.ReactNode
+ onComplete?: () => void
+ className?: string
+ isTourCompleted?: boolean
+}
+
+export function TourProvider({
+ children,
+ onComplete,
+ className,
+ isTourCompleted: initialCompleted = false,
+}: TourProviderProps) {
+ const [currentStep, setCurrentStep] = React.useState(-1)
+ const [steps, setSteps] = React.useState<TourStep[]>([])
+ const [isActive, setIsActive] = React.useState(false)
+ const [isTourCompleted, setIsTourCompleted] = React.useState(initialCompleted)
+
+ // Track expansion states
+ const [isMenuExpanded, setIsMenuExpanded] = React.useState(false)
+ const [isChatExpanded, setIsChatExpanded] = React.useState(false)
+
+ // Calculate if tour should be paused
+ const isPaused = React.useMemo(() => {
+ return isActive && (isMenuExpanded || isChatExpanded)
+ }, [isActive, isMenuExpanded, isChatExpanded])
+
+ const startTour = React.useCallback(() => {
+ console.debug("Starting tour with", steps.length, "steps")
+ analytics.tourStarted()
+ setCurrentStep(0)
+ setIsActive(true)
+ }, [steps])
+
+ const endTour = React.useCallback(() => {
+ setCurrentStep(-1)
+ setIsActive(false)
+ setIsTourCompleted(true) // Mark tour as completed when ended/skipped
+ analytics.tourSkipped()
+ if (onComplete) {
+ onComplete()
+ }
+ }, [onComplete])
+
+ const nextStep = React.useCallback(() => {
+ if (currentStep < steps.length - 1) {
+ setCurrentStep(currentStep + 1)
+ } else {
+ analytics.tourCompleted()
+ endTour()
+ setIsTourCompleted(true)
+ }
+ }, [currentStep, steps.length, endTour])
+
+ const previousStep = React.useCallback(() => {
+ if (currentStep > 0) {
+ setCurrentStep(currentStep - 1)
+ }
+ }, [currentStep])
+
+ const setMenuExpanded = React.useCallback((expanded: boolean) => {
+ setIsMenuExpanded(expanded)
+ }, [])
+
+ const setChatExpanded = React.useCallback((expanded: boolean) => {
+ setIsChatExpanded(expanded)
+ }, [])
+
+ const value = React.useMemo(
+ () => ({
+ currentStep,
+ totalSteps: steps.length,
+ nextStep,
+ previousStep,
+ endTour,
+ isActive,
+ isPaused,
+ startTour,
+ setSteps,
+ steps,
+ isTourCompleted,
+ setIsTourCompleted,
+ setMenuExpanded,
+ setChatExpanded,
+ }),
+ [
+ currentStep,
+ steps,
+ nextStep,
+ previousStep,
+ endTour,
+ isActive,
+ isPaused,
+ startTour,
+ isTourCompleted,
+ setMenuExpanded,
+ setChatExpanded,
+ ],
+ )
+
+ return (
+ <TourContext.Provider value={value}>
+ {children}
+ {isActive && !isPaused && (
+ <>
+ {console.log(
+ "Rendering TourHighlight for step:",
+ currentStep,
+ currentStep >= 0 && currentStep < steps.length
+ ? steps[currentStep]
+ : "No step",
+ )}
+ <TourHighlight
+ className={className}
+ currentStepIndex={currentStep}
+ steps={steps}
+ />
+ </>
+ )}
+ </TourContext.Provider>
+ )
+}
+
+// Tour Highlight Component
+function TourHighlight({
+ currentStepIndex,
+ steps,
+ className,
+}: {
+ currentStepIndex: number
+ steps: TourStep[]
+ className?: string
+}) {
+ const { nextStep, previousStep, endTour } = useTour()
+ const [elementRect, setElementRect] = React.useState<DOMRect | null>(null)
+
+ // Get current step safely
+ const step =
+ currentStepIndex >= 0 && currentStepIndex < steps.length
+ ? steps[currentStepIndex]
+ : null
+
+ React.useEffect(() => {
+ if (!step) return
+
+ // Use requestAnimationFrame to ensure DOM is ready
+ const rafId = requestAnimationFrame(() => {
+ const element = document.getElementById(step.selectorId)
+ console.debug(
+ "Looking for element with ID:",
+ step.selectorId,
+ "Found:",
+ !!element,
+ )
+ if (element) {
+ const rect = element.getBoundingClientRect()
+ console.debug("Element rect:", {
+ id: step.selectorId,
+ top: rect.top,
+ left: rect.left,
+ width: rect.width,
+ height: rect.height,
+ bottom: rect.bottom,
+ right: rect.right,
+ })
+ setElementRect(rect)
+ }
+ })
+
+ // Add click listener for onClickWithinArea
+ let clickHandler: ((e: MouseEvent) => void) | null = null
+ if (step.onClickWithinArea) {
+ const element = document.getElementById(step.selectorId)
+ if (element) {
+ clickHandler = (e: MouseEvent) => {
+ if (element.contains(e.target as Node)) {
+ step.onClickWithinArea?.()
+ }
+ }
+ document.addEventListener("click", clickHandler)
+ }
+ }
+
+ return () => {
+ cancelAnimationFrame(rafId)
+ if (clickHandler) {
+ document.removeEventListener("click", clickHandler)
+ }
+ }
+ }, [step])
+
+ if (!step) return null
+
+ // Keep the wrapper mounted but animate the content
+ return (
+ <AnimatePresence mode="wait">
+ {elementRect && (
+ <motion.div
+ animate={{ opacity: 1 }}
+ exit={{ opacity: 0 }}
+ initial={{ opacity: 0 }}
+ key={`tour-step-${currentStepIndex}`}
+ transition={{ duration: 0.2 }}
+ >
+ {/* Highlight Border */}
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ className={`fixed z-[101] pointer-events-none ${className}`}
+ exit={{ opacity: 0, scale: 0.95 }}
+ initial={{ opacity: 0, scale: 0.95 }}
+ style={{
+ top: elementRect.top + window.scrollY,
+ left: elementRect.left + window.scrollX,
+ width: elementRect.width,
+ height: elementRect.height,
+ }}
+ >
+ <div className="absolute inset-0 rounded-lg outline-4 outline-blue-500/50 outline-offset-0" />
+ </motion.div>
+
+ {/* Tooltip with Glass Effect */}
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="fixed z-[102] w-72 rounded-lg shadow-xl overflow-hidden"
+ exit={{ opacity: 0, y: step.position === "top" ? 10 : -10 }}
+ initial={{ opacity: 0, y: step.position === "top" ? 10 : -10 }}
+ style={{
+ top: (() => {
+ const baseTop =
+ step.position === "bottom"
+ ? elementRect.bottom + 8
+ : step.position === "top"
+ ? elementRect.top - 200
+ : elementRect.top + elementRect.height / 2 - 100
+
+ // Ensure tooltip stays within viewport
+ const maxTop = window.innerHeight - 250 // Leave space for tooltip height
+ const minTop = 10
+ return Math.max(minTop, Math.min(baseTop, maxTop))
+ })(),
+ left: (() => {
+ const baseLeft =
+ step.position === "right"
+ ? elementRect.right + 8
+ : step.position === "left"
+ ? elementRect.left - 300
+ : elementRect.left + elementRect.width / 2 - 150
+
+ // Ensure tooltip stays within viewport
+ const maxLeft = window.innerWidth - 300 // Tooltip width
+ const minLeft = 10
+ return Math.max(minLeft, Math.min(baseLeft, maxLeft))
+ })(),
+ }}
+ >
+ {/* Glass effect background */}
+ <GlassMenuEffect rounded="rounded-lg" />
+
+ {/* Content */}
+ <div className="relative z-10 p-4">
+ <div className="mb-4 text-white">{step.content}</div>
+ <div className="flex items-center justify-between">
+ <span className="text-sm text-gray-300">
+ {currentStepIndex + 1} / {steps.length}
+ </span>
+ <div className="flex gap-2">
+ <Button
+ className="border-white/20 text-white hover:bg-white/10"
+ onClick={endTour}
+ size="sm"
+ variant="outline"
+ >
+ Skip
+ </Button>
+ {currentStepIndex > 0 && (
+ <Button
+ className="border-white/20 text-white hover:bg-white/10"
+ onClick={previousStep}
+ size="sm"
+ variant="outline"
+ >
+ Previous
+ </Button>
+ )}
+ <Button
+ className="bg-white/20 text-white hover:bg-white/30"
+ onClick={nextStep}
+ size="sm"
+ >
+ {currentStepIndex === steps.length - 1 ? "Finish" : "Next"}
+ </Button>
+ </div>
+ </div>
+ </div>
+ </motion.div>
+ </motion.div>
+ )}
+ </AnimatePresence>
+ )
+}
+
+// Tour Alert Dialog
+interface TourAlertDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+}
+
+export function TourAlertDialog({ open, onOpenChange }: TourAlertDialogProps) {
+ const { startTour, setIsTourCompleted } = useTour()
+
+ const handleStart = () => {
+ console.debug("TourAlertDialog: Starting tour")
+ onOpenChange(false)
+ startTour()
+ }
+
+ const handleSkip = () => {
+ analytics.tourSkipped()
+ setIsTourCompleted(true) // Mark tour as completed when skipped
+ onOpenChange(false)
+ }
+
+ if (!open) return null
+
+ return (
+ <AnimatePresence>
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[102] w-[90vw] max-w-2xl rounded-lg shadow-xl overflow-hidden"
+ exit={{ opacity: 0, scale: 0.95 }}
+ initial={{ opacity: 0, scale: 0.95 }}
+ >
+ {/* Glass effect background */}
+ <GlassMenuEffect rounded="rounded-lg" />
+
+ {/* Content */}
+ <div className="relative z-10 p-8 md:p-10 lg:p-12">
+ <h2 className="text-3xl md:text-4xl font-bold text-white mb-4">
+ Welcome to supermemory™
+ </h2>
+ <p className="text-lg md:text-xl text-gray-200 mb-8 leading-relaxed">
+ This is your personal knowledge graph where all your memories are
+ stored and connected. Let's take a quick tour to help you get
+ familiar with the interface.
+ </p>
+ <div className="flex gap-4 justify-end">
+ <Button
+ className="border-white/20 text-white hover:bg-white/10 px-6 py-2 text-base"
+ onClick={handleSkip}
+ size="lg"
+ variant="outline"
+ >
+ Skip Tour
+ </Button>
+ <Button
+ className="bg-white/20 text-white hover:bg-white/30 px-6 py-2 text-base"
+ onClick={handleStart}
+ size="lg"
+ >
+ Start Tour
+ </Button>
+ </div>
+ </div>
+ </motion.div>
+ </AnimatePresence>
+ )
+}
diff --git a/apps/web/components/views/add-memory.tsx b/apps/web/components/views/add-memory.tsx
new file mode 100644
index 00000000..744faa35
--- /dev/null
+++ b/apps/web/components/views/add-memory.tsx
@@ -0,0 +1,1425 @@
+import { $fetch } from "@lib/api"
+import {
+ fetchConsumerProProduct,
+ fetchMemoriesFeature,
+} from "@repo/lib/queries"
+import { Button } from "@repo/ui/components/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@repo/ui/components/dialog"
+import { Input } from "@repo/ui/components/input"
+import { Label } from "@repo/ui/components/label"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@repo/ui/components/select"
+import {
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from "@repo/ui/components/tabs"
+import { Textarea } from "@repo/ui/components/textarea"
+import { useForm } from "@tanstack/react-form"
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
+import {
+ Dropzone,
+ DropzoneContent,
+ DropzoneEmptyState,
+} from "@ui/components/shadcn-io/dropzone"
+import { useCustomer } from "autumn-js/react"
+import {
+ Brain,
+ FileIcon,
+ Link as LinkIcon,
+ Loader2,
+ PlugIcon,
+ Plus,
+ UploadIcon,
+} from "lucide-react"
+import { AnimatePresence, motion } from "motion/react"
+import Link from "next/link"
+import { useEffect, useState } from "react"
+import { toast } from "sonner"
+import { z } from "zod"
+import { useProject } from "@/stores"
+import { ConnectionsTabContent } from "./connections-tab-content"
+import { analytics } from "@/lib/analytics"
+
+// // Processing status component
+// function ProcessingStatus({ status }: { status: string }) {
+// const statusConfig = {
+// queued: { color: "text-yellow-400", label: "Queued", icon: "⏳" },
+// extracting: { color: "text-blue-400", label: "Extracting", icon: "📤" },
+// chunking: { color: "text-indigo-400", label: "Chunking", icon: "✂️" },
+// embedding: { color: "text-purple-400", label: "Embedding", icon: "🧠" },
+// indexing: { color: "text-pink-400", label: "Indexing", icon: "📝" },
+// unknown: { color: "text-gray-400", label: "Processing", icon: "⚙️" },
+// }
+
+// const config =
+// statusConfig[status as keyof typeof statusConfig] || statusConfig.unknown
+
+// return (
+// <div className={`flex items-center gap-1 text-xs ${config.color}`}>
+// <span>{config.icon}</span>
+// <span>{config.label}</span>
+// </div>
+// )
+// }
+
+export function AddMemoryView({
+ onClose,
+ initialTab = "note",
+}: {
+ onClose?: () => void
+ initialTab?: "note" | "link" | "file" | "connect"
+}) {
+ const queryClient = useQueryClient()
+ const { selectedProject, setSelectedProject } = useProject()
+ const [showAddDialog, setShowAddDialog] = useState(true)
+ const [selectedFiles, setSelectedFiles] = useState<File[]>([])
+ const [activeTab, setActiveTab] = useState<
+ "note" | "link" | "file" | "connect"
+ >(initialTab)
+ const autumn = useCustomer()
+ const [showCreateProjectDialog, setShowCreateProjectDialog] = useState(false)
+ const [newProjectName, setNewProjectName] = useState("")
+
+ // Check memory limits
+ const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any)
+
+ const memoriesUsed = memoriesCheck?.usage ?? 0
+ const memoriesLimit = memoriesCheck?.included_usage ?? 0
+
+ // Check if user is pro
+ const { data: proCheck } = fetchConsumerProProduct(autumn as any)
+ const isProUser = proCheck?.allowed ?? false
+
+ const canAddMemory = memoriesUsed < memoriesLimit
+
+ // Fetch projects for the dropdown
+ const { data: projects = [], isLoading: isLoadingProjects } = useQuery({
+ queryKey: ["projects"],
+ queryFn: async () => {
+ const response = await $fetch("@get/projects")
+
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to load projects")
+ }
+
+ return response.data?.projects || []
+ },
+ staleTime: 30 * 1000,
+ })
+
+ // Create project mutation
+ const createProjectMutation = useMutation({
+ mutationFn: async (name: string) => {
+ const response = await $fetch("@post/projects", {
+ body: { name },
+ })
+
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to create project")
+ }
+
+ return response.data
+ },
+ onSuccess: (data) => {
+ analytics.projectCreated()
+ toast.success("Project created successfully!")
+ setShowCreateProjectDialog(false)
+ setNewProjectName("")
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
+ // Set the newly created project as selected
+ if (data?.containerTag) {
+ setSelectedProject(data.containerTag)
+ // Update form values
+ addContentForm.setFieldValue("project", data.containerTag)
+ fileUploadForm.setFieldValue("project", data.containerTag)
+ }
+ },
+ onError: (error) => {
+ toast.error("Failed to create project", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ })
+
+ const addContentForm = useForm({
+ defaultValues: {
+ content: "",
+ project: selectedProject || "sm_project_default",
+ },
+ onSubmit: async ({ value, formApi }) => {
+ addContentMutation.mutate({
+ content: value.content,
+ project: value.project,
+ contentType: activeTab as "note" | "link",
+ })
+ formApi.reset()
+ },
+ validators: {
+ onChange: z.object({
+ content: z.string().min(1, "Content is required"),
+ project: z.string(),
+ }),
+ },
+ })
+
+ // Re-validate content field when tab changes between note/link
+ // biome-ignore lint/correctness/useExhaustiveDependencies: It is what it is
+ useEffect(() => {
+ // Trigger validation of the content field when switching between note/link
+ if (activeTab === "note" || activeTab === "link") {
+ const currentValue = addContentForm.getFieldValue("content")
+ if (currentValue) {
+ addContentForm.validateField("content", "change")
+ }
+ }
+ }, [activeTab])
+
+ // Form for file upload metadata
+ const fileUploadForm = useForm({
+ defaultValues: {
+ title: "",
+ description: "",
+ project: selectedProject || "sm_project_default",
+ },
+ onSubmit: async ({ value, formApi }) => {
+ if (selectedFiles.length === 0) {
+ toast.error("Please select a file to upload")
+ return
+ }
+
+ for (const file of selectedFiles) {
+ fileUploadMutation.mutate({
+ file,
+ title: value.title || undefined,
+ description: value.description || undefined,
+ project: value.project,
+ })
+ }
+
+ formApi.reset()
+ setSelectedFiles([])
+ },
+ })
+
+ const handleUpgrade = async () => {
+ try {
+ await autumn.attach({
+ productId: "consumer_pro",
+ successUrl: "https://app.supermemory.ai/",
+ })
+ window.location.reload()
+ } catch (error) {
+ console.error(error)
+ }
+ }
+
+ const addContentMutation = useMutation({
+ mutationFn: async ({
+ content,
+ project,
+ contentType,
+ }: {
+ content: string
+ project: string
+ contentType: "note" | "link"
+ }) => {
+ // close the modal
+ onClose?.()
+
+ const processingPromise = (async () => {
+ // First, create the memory
+ const response = await $fetch("@post/memories", {
+ body: {
+ content: content,
+ containerTags: [project],
+ metadata: {
+ sm_source: "consumer", // Use "consumer" source to bypass limits
+ },
+ },
+ })
+
+ if (response.error) {
+ throw new Error(
+ response.error?.message || `Failed to add ${contentType}`,
+ )
+ }
+
+ const memoryId = response.data.id
+
+ // Polling function to check status
+ const pollForCompletion = async (): Promise<any> => {
+ let attempts = 0
+ const maxAttempts = 60 // Maximum 5 minutes (60 attempts * 5 seconds)
+
+ while (attempts < maxAttempts) {
+ try {
+ const memory = await $fetch<{ status: string; content: string }>(
+ "@get/memories/" + memoryId,
+ )
+
+ if (memory.error) {
+ throw new Error(
+ memory.error?.message || "Failed to fetch memory status",
+ )
+ }
+
+ // Check if processing is complete
+ // Adjust this condition based on your API response structure
+ if (
+ memory.data?.status === "done" ||
+ // Sometimes the memory might be ready when it has content and no processing status
+ memory.data?.content
+ ) {
+ return memory.data
+ }
+
+ // If still processing, wait and try again
+ await new Promise((resolve) => setTimeout(resolve, 5000)) // Wait 5 seconds
+ attempts++
+ } catch (error) {
+ console.error("Error polling memory status:", error)
+ // Don't throw immediately, retry a few times
+ if (attempts >= 3) {
+ throw new Error("Failed to check processing status")
+ }
+ await new Promise((resolve) => setTimeout(resolve, 5000))
+ attempts++
+ }
+ }
+
+ // If we've exceeded max attempts, throw an error
+ throw new Error("Memory processing timed out. Please check back later.")
+ }
+
+ // Wait for completion
+ const completedMemory = await pollForCompletion()
+ return completedMemory
+ })()
+
+ toast.promise(processingPromise, {
+ loading: "Processing...",
+ success: `${contentType === "link" ? "Link" : "Note"} created successfully!`,
+ error: (err) => `Failed to add ${contentType}: ${err instanceof Error ? err.message : "Unknown error"}`,
+ })
+
+ return processingPromise
+ },
+ onMutate: async ({ content, project, contentType }) => {
+ console.log("🚀 onMutate starting...")
+
+ // Cancel any outgoing refetches
+ await queryClient.cancelQueries({ queryKey: ["documents-with-memories", project] })
+ console.log("✅ Cancelled queries")
+
+ // Snapshot the previous value
+ const previousMemories = queryClient.getQueryData([
+ "documents-with-memories",
+ project,
+ ])
+ console.log("📸 Previous memories:", previousMemories)
+
+ // Create optimistic memory
+ const optimisticMemory = {
+ id: `temp-${Date.now()}`,
+ content: contentType === "link" ? "" : content,
+ url: contentType === "link" ? content : null,
+ title:
+ contentType === "link" ? "Processing..." : content.substring(0, 100),
+ description: contentType === "link" ? "Extracting content..." : "Processing content...",
+ containerTags: [project],
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ status: "queued",
+ type: contentType,
+ metadata: {
+ processingStage: "queued",
+ processingMessage: "Added to processing queue"
+ },
+ memoryEntries: [],
+ isOptimistic: true,
+ }
+ console.log("🎯 Created optimistic memory:", optimisticMemory)
+
+ // Optimistically update to include the new memory
+ queryClient.setQueryData(["documents-with-memories", project], (old: any) => {
+ console.log("🔄 Old data:", old)
+ const newData = old
+ ? {
+ ...old,
+ documents: [optimisticMemory, ...(old.documents || [])],
+ totalCount: (old.totalCount || 0) + 1,
+ }
+ : { documents: [optimisticMemory], totalCount: 1 }
+ console.log("✨ New data:", newData)
+ return newData
+ })
+
+ console.log("✅ onMutate completed")
+ return { previousMemories, optimisticId: optimisticMemory.id }
+ },
+ // If the mutation fails, roll back to the previous value
+ onError: (error, variables, context) => {
+ if (context?.previousMemories) {
+ queryClient.setQueryData(
+ ["documents-with-memories", variables.project],
+ context.previousMemories,
+ )
+ }
+ },
+ onSuccess: (_data, variables) => {
+ analytics.memoryAdded({
+ type: variables.contentType === "link" ? "link" : "note",
+ project_id: variables.project,
+ content_length: variables.content.length,
+ })
+
+ queryClient.invalidateQueries({ queryKey: ["documents-with-memories", variables.project] })
+
+ setTimeout(() => {
+ queryClient.invalidateQueries({ queryKey: ["documents-with-memories", variables.project] })
+ }, 30000) // 30 seconds
+
+ setTimeout(() => {
+ queryClient.invalidateQueries({ queryKey: ["documents-with-memories", variables.project] })
+ }, 120000) // 2 minutes
+
+ setShowAddDialog(false)
+ onClose?.()
+ },
+ })
+
+ const fileUploadMutation = useMutation({
+ mutationFn: async ({
+ file,
+ title,
+ description,
+ project,
+ }: {
+ file: File
+ title?: string
+ description?: string
+ project: string
+ }) => {
+ // TEMPORARILY DISABLED: Limit check disabled
+ // Check if user can add more memories
+ // if (!canAddMemory && !isProUser) {
+ // throw new Error(
+ // `Free plan limit reached (${memoriesLimit} memories). Upgrade to Pro for up to 500 memories.`,
+ // );
+ // }
+
+ const formData = new FormData()
+ formData.append("file", file)
+ formData.append("containerTags", JSON.stringify([project]))
+
+ const response = await fetch(
+ `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/memories/file`,
+ {
+ method: "POST",
+ body: formData,
+ credentials: "include",
+ },
+ )
+
+ if (!response.ok) {
+ const error = await response.json()
+ throw new Error(error.error || "Failed to upload file")
+ }
+
+ const data = await response.json()
+
+ // If we have metadata, we can update the document after creation
+ if (title || description) {
+ await $fetch(`@patch/memories/${data.id}`, {
+ body: {
+ metadata: {
+ ...(title && { title }),
+ ...(description && { description }),
+ sm_source: "consumer", // Use "consumer" source to bypass limits
+ },
+ },
+ })
+ }
+
+ return data
+ },
+ // Optimistic update
+ onMutate: async ({ file, title, description, project }) => {
+ // Cancel any outgoing refetches
+ await queryClient.cancelQueries({ queryKey: ["documents-with-memories", project] })
+
+ // Snapshot the previous value
+ const previousMemories = queryClient.getQueryData([
+ "documents-with-memories",
+ project,
+ ])
+
+ // Create optimistic memory for the file
+ const optimisticMemory = {
+ id: `temp-file-${Date.now()}`,
+ content: "",
+ url: null,
+ title: title || file.name,
+ description: description || `Uploading ${file.name}...`,
+ containerTags: [project],
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ status: "processing",
+ type: "file",
+ metadata: {
+ fileName: file.name,
+ fileSize: file.size,
+ mimeType: file.type,
+ },
+ memoryEntries: [],
+ }
+
+ // Optimistically update to include the new memory
+ queryClient.setQueryData(["documents-with-memories", project], (old: any) => {
+ if (!old) return { documents: [optimisticMemory], totalCount: 1 }
+ return {
+ ...old,
+ documents: [optimisticMemory, ...(old.documents || [])],
+ totalCount: (old.totalCount || 0) + 1,
+ }
+ })
+
+ // Return a context object with the snapshotted value
+ return { previousMemories }
+ },
+ // If the mutation fails, roll back to the previous value
+ onError: (error, variables, context) => {
+ if (context?.previousMemories) {
+ queryClient.setQueryData(
+ ["documents-with-memories", variables.project],
+ context.previousMemories,
+ )
+ }
+ toast.error("Failed to upload file", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ onSuccess: (_data, variables) => {
+ analytics.memoryAdded({
+ type: "file",
+ project_id: variables.project,
+ file_size: variables.file.size,
+ file_type: variables.file.type,
+ })
+ toast.success("File uploaded successfully!", {
+ description: "Your file is being processed",
+ })
+ setShowAddDialog(false)
+ onClose?.()
+ },
+ // Always refetch after error or success
+ onSettled: () => {
+ queryClient.invalidateQueries({ queryKey: ["documents-with-memories"] })
+ },
+ })
+
+ return (
+ <AnimatePresence mode="wait">
+ {showAddDialog && (
+ <Dialog
+ key="add-memory-dialog"
+ onOpenChange={(open) => {
+ setShowAddDialog(open)
+ if (!open) onClose?.()
+ }}
+ open={showAddDialog}
+ >
+ <DialogContent className="sm:max-w-3xl bg-[#0f1419] backdrop-blur-xl border-white/10 text-white z-[80]">
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.95 }}
+ initial={{ opacity: 0, scale: 0.95 }}
+ >
+ <DialogHeader>
+ <DialogTitle>Add to Memory</DialogTitle>
+ <DialogDescription className="text-white/60">
+ Save any webpage, article, or file to your memory
+ </DialogDescription>
+ {
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="mt-2"
+ initial={{ opacity: 0, y: -10 }}
+ >
+ <div className="text-xs text-white/50">
+ {memoriesUsed} of {memoriesLimit} memories used
+ {!isProUser && memoriesUsed >= memoriesLimit * 0.8 && (
+ <span className="text-yellow-400 ml-2">
+ • {memoriesLimit - memoriesUsed} remaining
+ </span>
+ )}
+ </div>
+ {!canAddMemory && !isProUser && (
+ <motion.div
+ animate={{ opacity: 1, height: "auto" }}
+ className="mt-2 p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg"
+ initial={{ opacity: 0, height: 0 }}
+ >
+ <p className="text-sm text-yellow-400">
+ You've reached the free plan limit.
+ <Button
+ asChild
+ className="text-yellow-400 hover:text-yellow-300 px-2"
+ onClick={handleUpgrade}
+ size="sm"
+ variant="link"
+ >
+ Upgrade to Pro
+ </Button>
+ for up to 5000 memories.
+ </p>
+ </motion.div>
+ )}
+ </motion.div>
+ }
+ </DialogHeader>
+
+ <Tabs
+ className="mt-4"
+ onValueChange={(v) =>
+ setActiveTab(v as "note" | "link" | "file" | "connect")
+ }
+ value={activeTab}
+ >
+ <TabsList className="grid w-full grid-cols-4 bg-white/5">
+ <TabsTrigger
+ className="data-[state=active]:bg-white/10"
+ value="note"
+ >
+ <Brain className="h-4 w-4 mr-2" />
+ Note
+ </TabsTrigger>
+ <TabsTrigger
+ className="data-[state=active]:bg-white/10"
+ value="link"
+ >
+ <LinkIcon className="h-4 w-4 mr-2" />
+ Link
+ </TabsTrigger>
+ <TabsTrigger
+ className="data-[state=active]:bg-white/10"
+ value="file"
+ >
+ <FileIcon className="h-4 w-4 mr-2" />
+ File
+ </TabsTrigger>
+ <TabsTrigger
+ className="data-[state=active]:bg-white/10"
+ value="connect"
+ >
+ <PlugIcon className="h-4 w-4 mr-2" />
+ Connect
+ </TabsTrigger>
+ </TabsList>
+
+ <TabsContent className="space-y-4 mt-4" value="note">
+ <form
+ onSubmit={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ addContentForm.handleSubmit()
+ }}
+ >
+ <div className="grid gap-4">
+ {/* Note Input */}
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex flex-col gap-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.1 }}
+ >
+ <label
+ className="text-sm font-medium"
+ htmlFor="note-content"
+ >
+ Note
+ </label>
+ <addContentForm.Field
+ name="content"
+ validators={{
+ onChange: ({ value }) => {
+ if (!value || value.trim() === "") {
+ return "Note is required"
+ }
+ return undefined
+ },
+ }}
+ >
+ {({ state, handleChange, handleBlur }) => (
+ <>
+ <Textarea
+ className={`bg-white/5 border-white/10 text-white min-h-32 max-h-64 overflow-y-auto resize-none ${addContentMutation.isPending
+ ? "opacity-50"
+ : ""
+ }`}
+ disabled={addContentMutation.isPending}
+ id="note-content"
+ onBlur={handleBlur}
+ onChange={(e) => handleChange(e.target.value)}
+ placeholder="Write your note here..."
+ value={state.value}
+ />
+ {state.meta.errors.length > 0 && (
+ <motion.p
+ animate={{ opacity: 1, height: "auto" }}
+ className="text-sm text-red-400 mt-1"
+ exit={{ opacity: 0, height: 0 }}
+ initial={{ opacity: 0, height: 0 }}
+ >
+ {state.meta.errors
+ .map((error) =>
+ typeof error === "string"
+ ? error
+ : (error?.message ??
+ `Error: ${JSON.stringify(error)}`),
+ )
+ .join(", ")}
+ </motion.p>
+ )}
+ </>
+ )}
+ </addContentForm.Field>
+ </motion.div>
+
+ {/* Project Selection */}
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className={`flex flex-col gap-2 ${addContentMutation.isPending ? "opacity-50" : ""
+ }`}
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.15 }}
+ >
+ <label
+ className="text-sm font-medium"
+ htmlFor="note-project"
+ >
+ Project
+ </label>
+ <addContentForm.Field name="project">
+ {({ state, handleChange }) => (
+ <Select
+ disabled={
+ isLoadingProjects ||
+ addContentMutation.isPending
+ }
+ onValueChange={(value) => {
+ if (value === "create-new-project") {
+ setShowCreateProjectDialog(true)
+ } else {
+ handleChange(value)
+ }
+ }}
+ value={state.value}
+ >
+ <SelectTrigger
+ className="bg-white/5 border-white/10 text-white"
+ id="note-project"
+ >
+ <SelectValue placeholder="Select a project" />
+ </SelectTrigger>
+ <SelectContent className="bg-black/90 backdrop-blur-xl border-white/10">
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ key="default"
+ value="sm_project_default"
+ >
+ Default Project
+ </SelectItem>
+ {projects
+ .filter(
+ (p) =>
+ p.containerTag !== "sm_project_default" &&
+ p.id,
+ )
+ .map((project) => (
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ key={project.id || project.containerTag}
+ value={project.containerTag}
+ >
+ {project.name}
+ </SelectItem>
+ ))}
+ <SelectItem
+ className="text-white hover:bg-white/10 border-t border-white/10 mt-1"
+ key="create-new"
+ value="create-new-project"
+ >
+ <div className="flex items-center gap-2">
+ <Plus className="h-4 w-4" />
+ <span>Create new project</span>
+ </div>
+ </SelectItem>
+ </SelectContent>
+ </Select>
+ )}
+ </addContentForm.Field>
+ <p className="text-xs text-white/50 mt-1">
+ Choose which project to save this note to
+ </p>
+ </motion.div>
+ </div>
+ <DialogFooter className="mt-6">
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
+ onClick={() => {
+ setShowAddDialog(false)
+ onClose?.()
+ addContentForm.reset()
+ }}
+ type="button"
+ variant="outline"
+ >
+ Cancel
+ </Button>
+ </motion.div>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ disabled={
+ addContentMutation.isPending ||
+ !addContentForm.state.canSubmit
+ }
+ type="submit"
+ >
+ {addContentMutation.isPending ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ Adding...
+ </>
+ ) : (
+ <>
+ <Plus className="h-4 w-4 mr-2" />
+ Add Note
+ </>
+ )}
+ </Button>
+ </motion.div>
+ </DialogFooter>
+ </form>
+ </TabsContent>
+
+ <TabsContent className="space-y-4 mt-4" value="link">
+ <form
+ onSubmit={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ addContentForm.handleSubmit()
+ }}
+ >
+ <div className="grid gap-4">
+ {/* Link Input */}
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex flex-col gap-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.1 }}
+ >
+ <label
+ className="text-sm font-medium"
+ htmlFor="link-content"
+ >
+ Link
+ </label>
+ <addContentForm.Field
+ name="content"
+ validators={{
+ onChange: ({ value }) => {
+ if (!value || value.trim() === "") {
+ return "Link is required"
+ }
+ try {
+ new URL(value)
+ return undefined
+ } catch {
+ return "Please enter a valid link"
+ }
+ },
+ }}
+ >
+ {({ state, handleChange, handleBlur }) => (
+ <>
+ <Input
+ className={`bg-white/5 border-white/10 text-white ${addContentMutation.isPending
+ ? "opacity-50"
+ : ""
+ }`}
+ disabled={addContentMutation.isPending}
+ id="link-content"
+ onBlur={handleBlur}
+ onChange={(e) => handleChange(e.target.value)}
+ placeholder="https://example.com/article"
+ value={state.value}
+ />
+ {state.meta.errors.length > 0 && (
+ <motion.p
+ animate={{ opacity: 1, height: "auto" }}
+ className="text-sm text-red-400 mt-1"
+ exit={{ opacity: 0, height: 0 }}
+ initial={{ opacity: 0, height: 0 }}
+ >
+ {state.meta.errors
+ .map((error) =>
+ typeof error === "string"
+ ? error
+ : (error?.message ??
+ `Error: ${JSON.stringify(error)}`),
+ )
+ .join(", ")}
+ </motion.p>
+ )}
+ </>
+ )}
+ </addContentForm.Field>
+ </motion.div>
+
+ {/* Project Selection */}
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className={`flex flex-col gap-2 ${addContentMutation.isPending ? "opacity-50" : ""
+ }`}
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.15 }}
+ >
+ <label
+ className="text-sm font-medium"
+ htmlFor="link-project"
+ >
+ Project
+ </label>
+ <addContentForm.Field name="project">
+ {({ state, handleChange }) => (
+ <Select
+ disabled={
+ isLoadingProjects ||
+ addContentMutation.isPending
+ }
+ onValueChange={(value) => {
+ if (value === "create-new-project") {
+ setShowCreateProjectDialog(true)
+ } else {
+ handleChange(value)
+ }
+ }}
+ value={state.value}
+ >
+ <SelectTrigger
+ className="bg-white/5 border-white/10 text-white"
+ id="link-project"
+ >
+ <SelectValue placeholder="Select a project" />
+ </SelectTrigger>
+ <SelectContent className="bg-black/90 backdrop-blur-xl border-white/10">
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ key="default"
+ value="sm_project_default"
+ >
+ Default Project
+ </SelectItem>
+ {projects
+ .filter(
+ (p) =>
+ p.containerTag !== "sm_project_default" &&
+ p.id,
+ )
+ .map((project) => (
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ key={project.id || project.containerTag}
+ value={project.containerTag}
+ >
+ {project.name}
+ </SelectItem>
+ ))}
+ <SelectItem
+ className="text-white hover:bg-white/10 border-t border-white/10 mt-1"
+ key="create-new"
+ value="create-new-project"
+ >
+ <div className="flex items-center gap-2">
+ <Plus className="h-4 w-4" />
+ <span>Create new project</span>
+ </div>
+ </SelectItem>
+ </SelectContent>
+ </Select>
+ )}
+ </addContentForm.Field>
+ <p className="text-xs text-white/50 mt-1">
+ Choose which project to save this link to
+ </p>
+ </motion.div>
+ </div>
+ <DialogFooter className="mt-6">
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
+ onClick={() => {
+ setShowAddDialog(false)
+ onClose?.()
+ addContentForm.reset()
+ }}
+ type="button"
+ variant="outline"
+ >
+ Cancel
+ </Button>
+ </motion.div>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ disabled={
+ addContentMutation.isPending ||
+ !addContentForm.state.canSubmit
+ }
+ type="submit"
+ >
+ {addContentMutation.isPending ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ Adding...
+ </>
+ ) : (
+ <>
+ <Plus className="h-4 w-4 mr-2" />
+ Add Link
+ </>
+ )}
+ </Button>
+ </motion.div>
+ </DialogFooter>
+ </form>
+ </TabsContent>
+
+ <TabsContent className="space-y-4 mt-4" value="file">
+ <form
+ onSubmit={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ fileUploadForm.handleSubmit()
+ }}
+ >
+ <div className="grid gap-4">
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex flex-col gap-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.1 }}
+ >
+ <label className="text-sm font-medium" htmlFor="file">
+ File
+ </label>
+ <Dropzone
+ accept={{
+ "application/pdf": [".pdf"],
+ "application/msword": [".doc"],
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
+ [".docx"],
+ "text/plain": [".txt"],
+ "text/markdown": [".md"],
+ "text/csv": [".csv"],
+ "application/json": [".json"],
+ "image/*": [
+ ".png",
+ ".jpg",
+ ".jpeg",
+ ".gif",
+ ".webp",
+ ],
+ }}
+ className="bg-white/5 border-white/10 hover:bg-white/10 min-h-40"
+ maxFiles={10}
+ maxSize={10 * 1024 * 1024} // 10MB
+ onDrop={(acceptedFiles) =>
+ setSelectedFiles(acceptedFiles)
+ }
+ src={selectedFiles}
+ >
+ <DropzoneEmptyState />
+ <DropzoneContent className="overflow-auto" />
+ </Dropzone>
+ </motion.div>
+
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex flex-col gap-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.15 }}
+ >
+ <label
+ className="text-sm font-medium"
+ htmlFor="file-title"
+ >
+ Title (optional)
+ </label>
+ <fileUploadForm.Field name="title">
+ {({ state, handleChange, handleBlur }) => (
+ <Input
+ className="bg-white/5 border-white/10 text-white"
+ id="file-title"
+ onBlur={handleBlur}
+ onChange={(e) => handleChange(e.target.value)}
+ placeholder="Give this file a title"
+ value={state.value}
+ />
+ )}
+ </fileUploadForm.Field>
+ </motion.div>
+
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex flex-col gap-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.2 }}
+ >
+ <label
+ className="text-sm font-medium"
+ htmlFor="file-description"
+ >
+ Description (optional)
+ </label>
+ <fileUploadForm.Field name="description">
+ {({ state, handleChange, handleBlur }) => (
+ <Textarea
+ className="bg-white/5 border-white/10 text-white min-h-20 max-h-40 overflow-y-auto resize-none"
+ id="file-description"
+ onBlur={handleBlur}
+ onChange={(e) => handleChange(e.target.value)}
+ placeholder="Add notes or context about this file"
+ value={state.value}
+ />
+ )}
+ </fileUploadForm.Field>
+ </motion.div>
+
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex flex-col gap-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.25 }}
+ >
+ <label
+ className="text-sm font-medium"
+ htmlFor="file-project"
+ >
+ Project
+ </label>
+ <fileUploadForm.Field name="project">
+ {({ state, handleChange }) => (
+ <Select
+ disabled={isLoadingProjects}
+ onValueChange={(value) => {
+ if (value === "create-new-project") {
+ setShowCreateProjectDialog(true)
+ } else {
+ handleChange(value)
+ }
+ }}
+ value={state.value}
+ >
+ <SelectTrigger
+ className="bg-white/5 border-white/10 text-white"
+ id="file-project"
+ >
+ <SelectValue placeholder="Select a project" />
+ </SelectTrigger>
+ <SelectContent className="bg-black/90 backdrop-blur-xl border-white/10">
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ key="default"
+ value="sm_project_default"
+ >
+ Default Project
+ </SelectItem>
+ {projects
+ .filter(
+ (p) =>
+ p.containerTag !== "sm_project_default" &&
+ p.id,
+ )
+ .map((project) => (
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ key={project.id || project.containerTag}
+ value={project.containerTag}
+ >
+ {project.name}
+ </SelectItem>
+ ))}
+ <SelectItem
+ className="text-white hover:bg-white/10 border-t border-white/10 mt-1"
+ key="create-new"
+ value="create-new-project"
+ >
+ <div className="flex items-center gap-2">
+ <Plus className="h-4 w-4" />
+ <span>Create new project</span>
+ </div>
+ </SelectItem>
+ </SelectContent>
+ </Select>
+ )}
+ </fileUploadForm.Field>
+ <p className="text-xs text-white/50 mt-1">
+ Choose which project to save this file to
+ </p>
+ </motion.div>
+ </div>
+ <DialogFooter className="mt-6">
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
+ onClick={() => {
+ setShowAddDialog(false)
+ onClose?.()
+ fileUploadForm.reset()
+ setSelectedFiles([])
+ }}
+ type="button"
+ variant="outline"
+ >
+ Cancel
+ </Button>
+ </motion.div>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ disabled={
+ fileUploadMutation.isPending ||
+ selectedFiles.length === 0
+ }
+ type="submit"
+ >
+ {fileUploadMutation.isPending ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ Uploading...
+ </>
+ ) : (
+ <>
+ <UploadIcon className="h-4 w-4 mr-2" />
+ Upload File
+ </>
+ )}
+ </Button>
+ </motion.div>
+ </DialogFooter>
+ </form>
+ </TabsContent>
+
+ <TabsContent className="space-y-4 mt-4" value="connect">
+ <ConnectionsTabContent />
+ </TabsContent>
+ </Tabs>
+ </motion.div>
+ </DialogContent>
+ </Dialog>
+ )}
+
+ {/* Create Project Dialog */}
+ {showCreateProjectDialog && (
+ <Dialog
+ key="create-project-dialog"
+ onOpenChange={setShowCreateProjectDialog}
+ open={showCreateProjectDialog}
+ >
+ <DialogContent className="sm:max-w-2xl bg-black/90 backdrop-blur-xl border-white/10 text-white z-[80]">
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ initial={{ opacity: 0, scale: 0.95 }}
+ >
+ <DialogHeader>
+ <DialogTitle>Create New Project</DialogTitle>
+ <DialogDescription className="text-white/60">
+ Give your project a unique name
+ </DialogDescription>
+ </DialogHeader>
+ <div className="grid gap-4 py-4">
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex flex-col gap-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.1 }}
+ >
+ <Label htmlFor="projectName">Project Name</Label>
+ <Input
+ className="bg-white/5 border-white/10 text-white"
+ id="projectName"
+ onChange={(e) => setNewProjectName(e.target.value)}
+ placeholder="My Awesome Project"
+ value={newProjectName}
+ />
+ <p className="text-xs text-white/50">
+ This will help you organize your memories
+ </p>
+ </motion.div>
+ </div>
+ <DialogFooter>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
+ onClick={() => {
+ setShowCreateProjectDialog(false)
+ setNewProjectName("")
+ }}
+ type="button"
+ variant="outline"
+ >
+ Cancel
+ </Button>
+ </motion.div>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ disabled={
+ createProjectMutation.isPending || !newProjectName.trim()
+ }
+ onClick={() => createProjectMutation.mutate(newProjectName)}
+ type="button"
+ >
+ {createProjectMutation.isPending ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ Creating...
+ </>
+ ) : (
+ "Create Project"
+ )}
+ </Button>
+ </motion.div>
+ </DialogFooter>
+ </motion.div>
+ </DialogContent>
+ </Dialog>
+ )}
+ </AnimatePresence>
+ )
+}
+
+export function AddMemoryExpandedView() {
+ const [showDialog, setShowDialog] = useState(false)
+ const [selectedTab, setSelectedTab] = useState<
+ "note" | "link" | "file" | "connect"
+ >("note")
+
+ const handleOpenDialog = (tab: "note" | "link" | "file" | "connect") => {
+ setSelectedTab(tab)
+ setShowDialog(true)
+ }
+
+ return (
+ <>
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="space-y-6"
+ initial={{ opacity: 0, y: 10 }}
+ >
+ <p className="text-sm text-white/70">
+ Save any webpage, article, or file to your memory
+ </p>
+
+ <div className="flex flex-wrap gap-2">
+ <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ onClick={() => handleOpenDialog("note")}
+ size="sm"
+ variant="outline"
+ >
+ <Brain className="h-4 w-4 mr-2" />
+ Note
+ </Button>
+ </motion.div>
+
+ <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ onClick={() => handleOpenDialog("link")}
+ size="sm"
+ variant="outline"
+ >
+ <LinkIcon className="h-4 w-4 mr-2" />
+ Link
+ </Button>
+ </motion.div>
+
+ <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ onClick={() => handleOpenDialog("file")}
+ size="sm"
+ variant="outline"
+ >
+ <FileIcon className="h-4 w-4 mr-2" />
+ File
+ </Button>
+ </motion.div>
+
+ <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ onClick={() => handleOpenDialog("connect")}
+ size="sm"
+ variant="outline"
+ >
+ <PlugIcon className="h-4 w-4 mr-2" />
+ Connect
+ </Button>
+ </motion.div>
+ </div>
+ </motion.div>
+
+ {showDialog && (
+ <AddMemoryView
+ initialTab={selectedTab}
+ onClose={() => setShowDialog(false)}
+ />
+ )}
+ </>
+ )
+}
diff --git a/apps/web/components/views/billing.tsx b/apps/web/components/views/billing.tsx
new file mode 100644
index 00000000..b0c436bb
--- /dev/null
+++ b/apps/web/components/views/billing.tsx
@@ -0,0 +1,261 @@
+import { useAuth } from "@lib/auth-context"
+import {
+ fetchConnectionsFeature,
+ fetchMemoriesFeature,
+ fetchSubscriptionStatus,
+} from "@lib/queries"
+import { Button } from "@ui/components/button"
+import { HeadingH3Bold } from "@ui/text/heading/heading-h3-bold"
+import { useCustomer } from "autumn-js/react"
+import { CheckCircle, LoaderIcon, X } from "lucide-react"
+import { motion } from "motion/react"
+import Link from "next/link"
+import { useState, useEffect } from "react"
+import { analytics } from "@/lib/analytics"
+
+export function BillingView() {
+ const autumn = useCustomer()
+ const { user } = useAuth()
+ const [isLoading, setIsLoading] = useState(false)
+
+ useEffect(() => {
+ analytics.billingViewed()
+ }, [])
+
+ const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any)
+
+ const memoriesUsed = memoriesCheck?.usage ?? 0
+ const memoriesLimit = memoriesCheck?.included_usage ?? 0
+
+ const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any)
+
+ const connectionsUsed = connectionsCheck?.usage ?? 0
+
+ // Fetch subscription status with React Query
+ const {
+ data: status = {
+ consumer_pro: null,
+ },
+ isLoading: isCheckingStatus,
+ } = fetchSubscriptionStatus(autumn as any)
+
+ // Handle upgrade
+ const handleUpgrade = async () => {
+ analytics.upgradeInitiated()
+ setIsLoading(true)
+ try {
+ await autumn.attach({
+ productId: "consumer_pro",
+ successUrl: "https://app.supermemory.ai/",
+ })
+ analytics.upgradeCompleted()
+ window.location.reload()
+ } catch (error) {
+ console.error(error)
+ setIsLoading(false)
+ }
+ }
+
+ // Handle manage billing
+ const handleManageBilling = async () => {
+ analytics.billingPortalOpened()
+ await autumn.openBillingPortal({
+ returnUrl: "https://app.supermemory.ai",
+ })
+ }
+
+ const isPro = status.consumer_pro
+
+ if (user?.isAnonymous) {
+ return (
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ className="text-center py-8"
+ initial={{ opacity: 0, scale: 0.9 }}
+ transition={{ type: "spring", damping: 20 }}
+ >
+ <p className="text-white/70 mb-4">Sign in to unlock premium features</p>
+ <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
+ <Button
+ asChild
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ size="sm"
+ >
+ <Link href="/login">Sign in</Link>
+ </Button>
+ </motion.div>
+ </motion.div>
+ )
+ }
+
+ if (isPro) {
+ return (
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="space-y-6"
+ initial={{ opacity: 0, y: 10 }}
+ >
+ <div className="space-y-3">
+ <HeadingH3Bold className="text-white flex items-center gap-2">
+ Pro Plan
+ <span className="text-xs bg-green-500/20 text-green-400 px-2 py-0.5 rounded-full">
+ Active
+ </span>
+ </HeadingH3Bold>
+ <p className="text-sm text-white/70">
+ You're enjoying expanded memory capacity with supermemory Pro!
+ </p>
+ </div>
+
+ {/* Current Usage */}
+ <div className="space-y-3">
+ <h4 className="text-sm font-medium text-white/90">Current Usage</h4>
+ <div className="space-y-2">
+ <div className="flex justify-between items-center">
+ <span className="text-sm text-white/70">Memories</span>
+ <span className="text-sm text-white/90">
+ {memoriesUsed} / {memoriesLimit}
+ </span>
+ </div>
+ <div className="w-full bg-white/10 rounded-full h-2">
+ <div
+ className="bg-green-500 h-2 rounded-full transition-all"
+ style={{
+ width: `${Math.min((memoriesUsed / memoriesLimit) * 100, 100)}%`,
+ }}
+ />
+ </div>
+ </div>
+ <div className="space-y-2">
+ <div className="flex justify-between items-center">
+ <span className="text-sm text-white/70">Connections</span>
+ <span className="text-sm text-white/90">
+ {connectionsUsed} / 10
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ onClick={handleManageBilling}
+ size="sm"
+ variant="outline"
+ >
+ Manage Billing
+ </Button>
+ </motion.div>
+ </motion.div>
+ )
+ }
+
+ return (
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="space-y-6"
+ initial={{ opacity: 0, y: 10 }}
+ >
+ {/* Current Usage - Free Plan */}
+ <div className="space-y-3">
+ <HeadingH3Bold className="text-white">Current Plan: Free</HeadingH3Bold>
+ <div className="space-y-2">
+ <div className="flex justify-between items-center">
+ <span className="text-sm text-white/70">Memories</span>
+ <span
+ className={`text-sm ${memoriesUsed >= memoriesLimit ? "text-red-400" : "text-white/90"}`}
+ >
+ {memoriesUsed} / {memoriesLimit}
+ </span>
+ </div>
+ <div className="w-full bg-white/10 rounded-full h-2">
+ <div
+ className={`h-2 rounded-full transition-all ${
+ memoriesUsed >= memoriesLimit ? "bg-red-500" : "bg-blue-500"
+ }`}
+ style={{
+ width: `${Math.min((memoriesUsed / memoriesLimit) * 100, 100)}%`,
+ }}
+ />
+ </div>
+ </div>
+ </div>
+
+ {/* Comparison */}
+ <div className="space-y-4">
+ <HeadingH3Bold className="text-white">Upgrade to Pro</HeadingH3Bold>
+
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ {/* Free Plan */}
+ <div className="p-4 bg-white/5 rounded-lg border border-white/10">
+ <h4 className="font-medium text-white/90 mb-3">Free Plan</h4>
+ <ul className="space-y-2">
+ <li className="flex items-center gap-2 text-sm text-white/70">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ 200 memories
+ </li>
+ <li className="flex items-center gap-2 text-sm text-white/70">
+ <X className="h-4 w-4 text-red-400" />
+ No connections
+ </li>
+ <li className="flex items-center gap-2 text-sm text-white/70">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ Basic search
+ </li>
+ </ul>
+ </div>
+
+ {/* Pro Plan */}
+ <div className="p-4 bg-gradient-to-br from-blue-500/10 to-purple-500/10 rounded-lg border border-blue-500/20">
+ <h4 className="font-medium text-white mb-3 flex items-center gap-2">
+ Pro Plan
+ <span className="text-xs bg-blue-500/20 text-blue-400 px-2 py-0.5 rounded-full">
+ Recommended
+ </span>
+ </h4>
+ <ul className="space-y-2">
+ <li className="flex items-center gap-2 text-sm text-white/90">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ 5000 memories
+ </li>
+ <li className="flex items-center gap-2 text-sm text-white/90">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ 10 connections
+ </li>
+ <li className="flex items-center gap-2 text-sm text-white/90">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ Advanced search
+ </li>
+ <li className="flex items-center gap-2 text-sm text-white/90">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ Priority support
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
+ <Button
+ className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white border-0 w-full"
+ disabled={isLoading || isCheckingStatus}
+ onClick={handleUpgrade}
+ size="sm"
+ >
+ {isLoading || isCheckingStatus ? (
+ <>
+ <LoaderIcon className="h-4 w-4 animate-spin mr-2" />
+ Upgrading...
+ </>
+ ) : (
+ <div>Upgrade to Pro - $15/month (only for first 100 users)</div>
+ )}
+ </Button>
+ </motion.div>
+
+ <p className="text-xs text-white/50 text-center">
+ Cancel anytime. No questions asked.
+ </p>
+ </div>
+ </motion.div>
+ )
+}
diff --git a/apps/web/components/views/chat/chat-messages.tsx b/apps/web/components/views/chat/chat-messages.tsx
new file mode 100644
index 00000000..0517ac8d
--- /dev/null
+++ b/apps/web/components/views/chat/chat-messages.tsx
@@ -0,0 +1,322 @@
+'use client'
+
+import { useChat, useCompletion } from '@ai-sdk/react'
+import { DefaultChatTransport } from 'ai'
+import { useEffect, useRef, useState } from 'react'
+import { useProject, usePersistentChat } from '@/stores'
+import { Button } from '@ui/components/button'
+import { Input } from '@ui/components/input'
+import ReactMarkdown from 'react-markdown'
+import remarkGfm from 'remark-gfm'
+import { Spinner } from '../../spinner'
+import { X, ArrowUp, Check, RotateCcw, Copy } from 'lucide-react'
+import { useGraphHighlights } from '@/stores/highlights'
+import { cn } from '@lib/utils'
+import { TextShimmer } from '@/components/text-shimmer'
+import { toast } from 'sonner'
+
+function useStickyAutoScroll(triggerKeys: ReadonlyArray<unknown>) {
+ const scrollContainerRef = useRef<HTMLDivElement>(null)
+ const bottomRef = useRef<HTMLDivElement>(null)
+ const [isAutoScroll, setIsAutoScroll] = useState(true)
+ const [isFarFromBottom, setIsFarFromBottom] = useState(false)
+
+ function scrollToBottom(behavior: ScrollBehavior = 'auto') {
+ const node = bottomRef.current
+ if (node) node.scrollIntoView({ behavior, block: 'end' })
+ }
+
+ useEffect(function observeBottomVisibility() {
+ const container = scrollContainerRef.current
+ const sentinel = bottomRef.current
+ if (!container || !sentinel) return
+
+ const observer = new IntersectionObserver((entries) => {
+ if (!entries || entries.length === 0) return
+ const isIntersecting = entries.some((e) => e.isIntersecting)
+ setIsAutoScroll(isIntersecting)
+ }, { root: container, rootMargin: '0px 0px 80px 0px', threshold: 0 })
+ observer.observe(sentinel)
+ return () => observer.disconnect()
+ }, [])
+
+ useEffect(function observeContentResize() {
+ const container = scrollContainerRef.current
+ if (!container) return
+ const resizeObserver = new ResizeObserver(() => {
+ if (isAutoScroll) scrollToBottom('auto')
+ const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight
+ setIsFarFromBottom(distanceFromBottom > 100)
+ })
+ resizeObserver.observe(container)
+ return () => resizeObserver.disconnect()
+ }, [isAutoScroll])
+
+ function enableAutoScroll() {
+ setIsAutoScroll(true)
+ }
+
+ useEffect(function autoScrollOnNewContent() {
+ if (isAutoScroll) scrollToBottom('auto')
+ }, [isAutoScroll, ...triggerKeys])
+
+ function recomputeDistanceFromBottom() {
+ const container = scrollContainerRef.current
+ if (!container) return
+ const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight
+ setIsFarFromBottom(distanceFromBottom > 100)
+ }
+
+ useEffect(() => {
+ recomputeDistanceFromBottom()
+ }, [...triggerKeys])
+
+ function onScroll() {
+ recomputeDistanceFromBottom()
+ }
+
+ return { scrollContainerRef, bottomRef, isAutoScroll, isFarFromBottom, onScroll, enableAutoScroll, scrollToBottom } as const
+}
+
+export function ChatMessages() {
+ const { selectedProject } = useProject()
+ const { currentChatId, setCurrentChatId, setConversation, getCurrentConversation, setConversationTitle, getCurrentChat } = usePersistentChat()
+
+ const activeChatIdRef = useRef<string | null>(null)
+ const shouldGenerateTitleRef = useRef<boolean>(false)
+
+ const { setDocumentIds } = useGraphHighlights()
+
+ const { messages, sendMessage, status, stop, setMessages, id, regenerate } = useChat({
+ id: currentChatId ?? undefined,
+ transport: new DefaultChatTransport({
+ api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat`,
+ credentials: 'include',
+ body: { metadata: { projectId: selectedProject } },
+ }),
+ maxSteps: 2,
+ onFinish: (result) => {
+ const activeId = activeChatIdRef.current
+ if (!activeId) return
+ if (result.message.role !== 'assistant') return
+
+ if (shouldGenerateTitleRef.current) {
+ const textPart = result.message.parts.find((p: any) => p?.type === 'text') as any
+ const text = textPart?.text?.trim()
+ if (text) {
+ shouldGenerateTitleRef.current = false
+ complete(text)
+ }
+ }
+ },
+ })
+
+ useEffect(() => {
+ activeChatIdRef.current = (currentChatId ?? id) ?? null
+ }, [currentChatId, id])
+
+ useEffect(() => {
+ if (id && id !== currentChatId) {
+ setCurrentChatId(id)
+ }
+ }, [id, currentChatId, setCurrentChatId])
+
+ useEffect(() => {
+ const msgs = getCurrentConversation()
+ setMessages(msgs ?? [])
+ setInput('')
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [currentChatId])
+
+ useEffect(() => {
+ const activeId = currentChatId ?? id
+ if (activeId && messages.length > 0) {
+ setConversation(activeId, messages)
+ }
+ }, [messages, currentChatId, id, setConversation])
+
+ const [input, setInput] = useState('')
+ const { complete } = useCompletion({
+ api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/title`,
+ credentials: 'include',
+ onFinish: (_, completion) => {
+ const activeId = activeChatIdRef.current
+ if (!completion || !activeId) return
+ setConversationTitle(activeId, completion.trim())
+ }
+ })
+
+ // Update graph highlights from the most recent tool-searchMemories output
+ useEffect(() => {
+ try {
+ const lastAssistant = [...messages].reverse().find((m) => m.role === 'assistant')
+ if (!lastAssistant) return
+ const lastSearchPart = [...(lastAssistant.parts as any[])]
+ .reverse()
+ .find((p) => p?.type === 'tool-searchMemories' && p?.state === 'output-available')
+ if (!lastSearchPart) return
+ const output = (lastSearchPart as any).output
+ const ids = Array.isArray(output?.results)
+ ? (output.results as any[]).map((r) => r?.documentId).filter(Boolean) as string[]
+ : []
+ if (ids.length > 0) {
+ setDocumentIds(ids)
+ }
+ } catch { }
+ }, [messages, setDocumentIds])
+
+ useEffect(() => {
+ const currentSummary = getCurrentChat()
+ const hasTitle = Boolean(currentSummary?.title && currentSummary.title.trim().length > 0)
+ shouldGenerateTitleRef.current = !hasTitle
+ }, [currentChatId, id, getCurrentChat])
+ const { scrollContainerRef, bottomRef, isFarFromBottom, onScroll, enableAutoScroll, scrollToBottom } = useStickyAutoScroll([messages, status])
+
+ return (
+ <>
+ <div className="relative grow">
+ <div ref={scrollContainerRef} onScroll={onScroll} className="flex flex-col gap-2 absolute inset-0 overflow-y-auto px-4 pt-4 pb-7 scroll-pb-7">
+ {messages.map((message) => (
+ <div key={message.id} className={cn('flex flex-col', message.role === 'user' ? 'items-end' : 'items-start')}>
+ <div className="flex flex-col gap-2 max-w-4/5 bg-white/10 py-3 px-4 rounded-lg">
+ {message.parts
+ .filter((part) => ['text', 'tool-searchMemories', 'tool-addMemory'].includes(part.type))
+ .map((part, index) => {
+ switch (part.type) {
+ case 'text':
+ return (
+ <div key={index} className="prose prose-sm prose-invert max-w-none">
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{(part as any).text}</ReactMarkdown>
+ </div>
+ )
+ case 'tool-searchMemories':
+ switch (part.state) {
+ case 'input-available':
+ case 'input-streaming':
+ return (
+ <div key={index} className="text-sm flex items-center gap-2 text-muted-foreground">
+ <Spinner className="size-4" /> Searching memories...
+ </div>
+ )
+ case 'output-error':
+ return (
+ <div key={index} className="text-sm flex items-center gap-2 text-muted-foreground">
+ <X className="size-4" /> Error recalling memories
+ </div>
+ )
+ case 'output-available':
+ {
+ const output = (part as any).output
+ const foundCount = typeof output === 'object' && output !== null && 'count' in output ? Number(output.count) || 0 : 0
+ const ids = Array.isArray(output?.results) ? (output.results as any[]).map((r) => r?.documentId).filter(Boolean) as string[] : []
+ return (
+ <div key={index} className="text-sm flex items-center gap-2 text-muted-foreground">
+ <Check className="size-4" /> Found {foundCount} memories
+ </div>
+ )
+ }
+
+ }
+ case 'tool-addMemory':
+ switch (part.state) {
+ case 'input-available':
+ return (
+ <div key={index} className="text-sm flex items-center gap-2 text-muted-foreground">
+ <Spinner className="size-4" /> Adding memory...
+ </div>
+ )
+ case 'output-error':
+ return (
+ <div key={index} className="text-sm flex items-center gap-2 text-muted-foreground">
+ <X className="size-4" /> Error adding memory
+ </div>
+ )
+ case 'output-available':
+ return (
+ <div key={index} className="text-sm flex items-center gap-2 text-muted-foreground">
+ <Check className="size-4" /> Memory added
+ </div>
+ )
+ case 'input-streaming':
+ return (
+ <div key={index} className="text-sm flex items-center gap-2 text-muted-foreground">
+ <Spinner className="size-4" /> Adding memory...
+ </div>
+ )
+ }
+ }
+
+ return null
+ })}
+ </div>
+ {message.role === 'assistant' && (
+ <div className='flex items-center gap-0.5 mt-0.5'>
+ <Button variant="ghost" size="icon" className='size-7 text-muted-foreground hover:text-foreground' onClick={() => {
+ navigator.clipboard.writeText(message.parts.filter((p) => p.type === 'text')?.map((p) => (p as any).text).join('\n') ?? '')
+ toast.success('Copied to clipboard')
+ }}>
+ <Copy className="size-3.5" />
+ </Button>
+ <Button variant="ghost" size="icon" className='size-6 text-muted-foreground hover:text-foreground' onClick={() => regenerate({ messageId: message.id })}>
+ <RotateCcw className="size-3.5" />
+ </Button>
+
+ </div>
+ )}
+ </div>
+ ))}
+ {status === 'submitted' && (
+ <div className="flex text-muted-foreground justify-start gap-2 px-4 py-3 items-center w-full">
+ <Spinner className="size-4" />
+ <TextShimmer className='text-sm' duration={1.5}>Thinking...</TextShimmer>
+ </div>
+ )}
+ <div ref={bottomRef} />
+ </div>
+
+ <Button
+ type="button"
+ onClick={() => {
+ enableAutoScroll()
+ scrollToBottom('smooth')
+ }}
+ className={cn(
+ 'rounded-full w-fit mx-auto shadow-md z-10 absolute inset-x-0 bottom-4 flex justify-center',
+ 'transition-all duration-200 ease-out',
+ isFarFromBottom ? 'opacity-100 scale-100 pointer-events-auto' : 'opacity-0 scale-95 pointer-events-none',
+ )}
+ variant="default"
+ size="sm"
+ >
+ Scroll to bottom
+ </Button>
+ </div>
+
+ <form
+ className="flex gap-2 px-4 pb-4 pt-1 relative"
+ onSubmit={(e) => {
+ e.preventDefault()
+ if (status === 'submitted') return
+ if (status === 'streaming') {
+ stop()
+ return
+ }
+ if (input.trim()) {
+ enableAutoScroll()
+ scrollToBottom('auto')
+ sendMessage({ text: input })
+ setInput('')
+ }
+ }}
+ >
+ <div className="absolute top-0 left-0 -mt-7 w-full h-7 bg-gradient-to-t from-background to-transparent" />
+ <Input className="w-full" value={input} onChange={(e) => setInput(e.target.value)} disabled={status === 'submitted'} placeholder="Say something..." />
+ <Button type="submit" disabled={status === 'submitted'}>
+ {status === 'ready' ? <ArrowUp className="size-4" /> : status === 'submitted' ? <Spinner className="size-4" /> : <X className="size-4" />}
+ </Button>
+ </form>
+ </>
+ )
+}
+
+
diff --git a/apps/web/components/views/chat/index.tsx b/apps/web/components/views/chat/index.tsx
new file mode 100644
index 00000000..a547f590
--- /dev/null
+++ b/apps/web/components/views/chat/index.tsx
@@ -0,0 +1,124 @@
+'use client';
+
+import { useChatOpen, useProject } from '@/stores';
+import { usePersistentChat } from '@/stores';
+import { Button } from '@ui/components/button';
+import { Plus, X, Trash2, HistoryIcon } from 'lucide-react';
+import { useMemo, useState } from 'react';
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@ui/components/dialog";
+import { ScrollArea } from '@ui/components/scroll-area';
+import { cn } from '@lib/utils';
+import { ChatMessages } from './chat-messages';
+import { formatDistanceToNow } from 'date-fns';
+import { analytics } from '@/lib/analytics';
+
+export function ChatRewrite() {
+ const { setIsOpen } = useChatOpen();
+ const { selectedProject } = useProject();
+ const { conversations, currentChatId, setCurrentChatId, getCurrentChat } = usePersistentChat();
+
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
+
+ const sorted = useMemo(() => {
+ return [...conversations].sort((a, b) => (a.lastUpdated < b.lastUpdated ? 1 : -1));
+ }, [conversations]);
+
+ function handleNewChat() {
+ analytics.newChatStarted();
+ const newId = crypto.randomUUID();
+ setCurrentChatId(newId);
+ setIsDialogOpen(false);
+ }
+
+ function formatRelativeTime(isoString: string): string {
+ return formatDistanceToNow(new Date(isoString), { addSuffix: true });
+ }
+
+ return (
+ <div className='flex flex-col h-full overflow-y-hidden border-l bg-background'>
+ <div className="border-b px-4 py-3 flex justify-between items-center">
+ <h3 className="text-lg font-semibold line-clamp-1 text-ellipsis overflow-hidden">{getCurrentChat()?.title ?? "New Chat"}</h3>
+ <div className="flex items-center gap-2">
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
+ <DialogTrigger asChild>
+ <Button variant="outline" size="icon" onClick={() => analytics.chatHistoryViewed()}>
+ <HistoryIcon className='size-4 text-muted-foreground' />
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="sm:max-w-lg">
+ <DialogHeader className="pb-4 border-b rounded-t-lg">
+ <DialogTitle className="">
+ Conversations
+ </DialogTitle>
+ <DialogDescription>
+ Project <span className="font-mono font-medium">{selectedProject}</span>
+ </DialogDescription>
+ </DialogHeader>
+
+ <ScrollArea className="max-h-96">
+ <div className="flex flex-col gap-1">
+ {sorted.map((c) => {
+ const isActive = c.id === currentChatId;
+ return (
+ <div
+ key={c.id}
+ role="button"
+ tabIndex={0}
+ onClick={() => {
+ setCurrentChatId(c.id);
+ setIsDialogOpen(false);
+ }}
+ className={cn(
+ 'flex items-center justify-between rounded-md px-3 py-2 outline-none',
+ 'transition-colors',
+ isActive ? 'bg-primary/10' : 'hover:bg-muted'
+ )}
+ aria-current={isActive ? 'true' : undefined}
+ >
+ <div className="min-w-0">
+ <div className="flex items-center gap-2">
+ <span className={cn('text-sm font-medium truncate', isActive ? 'text-foreground' : undefined)}>
+ {c.title || 'Untitled Chat'}
+ </span>
+ </div>
+ <div className="text-xs text-muted-foreground">
+ Last updated {formatRelativeTime(c.lastUpdated)}
+ </div>
+ </div>
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ onClick={(e) => {
+ e.stopPropagation();
+ analytics.chatDeleted();
+ }}
+ aria-label="Delete conversation"
+ >
+ <Trash2 className="size-4 text-muted-foreground" />
+ </Button>
+ </div>
+ );
+ })}
+ {sorted.length === 0 && (
+ <div className="text-xs text-muted-foreground px-3 py-2">No conversations yet</div>
+ )}
+ </div>
+ </ScrollArea>
+ <Button variant="outline" size="lg" className="w-full border-dashed" onClick={handleNewChat}>
+ <Plus className='size-4 mr-1' /> New Conversation
+ </Button>
+ </DialogContent>
+ </Dialog>
+ <Button variant="outline" size="icon" onClick={handleNewChat}>
+ <Plus className='size-4 text-muted-foreground' />
+ </Button>
+ <Button variant="outline" size="icon" onClick={() => setIsOpen(false)}>
+ <X className='size-4 text-muted-foreground' />
+ </Button>
+ </div>
+ </div>
+ <ChatMessages />
+ </div >
+ );
+} \ No newline at end of file
diff --git a/apps/web/components/views/connections-tab-content.tsx b/apps/web/components/views/connections-tab-content.tsx
new file mode 100644
index 00000000..177c2a4d
--- /dev/null
+++ b/apps/web/components/views/connections-tab-content.tsx
@@ -0,0 +1,382 @@
+"use client"
+
+import { $fetch } from "@lib/api"
+import {
+ fetchConnectionsFeature,
+ fetchConsumerProProduct,
+} from "@repo/lib/queries"
+import { Button } from "@repo/ui/components/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@repo/ui/components/dialog"
+import { Skeleton } from "@repo/ui/components/skeleton"
+import type { ConnectionResponseSchema } from "@repo/validation/api"
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
+import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
+import { useCustomer } from "autumn-js/react"
+import { Plus, Trash2 } from "lucide-react"
+import { AnimatePresence, motion } from "motion/react"
+import Link from "next/link"
+import { useEffect, useState } from "react"
+import { toast } from "sonner"
+import type { z } from "zod"
+import { useProject } from "@/stores"
+import { analytics } from "@/lib/analytics"
+
+// Define types
+type Connection = z.infer<typeof ConnectionResponseSchema>
+
+// Connector configurations
+const CONNECTORS = {
+ "google-drive": {
+ title: "Google Drive",
+ description: "Connect your Google Docs, Sheets, and Slides",
+ icon: GoogleDrive,
+ },
+ notion: {
+ title: "Notion",
+ description: "Import your Notion pages and databases",
+ icon: Notion,
+ },
+ onedrive: {
+ title: "OneDrive",
+ description: "Access your Microsoft Office documents",
+ icon: OneDrive,
+ },
+} as const
+
+type ConnectorProvider = keyof typeof CONNECTORS
+
+export function ConnectionsTabContent() {
+ const queryClient = useQueryClient()
+ const [showAddDialog, setShowAddDialog] = useState(false)
+ const { selectedProject } = useProject()
+ const autumn = useCustomer()
+
+ const handleUpgrade = async () => {
+ try {
+ await autumn.attach({
+ productId: "consumer_pro",
+ successUrl: "https://app.supermemory.ai/",
+ })
+ window.location.reload()
+ } catch (error) {
+ console.error(error)
+ }
+ }
+
+ const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any)
+ const connectionsUsed = connectionsCheck?.balance ?? 0
+ const connectionsLimit = connectionsCheck?.included_usage ?? 0
+
+ const { data: proCheck } = fetchConsumerProProduct(autumn as any)
+ const isProUser = proCheck?.allowed ?? false
+
+ const canAddConnection = connectionsUsed < connectionsLimit
+
+ // Fetch connections
+ const {
+ data: connections = [],
+ isLoading,
+ error,
+ } = useQuery({
+ queryKey: ["connections"],
+ queryFn: async () => {
+ const response = await $fetch("@post/connections/list", {
+ body: {
+ containerTags: [],
+ },
+ })
+
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to load connections")
+ }
+
+ return response.data as Connection[]
+ },
+ staleTime: 30 * 1000,
+ refetchInterval: 60 * 1000,
+ })
+
+ // Show error toast if connections fail to load
+ useEffect(() => {
+ if (error) {
+ toast.error("Failed to load connections", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ }
+ }, [error])
+
+ // Add connection mutation
+ const addConnectionMutation = useMutation({
+ mutationFn: async (provider: ConnectorProvider) => {
+ // Check if user can add connections
+ if (!canAddConnection && !isProUser) {
+ throw new Error(
+ "Free plan doesn't include connections. Upgrade to Pro for unlimited connections.",
+ )
+ }
+
+ const response = await $fetch("@post/connections/:provider", {
+ params: { provider },
+ body: {
+ redirectUrl: window.location.href,
+ containerTags: [selectedProject],
+ },
+ })
+
+ // biome-ignore lint/style/noNonNullAssertion: its fine
+ if ("data" in response && !("error" in response.data!)) {
+ return response.data
+ }
+
+ throw new Error(response.error?.message || "Failed to connect")
+ },
+ onSuccess: (data, provider) => {
+ analytics.connectionAdded(provider)
+ analytics.connectionAuthStarted()
+ if (data?.authLink) {
+ window.location.href = data.authLink
+ }
+ },
+ onError: (error, provider) => {
+ analytics.connectionAuthFailed()
+ toast.error(`Failed to connect ${provider}`, {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ })
+
+ // Delete connection mutation
+ const deleteConnectionMutation = useMutation({
+ mutationFn: async (connectionId: string) => {
+ await $fetch(`@delete/connections/${connectionId}`)
+ },
+ onSuccess: () => {
+ analytics.connectionDeleted()
+ toast.success(
+ "Connection removal has started. supermemory will permanently delete the documents in the next few minutes.",
+ )
+ queryClient.invalidateQueries({ queryKey: ["connections"] })
+ },
+ onError: (error) => {
+ toast.error("Failed to remove connection", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ })
+
+ const getProviderIcon = (provider: string) => {
+ const connector = CONNECTORS[provider as ConnectorProvider]
+ if (connector) {
+ const Icon = connector.icon
+ return <Icon className="h-10 w-10" />
+ }
+ return <span className="text-2xl">📎</span>
+ }
+
+ return (
+ <div className="space-y-4">
+ <div className="flex justify-between items-center mb-4">
+ <div>
+ <p className="text-sm text-white/70">
+ Connect your favorite services to import documents
+ </p>
+ {!isProUser && (
+ <p className="text-xs text-white/50 mt-1">
+ Connections require a Pro subscription
+ </p>
+ )}
+ </div>
+ <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ disabled={!canAddConnection && !isProUser}
+ onClick={() => setShowAddDialog(true)}
+ size="sm"
+ >
+ <Plus className="h-4 w-4 mr-2" />
+ Add
+ </Button>
+ </motion.div>
+ </div>
+
+ {/* Show upgrade prompt for free users */}
+ {!isProUser && (
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg"
+ initial={{ opacity: 0, y: -10 }}
+ >
+ <p className="text-sm text-yellow-400 mb-2">
+ 🔌 Connections are a Pro feature
+ </p>
+ <p className="text-xs text-white/60 mb-3">
+ Connect Google Drive, Notion, OneDrive and more to automatically
+ sync your documents.
+ </p>
+ <Button
+ asChild
+ className="bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-400 border-yellow-500/30"
+ onClick={handleUpgrade}
+ size="sm"
+ variant="secondary"
+ >
+ Upgrade to Pro
+ </Button>
+ </motion.div>
+ )}
+
+ {isLoading ? (
+ <div className="space-y-3">
+ {[...Array(2)].map((_, i) => (
+ <motion.div
+ animate={{ opacity: 1 }}
+ className="p-4 bg-white/5 rounded-lg"
+ initial={{ opacity: 0 }}
+ key={`skeleton-${Date.now()}-${i}`}
+ transition={{ delay: i * 0.1 }}
+ >
+ <Skeleton className="h-12 w-full bg-white/10" />
+ </motion.div>
+ ))}
+ </div>
+ ) : connections.length === 0 ? (
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ className="text-center py-8"
+ initial={{ opacity: 0, scale: 0.9 }}
+ transition={{ type: "spring", damping: 20 }}
+ >
+ <p className="text-white/50 mb-4">No connections yet</p>
+ <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ onClick={() => setShowAddDialog(true)}
+ size="sm"
+ variant="secondary"
+ >
+ Add Your First Connection
+ </Button>
+ </motion.div>
+ </motion.div>
+ ) : (
+ <motion.div className="space-y-2">
+ <AnimatePresence>
+ {connections.map((connection, index) => (
+ <motion.div
+ animate={{ opacity: 1, x: 0 }}
+ className="flex items-center justify-between p-3 bg-white/5 rounded-lg hover:bg-white/10 transition-colors"
+ exit={{ opacity: 0, x: 20 }}
+ initial={{ opacity: 0, x: -20 }}
+ key={connection.id}
+ layout
+ transition={{ delay: index * 0.05 }}
+ >
+ <div className="flex items-center gap-3">
+ <motion.div
+ animate={{ rotate: 0, opacity: 1 }}
+ initial={{ rotate: -180, opacity: 0 }}
+ transition={{ delay: index * 0.05 + 0.2 }}
+ >
+ {getProviderIcon(connection.provider)}
+ </motion.div>
+ <div>
+ <p className="font-medium text-white capitalize">
+ {connection.provider.replace("-", " ")}
+ </p>
+ {connection.email && (
+ <p className="text-sm text-white/60">
+ {connection.email}
+ </p>
+ )}
+ </div>
+ </div>
+ <motion.div
+ whileHover={{ scale: 1.1 }}
+ whileTap={{ scale: 0.9 }}
+ >
+ <Button
+ className="text-white/50 hover:text-red-400"
+ disabled={deleteConnectionMutation.isPending}
+ onClick={() =>
+ deleteConnectionMutation.mutate(connection.id)
+ }
+ size="icon"
+ variant="ghost"
+ >
+ <Trash2 className="h-4 w-4" />
+ </Button>
+ </motion.div>
+ </motion.div>
+ ))}
+ </AnimatePresence>
+ </motion.div>
+ )}
+
+ {/* Add Connection Dialog */}
+ <AnimatePresence>
+ {showAddDialog && (
+ <Dialog onOpenChange={setShowAddDialog} open={showAddDialog}>
+ <DialogContent className="sm:max-w-2xl bg-black/90 backdrop-blur-xl border-white/10 text-white">
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.95 }}
+ initial={{ opacity: 0, scale: 0.95 }}
+ >
+ <DialogHeader>
+ <DialogTitle>Add a Connection</DialogTitle>
+ <DialogDescription className="text-white/60">
+ Choose a service to connect and import your documents
+ </DialogDescription>
+ </DialogHeader>
+ <div className="grid gap-3 py-4">
+ {Object.entries(CONNECTORS).map(
+ ([provider, config], index) => {
+ const Icon = config.icon
+ return (
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ initial={{ opacity: 0, y: 20 }}
+ key={provider}
+ transition={{ delay: index * 0.05 }}
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ >
+ <Button
+ className="justify-start h-auto p-4 bg-white/5 hover:bg-white/10 border-white/10 text-white w-full"
+ disabled={addConnectionMutation.isPending}
+ onClick={() => {
+ addConnectionMutation.mutate(
+ provider as ConnectorProvider,
+ )
+ setShowAddDialog(false)
+ // onClose?.()
+ }}
+ variant="outline"
+ >
+ <Icon className="h-8 w-8 mr-3" />
+ <div className="text-left">
+ <div className="font-medium">{config.title}</div>
+ <div className="text-sm text-white/60 mt-0.5">
+ {config.description}
+ </div>
+ </div>
+ </Button>
+ </motion.div>
+ )
+ },
+ )}
+ </div>
+ </motion.div>
+ </DialogContent>
+ </Dialog>
+ )}
+ </AnimatePresence>
+ </div>
+ )
+}
diff --git a/apps/web/components/views/mcp/index.tsx b/apps/web/components/views/mcp/index.tsx
new file mode 100644
index 00000000..41ef97b8
--- /dev/null
+++ b/apps/web/components/views/mcp/index.tsx
@@ -0,0 +1,311 @@
+import { $fetch } from "@lib/api"
+import { authClient } from "@lib/auth"
+import { useAuth } from "@lib/auth-context"
+import { useForm } from "@tanstack/react-form"
+import { useMutation } from "@tanstack/react-query"
+import { Button } from "@ui/components/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@ui/components/dialog"
+import { Input } from "@ui/components/input"
+import { CopyableCell } from "@ui/copyable-cell"
+import { Loader2 } from "lucide-react"
+import { AnimatePresence, motion } from "motion/react"
+import Image from "next/image"
+import { generateSlug } from "random-word-slugs"
+import { useState, useEffect } from "react"
+import { toast } from "sonner"
+import { z } from "zod/v4"
+import { InstallationDialogContent } from "./installation-dialog-content"
+import { analytics } from "@/lib/analytics"
+
+// Validation schemas
+const mcpMigrationSchema = z.object({
+ url: z
+ .string()
+ .min(1, "MCP Link is required")
+ .regex(
+ /^https:\/\/mcp\.supermemory\.ai\/[^/]+\/sse$/,
+ "Link must be in format: https://mcp.supermemory.ai/userId/sse",
+ ),
+})
+
+export function MCPView() {
+ const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false)
+ const projectId = localStorage.getItem("selectedProject") ?? "default"
+ const { org } = useAuth()
+ const [apiKey, setApiKey] = useState<string>()
+ const [isInstallDialogOpen, setIsInstallDialogOpen] = useState(false)
+
+ useEffect(() => {
+ analytics.mcpViewOpened()
+ }, [])
+ const apiKeyMutation = useMutation({
+ mutationFn: async () => {
+ if (apiKey) return apiKey
+ const res = await authClient.apiKey.create({
+ metadata: {
+ organizationId: org?.id,
+ },
+ name: generateSlug(),
+ prefix: `sm_${org?.id}_`,
+ })
+ return res.key
+ },
+ onSuccess: (data) => {
+ setApiKey(data)
+ setIsInstallDialogOpen(true)
+ },
+ })
+
+ // Form for MCP migration
+ const mcpMigrationForm = useForm({
+ defaultValues: { url: "" },
+ onSubmit: async ({ value, formApi }) => {
+ const userId = extractUserIdFromMCPUrl(value.url)
+ if (userId) {
+ migrateMCPMutation.mutate({ userId, projectId })
+ formApi.reset()
+ }
+ },
+ validators: {
+ onChange: mcpMigrationSchema,
+ },
+ })
+
+ const extractUserIdFromMCPUrl = (url: string): string | null => {
+ const regex = /^https:\/\/mcp\.supermemory\.ai\/([^/]+)\/sse$/
+ const match = url.trim().match(regex)
+ return match?.[1] || null
+ }
+
+ // Migrate MCP mutation
+ const migrateMCPMutation = useMutation({
+ mutationFn: async ({
+ userId,
+ projectId,
+ }: {
+ userId: string
+ projectId: string
+ }) => {
+ const response = await $fetch("@post/memories/migrate-mcp", {
+ body: { userId, projectId },
+ })
+
+ if (response.error) {
+ throw new Error(
+ response.error?.message || "Failed to migrate documents",
+ )
+ }
+
+ return response.data
+ },
+ onSuccess: (data) => {
+ toast.success("Migration completed!", {
+ description: `Successfully migrated ${data?.migratedCount} documents`,
+ })
+ setIsMigrateDialogOpen(false)
+ },
+ onError: (error) => {
+ toast.error("Migration failed", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ })
+
+ return (
+ <div className="space-y-6">
+ <div>
+ <p className="text-sm text-white/70">
+ Use MCP to create and access memories directly from your AI assistant.
+ Integrate supermemory with Claude Desktop, Cursor, and other AI tools.
+ </p>
+ </div>
+
+ <div className="space-y-4">
+ <div>
+ <label
+ className="text-sm font-medium text-white/80 block mb-2"
+ htmlFor="mcp-server-url"
+ >
+ MCP Server URL
+ </label>
+ <div className="p-3 bg-white/5 rounded border border-white/10">
+ <CopyableCell
+ className="font-mono text-sm text-blue-400"
+ value="https://api.supermemory.ai/mcp"
+ />
+ </div>
+ <p className="text-xs text-white/50 mt-2">
+ Use this URL to configure supermemory in your AI assistant
+ </p>
+ </div>
+
+ <div className="flex items-center gap-4">
+ <Dialog
+ onOpenChange={setIsInstallDialogOpen}
+ open={isInstallDialogOpen}
+ >
+ <DialogTrigger asChild>
+ <Button
+ disabled={apiKeyMutation.isPending}
+ onClick={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ apiKeyMutation.mutate()
+ }}
+ >
+ Install Now
+ </Button>
+ </DialogTrigger>
+ {apiKey && <InstallationDialogContent />}
+ </Dialog>
+ <motion.a
+ className="inline-block"
+ href="https://cursor.com/install-mcp?name=supermemory&config=JTdCJTIydXJsJTIyJTNBJTIyaHR0cHMlM0ElMkYlMkZhcGkuc3VwZXJtZW1vcnkuYWklMkZtY3AlMjIlN0Q%3D"
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Image
+ alt="Add supermemory MCP server to Cursor"
+ height="32"
+ src="https://cursor.com/deeplink/mcp-install-dark.svg"
+ width="128"
+ />
+ </motion.a>
+
+ <div className="h-8 w-px bg-white/10" />
+
+ <motion.div whileTap={{ scale: 0.95 }}>
+ <Button
+ className="bg-white/5 hover:bg-white/10 border-white/10 text-white h-8"
+ onClick={() => setIsMigrateDialogOpen(true)}
+ size="sm"
+ variant="outline"
+ >
+ Migrate from v1
+ </Button>
+ </motion.div>
+ </div>
+ </div>
+ <AnimatePresence>
+ {isMigrateDialogOpen && (
+ <Dialog
+ onOpenChange={setIsMigrateDialogOpen}
+ open={isMigrateDialogOpen}
+ >
+ <DialogContent className="sm:max-w-2xl bg-black/90 backdrop-blur-xl border-white/10 text-white">
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.95 }}
+ initial={{ opacity: 0, scale: 0.95 }}
+ >
+ <DialogHeader>
+ <DialogTitle>Migrate from MCP v1</DialogTitle>
+ <DialogDescription className="text-white/60">
+ Migrate your MCP documents from the legacy system.
+ </DialogDescription>
+ </DialogHeader>
+ <form
+ onSubmit={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ mcpMigrationForm.handleSubmit()
+ }}
+ >
+ <div className="grid gap-4">
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex flex-col gap-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.1 }}
+ >
+ <label className="text-sm font-medium" htmlFor="mcpUrl">
+ MCP Link
+ </label>
+ <mcpMigrationForm.Field name="url">
+ {({ state, handleChange, handleBlur }) => (
+ <>
+ <Input
+ className="bg-white/5 border-white/10 text-white"
+ id="mcpUrl"
+ onBlur={handleBlur}
+ onChange={(e) => handleChange(e.target.value)}
+ placeholder="https://mcp.supermemory.ai/your-user-id/sse"
+ value={state.value}
+ />
+ {state.meta.errors.length > 0 && (
+ <motion.p
+ animate={{ opacity: 1, height: "auto" }}
+ className="text-sm text-red-400 mt-1"
+ exit={{ opacity: 0, height: 0 }}
+ initial={{ opacity: 0, height: 0 }}
+ >
+ {state.meta.errors.join(", ")}
+ </motion.p>
+ )}
+ </>
+ )}
+ </mcpMigrationForm.Field>
+ <p className="text-xs text-white/50">
+ Enter your old MCP Link in the format: <br />
+ <span className="font-mono">
+ https://mcp.supermemory.ai/userId/sse
+ </span>
+ </p>
+ </motion.div>
+ </div>
+ <div className="flex justify-end gap-3 mt-4">
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
+ onClick={() => {
+ setIsMigrateDialogOpen(false)
+ mcpMigrationForm.reset()
+ }}
+ type="button"
+ variant="outline"
+ >
+ Cancel
+ </Button>
+ </motion.div>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ disabled={
+ migrateMCPMutation.isPending ||
+ !mcpMigrationForm.state.canSubmit
+ }
+ type="submit"
+ >
+ {migrateMCPMutation.isPending ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ Migrating...
+ </>
+ ) : (
+ "Migrate"
+ )}
+ </Button>
+ </motion.div>
+ </div>
+ </form>
+ </motion.div>
+ </DialogContent>
+ </Dialog>
+ )}
+ </AnimatePresence>
+ </div>
+ )
+}
diff --git a/apps/web/components/views/mcp/installation-dialog-content.tsx b/apps/web/components/views/mcp/installation-dialog-content.tsx
new file mode 100644
index 00000000..3a6b2f3e
--- /dev/null
+++ b/apps/web/components/views/mcp/installation-dialog-content.tsx
@@ -0,0 +1,79 @@
+import { Button } from "@ui/components/button"
+import {
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@ui/components/dialog"
+import { Input } from "@ui/components/input"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@ui/components/select"
+import { CopyIcon } from "lucide-react"
+import { useState } from "react"
+import { toast } from "sonner"
+import { analytics } from "@/lib/analytics"
+
+const clients = {
+ cursor: "Cursor",
+ claude: "Claude Desktop",
+ vscode: "VSCode",
+ cline: "Cline",
+ "roo-cline": "Roo Cline",
+ witsy: "Witsy",
+ enconvo: "Enconvo",
+ "gemini-cli": "Gemini CLI",
+ "claude-code": "Claude Code",
+} as const
+
+export function InstallationDialogContent() {
+ const [client, setClient] = useState<keyof typeof clients>("cursor")
+ return (
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Install the supermemory MCP Server</DialogTitle>
+ <DialogDescription>
+ Select the app you want to install supermemory MCP to, then run the
+ following command:
+ </DialogDescription>
+ </DialogHeader>
+ <div className="flex gap-2 items-center">
+ <Input
+ className="font-mono text-xs!"
+ readOnly
+ value={`npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${client} --oauth=yes`}
+ />
+ <Select
+ onValueChange={(value) => setClient(value as keyof typeof clients)}
+ value={client}
+ >
+ <SelectTrigger className="w-48">
+ <SelectValue placeholder="Theme" />
+ </SelectTrigger>
+ <SelectContent>
+ {Object.entries(clients).map(([key, value]) => (
+ <SelectItem key={key} value={key}>
+ {value}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ <Button
+ onClick={() => {
+ navigator.clipboard.writeText(
+ `npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${client} --oauth=yes`,
+ )
+ analytics.mcpInstallCmdCopied()
+ toast.success("Copied to clipboard!")
+ }}
+ >
+ <CopyIcon className="size-4" /> Copy Installation Command
+ </Button>
+ </DialogContent>
+ )
+}
diff --git a/apps/web/components/views/profile.tsx b/apps/web/components/views/profile.tsx
new file mode 100644
index 00000000..fd7334fe
--- /dev/null
+++ b/apps/web/components/views/profile.tsx
@@ -0,0 +1,266 @@
+"use client"
+
+import { authClient } from "@lib/auth"
+import { useAuth } from "@lib/auth-context"
+import {
+ fetchConnectionsFeature,
+ fetchMemoriesFeature,
+ fetchSubscriptionStatus,
+} from "@lib/queries"
+import { Button } from "@repo/ui/components/button"
+import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold"
+import { useCustomer } from "autumn-js/react"
+import { CreditCard, LoaderIcon, LogOut, User, CheckCircle, X } from "lucide-react"
+import { motion } from "motion/react"
+import Link from "next/link"
+import { usePathname, useRouter } from "next/navigation"
+import { useState } from "react"
+import { analytics } from "@/lib/analytics"
+
+export function ProfileView() {
+ const router = useRouter()
+ const pathname = usePathname()
+ const { user: session, org } = useAuth()
+ const organizations = org
+ const autumn = useCustomer()
+ const [isLoading, setIsLoading] = useState(false)
+
+ const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any)
+ const memoriesUsed = memoriesCheck?.usage ?? 0
+ const memoriesLimit = memoriesCheck?.included_usage ?? 0
+
+ const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any)
+ const connectionsUsed = connectionsCheck?.usage ?? 0
+
+ // Fetch subscription status with React Query
+ const {
+ data: status = {
+ consumer_pro: null,
+ },
+ isLoading: isCheckingStatus,
+ } = fetchSubscriptionStatus(autumn as any)
+
+ const isPro = status.consumer_pro
+
+ const handleLogout = () => {
+ analytics.userSignedOut()
+ authClient.signOut()
+ router.push("/login")
+ }
+
+ const handleUpgrade = async () => {
+ setIsLoading(true)
+ try {
+ await autumn.attach({
+ productId: "consumer_pro",
+ successUrl: "https://app.supermemory.ai/",
+ })
+ window.location.reload()
+ } catch (error) {
+ console.error(error)
+ setIsLoading(false)
+ }
+ }
+
+ // Handle manage billing
+ const handleManageBilling = async () => {
+ await autumn.openBillingPortal({
+ returnUrl: "https://app.supermemory.ai",
+ })
+ }
+
+ if (session?.isAnonymous) {
+ return (
+ <div className="space-y-4">
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ className="text-center py-8"
+ initial={{ opacity: 0, scale: 0.9 }}
+ transition={{ type: "spring", damping: 20 }}
+ >
+ <p className="text-white/70 mb-4">Sign in to access your profile and billing</p>
+ <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
+ <Button
+ asChild
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ size="sm"
+ >
+ <Link href="/login">Sign in</Link>
+ </Button>
+ </motion.div>
+ </motion.div>
+ </div>
+ )
+ }
+
+ return (
+ <div className="space-y-4">
+ {/* Profile Section */}
+ <div className="bg-white/5 rounded-lg p-4 space-y-3">
+ <div className="flex items-center gap-3">
+ <div className="w-10 h-10 bg-white/10 rounded-full flex items-center justify-center">
+ <User className="w-5 h-5 text-white/80" />
+ </div>
+ <div className="flex-1">
+ <p className="text-white font-medium text-sm">{session?.email}</p>
+ <p className="text-white/60 text-xs">Logged in</p>
+ </div>
+ </div>
+ </div>
+
+ {/* Billing Section */}
+ <div className="bg-white/5 rounded-lg p-4 space-y-3">
+ <div className="flex items-center gap-3 mb-3">
+ <div className="w-10 h-10 bg-white/10 rounded-full flex items-center justify-center">
+ <CreditCard className="w-5 h-5 text-white/80" />
+ </div>
+ <div className="flex-1">
+ <HeadingH3Bold className="text-white text-sm">
+ {isPro ? "Pro Plan" : "Free Plan"}
+ {isPro && (
+ <span className="ml-2 text-xs bg-green-500/20 text-green-400 px-2 py-0.5 rounded-full">
+ Active
+ </span>
+ )}
+ </HeadingH3Bold>
+ <p className="text-white/60 text-xs">
+ {isPro ? "Expanded memory capacity" : "Basic plan"}
+ </p>
+ </div>
+ </div>
+
+ {/* Usage Stats */}
+ <div className="space-y-2">
+ <div className="flex justify-between items-center">
+ <span className="text-sm text-white/70">Memories</span>
+ <span className={`text-sm ${memoriesUsed >= memoriesLimit ? "text-red-400" : "text-white/90"}`}>
+ {memoriesUsed} / {memoriesLimit}
+ </span>
+ </div>
+ <div className="w-full bg-white/10 rounded-full h-2">
+ <div
+ className={`h-2 rounded-full transition-all ${
+ memoriesUsed >= memoriesLimit ? "bg-red-500" : isPro ? "bg-green-500" : "bg-blue-500"
+ }`}
+ style={{
+ width: `${Math.min((memoriesUsed / memoriesLimit) * 100, 100)}%`,
+ }}
+ />
+ </div>
+ </div>
+
+ {isPro && (
+ <div className="flex justify-between items-center">
+ <span className="text-sm text-white/70">Connections</span>
+ <span className="text-sm text-white/90">
+ {connectionsUsed} / 10
+ </span>
+ </div>
+ )}
+
+ {/* Billing Actions */}
+ <div className="pt-2">
+ {isPro ? (
+ <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
+ <Button
+ className="w-full bg-white/10 hover:bg-white/20 text-white border-white/20"
+ onClick={handleManageBilling}
+ size="sm"
+ variant="outline"
+ >
+ Manage Billing
+ </Button>
+ </motion.div>
+ ) : (
+ <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
+ <Button
+ className="w-full bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white border-0"
+ disabled={isLoading || isCheckingStatus}
+ onClick={handleUpgrade}
+ size="sm"
+ >
+ {isLoading || isCheckingStatus ? (
+ <>
+ <LoaderIcon className="h-4 w-4 animate-spin mr-2" />
+ Upgrading...
+ </>
+ ) : (
+ "Upgrade to Pro - $15/month"
+ )}
+ </Button>
+ </motion.div>
+ )}
+ </div>
+ </div>
+
+ {/* Plan Comparison - Only show for free users */}
+ {!isPro && (
+ <div className="bg-white/5 rounded-lg p-4 space-y-4">
+ <HeadingH3Bold className="text-white text-sm">Upgrade to Pro</HeadingH3Bold>
+
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ {/* Free Plan */}
+ <div className="p-3 bg-white/5 rounded-lg border border-white/10">
+ <h4 className="font-medium text-white/90 mb-3 text-sm">Free Plan</h4>
+ <ul className="space-y-2">
+ <li className="flex items-center gap-2 text-sm text-white/70">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ 200 memories
+ </li>
+ <li className="flex items-center gap-2 text-sm text-white/70">
+ <X className="h-4 w-4 text-red-400" />
+ No connections
+ </li>
+ <li className="flex items-center gap-2 text-sm text-white/70">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ Basic search
+ </li>
+ </ul>
+ </div>
+
+ {/* Pro Plan */}
+ <div className="p-3 bg-gradient-to-br from-blue-500/10 to-purple-500/10 rounded-lg border border-blue-500/20">
+ <h4 className="font-medium text-white mb-3 flex items-center gap-2 text-sm">
+ Pro Plan
+ <span className="text-xs bg-blue-500/20 text-blue-400 px-2 py-0.5 rounded-full">
+ Recommended
+ </span>
+ </h4>
+ <ul className="space-y-2">
+ <li className="flex items-center gap-2 text-sm text-white/90">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ 5000 memories
+ </li>
+ <li className="flex items-center gap-2 text-sm text-white/90">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ 10 connections
+ </li>
+ <li className="flex items-center gap-2 text-sm text-white/90">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ Advanced search
+ </li>
+ <li className="flex items-center gap-2 text-sm text-white/90">
+ <CheckCircle className="h-4 w-4 text-green-400" />
+ Priority support
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <p className="text-xs text-white/50 text-center">
+ $15/month (only for first 100 users) • Cancel anytime. No questions asked.
+ </p>
+ </div>
+ )}
+
+ <Button
+ className="w-full bg-red-500/20 hover:bg-red-500/30 text-red-200 border-red-500/30"
+ onClick={handleLogout}
+ variant="destructive"
+ >
+ <LogOut className="w-4 h-4 mr-2" />
+ Sign Out
+ </Button>
+ </div>
+ )
+}
diff --git a/apps/web/components/views/projects.tsx b/apps/web/components/views/projects.tsx
new file mode 100644
index 00000000..faa9a317
--- /dev/null
+++ b/apps/web/components/views/projects.tsx
@@ -0,0 +1,742 @@
+"use client"
+
+import { $fetch } from "@lib/api"
+import { Button } from "@repo/ui/components/button"
+
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@repo/ui/components/dialog"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@repo/ui/components/dropdown-menu"
+import { Input } from "@repo/ui/components/input"
+import { Label } from "@repo/ui/components/label"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@repo/ui/components/select"
+import { Skeleton } from "@repo/ui/components/skeleton"
+
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
+
+import { FolderIcon, Loader2, MoreVertical, Plus, Trash2 } from "lucide-react"
+import { AnimatePresence, motion } from "motion/react"
+
+import { useState } from "react"
+import { toast } from "sonner"
+import { useProject } from "@/stores"
+
+// Projects View Component
+export function ProjectsView() {
+ const queryClient = useQueryClient()
+ const { selectedProject, setSelectedProject } = useProject()
+ const [showCreateDialog, setShowCreateDialog] = useState(false)
+ const [projectName, setProjectName] = useState("")
+ const [deleteDialog, setDeleteDialog] = useState<{
+ open: boolean
+ project: null | { id: string; name: string; containerTag: string }
+ action: "move" | "delete"
+ targetProjectId: string
+ }>({
+ open: false,
+ project: null,
+ action: "move",
+ targetProjectId: "",
+ })
+ const [expDialog, setExpDialog] = useState<{
+ open: boolean
+ projectId: string
+ }>({
+ open: false,
+ projectId: "",
+ })
+
+ // Fetch projects
+ const {
+ data: projects = [],
+ isLoading,
+ // error,
+ } = useQuery({
+ queryKey: ["projects"],
+ queryFn: async () => {
+ const response = await $fetch("@get/projects")
+
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to load projects")
+ }
+
+ return response.data?.projects || []
+ },
+ staleTime: 30 * 1000,
+ })
+
+ // Create project mutation
+ const createProjectMutation = useMutation({
+ mutationFn: async (name: string) => {
+ const response = await $fetch("@post/projects", {
+ body: { name },
+ })
+
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to create project")
+ }
+
+ return response.data
+ },
+ onSuccess: () => {
+ toast.success("Project created successfully!")
+ setShowCreateDialog(false)
+ setProjectName("")
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
+ },
+ onError: (error) => {
+ toast.error("Failed to create project", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ })
+
+ // Delete project mutation
+ const deleteProjectMutation = useMutation({
+ mutationFn: async ({
+ projectId,
+ action,
+ targetProjectId,
+ }: {
+ projectId: string
+ action: "move" | "delete"
+ targetProjectId?: string
+ }) => {
+ const response = await $fetch(`@delete/projects/${projectId}`, {
+ body: { action, targetProjectId },
+ })
+
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to delete project")
+ }
+
+ return response.data
+ },
+ onSuccess: () => {
+ toast.success("Project deleted successfully")
+ setDeleteDialog({
+ open: false,
+ project: null,
+ action: "move",
+ targetProjectId: "",
+ })
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
+
+ // If we deleted the selected project, switch to default
+ if (deleteDialog.project?.containerTag === selectedProject) {
+ setSelectedProject("sm_project_default")
+ }
+ },
+ onError: (error) => {
+ toast.error("Failed to delete project", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ })
+
+ // Enable experimental mode mutation
+ const enableExperimentalMutation = useMutation({
+ mutationFn: async (projectId: string) => {
+ const response = await $fetch(`@post/projects/${projectId}/enable-experimental`)
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to enable experimental mode")
+ }
+ return response.data
+ },
+ onSuccess: () => {
+ toast.success("Experimental mode enabled for project")
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
+ setExpDialog({ open: false, projectId: "" })
+ },
+ onError: (error) => {
+ toast.error("Failed to enable experimental mode", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ })
+
+ // Handle project selection
+ const handleProjectSelect = (containerTag: string) => {
+ setSelectedProject(containerTag)
+ toast.success("Project switched successfully")
+ }
+
+ return (
+ <div className="space-y-4">
+ <div className="mb-4">
+ <p className="text-sm text-white/70">
+ Organize your memories into separate projects
+ </p>
+ </div>
+
+ <div className="flex justify-between items-center mb-4">
+ <p className="text-sm text-white/50">Current project:</p>
+ <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ onClick={() => setShowCreateDialog(true)}
+ size="sm"
+ >
+ <Plus className="h-4 w-4 mr-2" />
+ New Project
+ </Button>
+ </motion.div>
+ </div>
+
+ {isLoading ? (
+ <div className="space-y-3">
+ {[...Array(2)].map((_, i) => (
+ <motion.div
+ animate={{ opacity: 1 }}
+ className="p-4 bg-white/5 rounded-lg"
+ initial={{ opacity: 0 }}
+ key={`skeleton-project-${Date.now()}-${i}`}
+ transition={{ delay: i * 0.1 }}
+ >
+ <Skeleton className="h-12 w-full bg-white/10" />
+ </motion.div>
+ ))}
+ </div>
+ ) : projects.length === 0 ? (
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ className="text-center py-8"
+ initial={{ opacity: 0, scale: 0.9 }}
+ transition={{ type: "spring", damping: 20 }}
+ >
+ <p className="text-white/50 mb-4">No projects yet</p>
+ <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ onClick={() => setShowCreateDialog(true)}
+ size="sm"
+ variant="secondary"
+ >
+ Create Your First Project
+ </Button>
+ </motion.div>
+ </motion.div>
+ ) : (
+ <motion.div className="space-y-2">
+ <AnimatePresence>
+ {/* Default project */}
+ <motion.div
+ animate={{ opacity: 1, x: 0 }}
+ className={`flex items-center justify-between p-3 rounded-lg transition-colors cursor-pointer ${selectedProject === "sm_project_default"
+ ? "bg-white/20 border border-white/30"
+ : "bg-white/5 hover:bg-white/10"
+ }`}
+ exit={{ opacity: 0, x: 20 }}
+ initial={{ opacity: 0, x: -20 }}
+ key="default-project"
+ layout
+ onClick={() => handleProjectSelect("sm_project_default")}
+ >
+ <div className="flex items-center gap-3">
+ <motion.div
+ animate={{ rotate: 0, opacity: 1 }}
+ initial={{ rotate: -180, opacity: 0 }}
+ transition={{ delay: 0.1 }}
+ >
+ <FolderIcon className="h-5 w-5 text-white/80" />
+ </motion.div>
+ <div>
+ <p className="font-medium text-white">Default Project</p>
+ <p className="text-sm text-white/60">
+ Your default memory storage
+ </p>
+ </div>
+ </div>
+ {selectedProject === "sm_project_default" && (
+ <motion.div
+ animate={{ scale: 1 }}
+ initial={{ scale: 0 }}
+ transition={{ type: "spring", damping: 20 }}
+ >
+ <div className="w-2 h-2 bg-green-400 rounded-full" />
+ </motion.div>
+ )}
+ </motion.div>
+
+ {/* User projects */}
+ {projects
+ .filter((p) => p.containerTag !== "sm_project_default")
+ .map((project, index) => (
+ <motion.div
+ animate={{ opacity: 1, x: 0 }}
+ className={`flex items-center justify-between p-3 rounded-lg transition-colors cursor-pointer ${selectedProject === project.containerTag
+ ? "bg-white/20 border border-white/30"
+ : "bg-white/5 hover:bg-white/10"
+ }`}
+ exit={{ opacity: 0, x: 20 }}
+ initial={{ opacity: 0, x: -20 }}
+ key={project.id}
+ layout
+ onClick={() => handleProjectSelect(project.containerTag)}
+ transition={{ delay: (index + 1) * 0.05 }}
+ >
+ <div className="flex items-center gap-3">
+ <motion.div
+ animate={{ rotate: 0, opacity: 1 }}
+ initial={{ rotate: -180, opacity: 0 }}
+ transition={{ delay: (index + 1) * 0.05 + 0.2 }}
+ >
+ <FolderIcon className="h-5 w-5 text-white/80" />
+ </motion.div>
+ <div>
+ <p className="font-medium text-white">{project.name}</p>
+ <p className="text-sm text-white/60">
+ Created{" "}
+ {new Date(project.createdAt).toLocaleDateString()}
+ </p>
+ </div>
+ </div>
+ <div className="flex items-center gap-2">
+ {selectedProject === project.containerTag && (
+ <motion.div
+ animate={{ scale: 1 }}
+ initial={{ scale: 0 }}
+ transition={{ type: "spring", damping: 20 }}
+ >
+ <div className="w-2 h-2 bg-green-400 rounded-full" />
+ </motion.div>
+ )}
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ className="text-white/50 hover:text-white"
+ onClick={(e) => e.stopPropagation()}
+ size="icon"
+ variant="ghost"
+ >
+ <MoreVertical className="h-4 w-4" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent
+ align="end"
+ className="bg-black/90 border-white/10"
+ >
+ {/* Show experimental toggle only if NOT experimental and NOT default project */}
+ {!project.isExperimental &&
+ project.containerTag !== "sm_project_default" && (
+ <DropdownMenuItem
+ className="text-blue-400 hover:text-blue-300 cursor-pointer"
+ onClick={(e) => {
+ e.stopPropagation()
+ setExpDialog({
+ open: true,
+ projectId: project.id,
+ })
+ }}
+ >
+ <div className="h-4 w-4 mr-2 rounded border border-blue-400" />
+ Enable Experimental Mode
+ </DropdownMenuItem>
+ )}
+ {project.isExperimental && (
+ <DropdownMenuItem
+ className="text-blue-300/50"
+ disabled
+ >
+ <div className="h-4 w-4 mr-2 rounded bg-blue-400" />
+ Experimental Mode Active
+ </DropdownMenuItem>
+ )}
+ <DropdownMenuItem
+ className="text-red-400 hover:text-red-300 cursor-pointer"
+ onClick={(e) => {
+ e.stopPropagation()
+ setDeleteDialog({
+ open: true,
+ project: {
+ id: project.id,
+ name: project.name,
+ containerTag: project.containerTag,
+ },
+ action: "move",
+ targetProjectId: "",
+ })
+ }}
+ >
+ <Trash2 className="h-4 w-4 mr-2" />
+ Delete Project
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ </div>
+ </motion.div>
+ ))}
+ </AnimatePresence>
+ </motion.div>
+ )}
+
+ {/* Create Project Dialog */}
+ <AnimatePresence>
+ {showCreateDialog && (
+ <Dialog onOpenChange={setShowCreateDialog} open={showCreateDialog}>
+ <DialogContent className="sm:max-w-2xl bg-black/90 backdrop-blur-xl border-white/10 text-white">
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.95 }}
+ initial={{ opacity: 0, scale: 0.95 }}
+ >
+ <DialogHeader>
+ <DialogTitle>Create New Project</DialogTitle>
+ <DialogDescription className="text-white/60">
+ Give your project a unique name
+ </DialogDescription>
+ </DialogHeader>
+ <div className="grid gap-4 py-4">
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex flex-col gap-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.1 }}
+ >
+ <Label htmlFor="projectName">Project Name</Label>
+ <Input
+ className="bg-white/5 border-white/10 text-white"
+ id="projectName"
+ onChange={(e) => setProjectName(e.target.value)}
+ placeholder="My Awesome Project"
+ value={projectName}
+ />
+ <p className="text-xs text-white/50">
+ This will help you organize your memories
+ </p>
+ </motion.div>
+ </div>
+ <DialogFooter>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
+ onClick={() => {
+ setShowCreateDialog(false)
+ setProjectName("")
+ }}
+ type="button"
+ variant="outline"
+ >
+ Cancel
+ </Button>
+ </motion.div>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ disabled={
+ createProjectMutation.isPending || !projectName.trim()
+ }
+ onClick={() => createProjectMutation.mutate(projectName)}
+ type="button"
+ >
+ {createProjectMutation.isPending ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ Creating...
+ </>
+ ) : (
+ "Create Project"
+ )}
+ </Button>
+ </motion.div>
+ </DialogFooter>
+ </motion.div>
+ </DialogContent>
+ </Dialog>
+ )}
+ </AnimatePresence>
+
+ {/* Delete Project Dialog */}
+ <AnimatePresence>
+ {deleteDialog.open && deleteDialog.project && (
+ <Dialog
+ onOpenChange={(open) =>
+ setDeleteDialog((prev) => ({ ...prev, open }))
+ }
+ open={deleteDialog.open}
+ >
+ <DialogContent className="sm:max-w-3xl bg-black/90 backdrop-blur-xl border-white/10 text-white">
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.95 }}
+ initial={{ opacity: 0, scale: 0.95 }}
+ >
+ <DialogHeader>
+ <DialogTitle>Delete Project</DialogTitle>
+ <DialogDescription className="text-white/60">
+ Are you sure you want to delete "{deleteDialog.project.name}
+ "? Choose what to do with the documents in this project.
+ </DialogDescription>
+ </DialogHeader>
+ <div className="grid gap-4 py-4">
+ <div className="space-y-4">
+ <div className="flex items-center space-x-2">
+ <input
+ checked={deleteDialog.action === "move"}
+ className="w-4 h-4"
+ id="move"
+ name="action"
+ onChange={() =>
+ setDeleteDialog((prev) => ({
+ ...prev,
+ action: "move",
+ }))
+ }
+ type="radio"
+ />
+ <Label
+ className="text-white cursor-pointer"
+ htmlFor="move"
+ >
+ Move documents to another project
+ </Label>
+ </div>
+ {deleteDialog.action === "move" && (
+ <motion.div
+ animate={{ opacity: 1, height: "auto" }}
+ className="ml-6"
+ exit={{ opacity: 0, height: 0 }}
+ initial={{ opacity: 0, height: 0 }}
+ >
+ <Select
+ onValueChange={(value) =>
+ setDeleteDialog((prev) => ({
+ ...prev,
+ targetProjectId: value,
+ }))
+ }
+ value={deleteDialog.targetProjectId}
+ >
+ <SelectTrigger className="w-full bg-white/5 border-white/10 text-white">
+ <SelectValue placeholder="Select target project..." />
+ </SelectTrigger>
+ <SelectContent className="bg-black/90 backdrop-blur-xl border-white/10">
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ value="sm_project_default"
+ >
+ Default Project
+ </SelectItem>
+ {projects
+ .filter(
+ (p) =>
+ p.id !== deleteDialog.project?.id &&
+ p.containerTag !== "sm_project_default",
+ )
+ .map((project) => (
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ key={project.id}
+ value={project.id}
+ >
+ {project.name}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </motion.div>
+ )}
+ <div className="flex items-center space-x-2">
+ <input
+ checked={deleteDialog.action === "delete"}
+ className="w-4 h-4"
+ id="delete"
+ name="action"
+ onChange={() =>
+ setDeleteDialog((prev) => ({
+ ...prev,
+ action: "delete",
+ }))
+ }
+ type="radio"
+ />
+ <Label
+ className="text-white cursor-pointer"
+ htmlFor="delete"
+ >
+ Delete all documents in this project
+ </Label>
+ </div>
+ {deleteDialog.action === "delete" && (
+ <motion.p
+ animate={{ opacity: 1 }}
+ className="text-sm text-red-400 ml-6"
+ initial={{ opacity: 0 }}
+ >
+ ⚠️ This action cannot be undone. All documents will be
+ permanently deleted.
+ </motion.p>
+ )}
+ </div>
+ </div>
+ <DialogFooter>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
+ onClick={() =>
+ setDeleteDialog({
+ open: false,
+ project: null,
+ action: "move",
+ targetProjectId: "",
+ })
+ }
+ type="button"
+ variant="outline"
+ >
+ Cancel
+ </Button>
+ </motion.div>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className={`${deleteDialog.action === "delete"
+ ? "bg-red-600 hover:bg-red-700"
+ : "bg-white/10 hover:bg-white/20"
+ } text-white border-white/20`}
+ disabled={
+ deleteProjectMutation.isPending ||
+ (deleteDialog.action === "move" &&
+ !deleteDialog.targetProjectId)
+ }
+ onClick={() => {
+ if (deleteDialog.project) {
+ deleteProjectMutation.mutate({
+ projectId: deleteDialog.project.id,
+ action: deleteDialog.action,
+ targetProjectId:
+ deleteDialog.action === "move"
+ ? deleteDialog.targetProjectId
+ : undefined,
+ })
+ }
+ }}
+ type="button"
+ >
+ {deleteProjectMutation.isPending ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ {deleteDialog.action === "move"
+ ? "Moving..."
+ : "Deleting..."}
+ </>
+ ) : deleteDialog.action === "move" ? (
+ "Move & Delete Project"
+ ) : (
+ "Delete Everything"
+ )}
+ </Button>
+ </motion.div>
+ </DialogFooter>
+ </motion.div>
+ </DialogContent>
+ </Dialog>
+ )}
+ </AnimatePresence>
+
+ {/* Experimental Mode Confirmation Dialog */}
+ <AnimatePresence>
+ {expDialog.open && (
+ <Dialog
+ onOpenChange={(open) => setExpDialog({ ...expDialog, open })}
+ open={expDialog.open}
+ >
+ <DialogContent className="sm:max-w-lg bg-black/90 backdrop-blur-xl border-white/10 text-white">
+ <motion.div
+ animate={{ opacity: 1, scale: 1 }}
+ className="flex flex-col gap-4"
+ exit={{ opacity: 0, scale: 0.95 }}
+ initial={{ opacity: 0, scale: 0.95 }}
+ >
+ <DialogHeader>
+ <DialogTitle className="text-white">
+ Enable Experimental Mode?
+ </DialogTitle>
+ <DialogDescription className="text-white/60">
+ Experimental mode enables beta features and advanced memory
+ relationships for this project.
+ <br />
+ <br />
+ <span className="text-yellow-400 font-medium">
+ Warning:
+ </span>{" "}
+ This action is{" "}
+ <span className="text-red-400 font-bold">irreversible</span>
+ . Once enabled, you cannot return to regular mode for this
+ project.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
+ onClick={() =>
+ setExpDialog({ open: false, projectId: "" })
+ }
+ type="button"
+ variant="outline"
+ >
+ Cancel
+ </Button>
+ </motion.div>
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-blue-600 hover:bg-blue-700 text-white"
+ disabled={enableExperimentalMutation.isPending}
+ onClick={() =>
+ enableExperimentalMutation.mutate(expDialog.projectId)
+ }
+ type="button"
+ >
+ {enableExperimentalMutation.isPending ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ Enabling...
+ </>
+ ) : (
+ "Enable Experimental Mode"
+ )}
+ </Button>
+ </motion.div>
+ </DialogFooter>
+ </motion.div>
+ </DialogContent>
+ </Dialog>
+ )}
+ </AnimatePresence>
+ </div>
+ )
+}
diff --git a/apps/web/env.d.ts b/apps/web/env.d.ts
deleted file mode 100644
index 1ad612bc..00000000
--- a/apps/web/env.d.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { TypeOf } from "zod";
-import { zodEnv } from "~/lib/environment";
-
-declare global {
- namespace NodeJS {
- interface ProcessEnv extends Env, TypeOf<typeof zodEnv> {}
- }
-}
diff --git a/apps/web/eslint.config.mjs b/apps/web/eslint.config.mjs
deleted file mode 100644
index 23aad4b1..00000000
--- a/apps/web/eslint.config.mjs
+++ /dev/null
@@ -1,145 +0,0 @@
-import { fixupConfigRules, fixupPluginRules } from "@eslint/compat";
-import { FlatCompat } from "@eslint/eslintrc";
-import js from "@eslint/js";
-import typescriptEslint from "@typescript-eslint/eslint-plugin";
-import tsParser from "@typescript-eslint/parser";
-import { readGitignoreFiles } from "eslint-gitignore";
-import drizzle from "eslint-plugin-drizzle";
-import _import from "eslint-plugin-import";
-import jsxA11Y from "eslint-plugin-jsx-a11y";
-import react from "eslint-plugin-react";
-import globals from "globals";
-import path from "node:path";
-import { fileURLToPath } from "node:url";
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-const compat = new FlatCompat({
- baseDirectory: __dirname,
- recommendedConfig: js.configs.recommended,
- allConfig: js.configs.all,
-});
-
-// Read .gitignore and create an ignore function
-const gitIgnore = readGitignoreFiles();
-
-export default [
- {
- ignores: [...gitIgnore, "!**/.server", "!**/.client", "build"],
- },
- ...compat.extends("eslint:recommended"),
- {
- languageOptions: {
- globals: {
- ...globals.browser,
- ...globals.commonjs,
- },
-
- ecmaVersion: "latest",
- sourceType: "module",
-
- parserOptions: {
- ecmaFeatures: {
- jsx: true,
- },
- },
- },
- },
- {
- rules: {
- "import/no-named-as-default": "off",
- "import/no-named-as-default-member": "off",
- "react/prop-types": "off", // If using TypeScript, prop-types are less necessary
- "import/namespace": "off", // This seems to be causing issues with module resolution
- },
- },
- ...fixupConfigRules(
- compat.extends(
- "plugin:react/recommended",
- "plugin:react/jsx-runtime",
- "plugin:react-hooks/recommended",
- "plugin:jsx-a11y/recommended"
- ),
- ).map((config) => ({
- ...config,
- files: ["**/*.{js,jsx,ts,tsx}"],
- })),
- {
- files: ["**/*.{js,jsx,ts,tsx}"],
-
- plugins: {
- react: fixupPluginRules(react),
- "jsx-a11y": fixupPluginRules(jsxA11Y),
- },
-
- settings: {
- react: {
- version: "detect",
- },
-
- formComponents: ["Form"],
-
- linkComponents: [
- {
- name: "Link",
- linkAttribute: "to",
- },
- {
- name: "NavLink",
- linkAttribute: "to",
- },
- ],
-
- "import/resolver": {
- typescript: {},
- },
- },
- },
- ...fixupConfigRules(
- compat.extends(
- "plugin:@typescript-eslint/recommended",
- "plugin:import/recommended",
- "plugin:import/typescript",
- "plugin:drizzle/recommended",
- ),
- ).map((config) => ({
- ...config,
- files: ["**/*.{ts,tsx}"],
- })),
- {
- files: ["**/*.{ts,tsx}"],
-
- plugins: {
- "@typescript-eslint": fixupPluginRules(typescriptEslint),
- import: fixupPluginRules(_import),
- drizzle: fixupPluginRules(drizzle),
- },
-
- languageOptions: {
- parser: tsParser,
- },
-
- settings: {
- "import/internal-regex": "^~/",
-
- "import/resolver": {
- node: {
- extensions: [".ts", ".tsx"],
- },
-
- typescript: {
- alwaysTryTypes: true,
- },
- },
- },
- },
- {
- files: ["**/.eslintrc.cjs"],
-
- languageOptions: {
- globals: {
- ...globals.node,
- },
- },
- },
-];
diff --git a/apps/web/functions/[[path]].ts b/apps/web/functions/[[path]].ts
deleted file mode 100644
index 8f5be870..00000000
--- a/apps/web/functions/[[path]].ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import * as build from "../build/server";
-import server from "../server";
-
-import handle from "hono-remix-adapter/cloudflare-pages";
-
-export const onRequest = handle(build, server);
diff --git a/apps/web/functions/notion.ts b/apps/web/functions/notion.ts
deleted file mode 100644
index e69de29b..00000000
--- a/apps/web/functions/notion.ts
+++ /dev/null
diff --git a/apps/web/globals.css b/apps/web/globals.css
new file mode 100644
index 00000000..ab19ef98
--- /dev/null
+++ b/apps/web/globals.css
@@ -0,0 +1,2 @@
+@import "tailwindcss";
+@plugin "@tailwindcss/typography"; \ No newline at end of file
diff --git a/apps/web/hooks/use-project-mutations.ts b/apps/web/hooks/use-project-mutations.ts
new file mode 100644
index 00000000..21e1bb19
--- /dev/null
+++ b/apps/web/hooks/use-project-mutations.ts
@@ -0,0 +1,89 @@
+"use client"
+
+import { $fetch } from "@lib/api"
+import { useMutation, useQueryClient } from "@tanstack/react-query"
+import { toast } from "sonner"
+import { useProject } from "@/stores"
+
+export function useProjectMutations() {
+ const queryClient = useQueryClient()
+ const { selectedProject, setSelectedProject } = useProject()
+
+ const createProjectMutation = useMutation({
+ mutationFn: async (name: string) => {
+ const response = await $fetch("@post/projects", {
+ body: { name },
+ })
+
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to create project")
+ }
+
+ return response.data
+ },
+ onSuccess: (data) => {
+ toast.success("Project created successfully!")
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
+
+ // Automatically switch to the newly created project
+ if (data?.containerTag) {
+ setSelectedProject(data.containerTag)
+ }
+ },
+ onError: (error) => {
+ toast.error("Failed to create project", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ })
+
+ const deleteProjectMutation = useMutation({
+ mutationFn: async ({
+ projectId,
+ action,
+ targetProjectId,
+ }: {
+ projectId: string
+ action: "move" | "delete"
+ targetProjectId?: string
+ }) => {
+ const response = await $fetch(`@delete/projects/${projectId}`, {
+ body: { action, targetProjectId },
+ })
+
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to delete project")
+ }
+
+ return response.data
+ },
+ onSuccess: (_, variables) => {
+ toast.success("Project deleted successfully")
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
+
+ // If we deleted the selected project, switch to default
+ const deletedProject = queryClient.getQueryData<any[]>(["projects"])?.find(
+ p => p.id === variables.projectId
+ )
+ if (deletedProject?.containerTag === selectedProject) {
+ setSelectedProject("sm_project_default")
+ }
+ },
+ onError: (error) => {
+ toast.error("Failed to delete project", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ })
+
+ const switchProject = (containerTag: string) => {
+ setSelectedProject(containerTag)
+ toast.success("Project switched successfully")
+ }
+
+ return {
+ createProjectMutation,
+ deleteProjectMutation,
+ switchProject,
+ }
+}
diff --git a/apps/web/hooks/use-project-name.ts b/apps/web/hooks/use-project-name.ts
new file mode 100644
index 00000000..ef094ff6
--- /dev/null
+++ b/apps/web/hooks/use-project-name.ts
@@ -0,0 +1,26 @@
+"use client"
+
+import { useQueryClient } from "@tanstack/react-query"
+import { useMemo } from "react"
+import { useProject } from "@/stores"
+
+/**
+ * Returns the display name of the currently selected project.
+ * Falls back to the containerTag / id if a matching project record
+ * hasn’t been fetched yet.
+ */
+export function useProjectName() {
+ const { selectedProject } = useProject()
+ const queryClient = useQueryClient()
+
+ // This query is populated by ProjectsView – we just read from the cache.
+ const projects = queryClient.getQueryData(["projects"]) as
+ | Array<{ name: string; containerTag: string }>
+ | undefined
+
+ return useMemo(() => {
+ if (selectedProject === "sm_project_default") return "Default Project"
+ const found = projects?.find((p) => p.containerTag === selectedProject)
+ return found?.name ?? selectedProject
+ }, [projects, selectedProject])
+}
diff --git a/apps/web/hooks/use-resize-observer.ts b/apps/web/hooks/use-resize-observer.ts
new file mode 100644
index 00000000..c03fb83b
--- /dev/null
+++ b/apps/web/hooks/use-resize-observer.ts
@@ -0,0 +1,23 @@
+import { useEffect, useState } from "react";
+
+export default function useResizeObserver<T extends HTMLElement>(
+ ref: React.RefObject<T | null>
+) {
+ const [size, setSize] = useState({ width: 0, height: 0 });
+
+ useEffect(() => {
+ if (!ref.current) return;
+
+ const observer = new ResizeObserver(([entry]) => {
+ setSize({
+ width: entry?.contentRect.width ?? 0,
+ height: entry?.contentRect.height ?? 0,
+ });
+ });
+
+ observer.observe(ref.current);
+ return () => observer.disconnect();
+ }, [ref]);
+
+ return size;
+} \ No newline at end of file
diff --git a/apps/web/instrumentation-client.ts b/apps/web/instrumentation-client.ts
new file mode 100644
index 00000000..4528e4b7
--- /dev/null
+++ b/apps/web/instrumentation-client.ts
@@ -0,0 +1,24 @@
+// This file configures the initialization of Sentry on the client.
+// The added config here will be used whenever a users loads a page in their browser.
+// https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+import * as Sentry from "@sentry/nextjs"
+
+Sentry.init({
+ _experiments: {
+ enableLogs: true,
+ },
+
+ enabled: process.env.NODE_ENV === "production",
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+ dsn: "https://a12ac22517d938dc69f9c5ad67bf8e2d@o4508385422802944.ingest.us.sentry.io/4509454536998913",
+
+ integrations: [Sentry.consoleLoggingIntegration()],
+
+ // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
+ tracesSampleRate: 1,
+})
+
+export const onRouterTransitionStart = Sentry.captureRouterTransitionStart
diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts
new file mode 100644
index 00000000..21fec3ea
--- /dev/null
+++ b/apps/web/lib/analytics.ts
@@ -0,0 +1,44 @@
+import posthog from "posthog-js"
+
+export const analytics = {
+ userSignedOut: () => posthog.capture("user_signed_out"),
+ tourStarted: () => posthog.capture("tour_started"),
+ tourCompleted: () => posthog.capture("tour_completed"),
+ tourSkipped: () => posthog.capture("tour_skipped"),
+
+ memoryAdded: (props: {
+ type: "note" | "link" | "file"
+ project_id?: string
+ content_length?: number
+ file_size?: number
+ file_type?: string
+ }) => posthog.capture("memory_added", props),
+
+ memoryDetailOpened: () => posthog.capture("memory_detail_opened"),
+
+ projectCreated: () => posthog.capture("project_created"),
+
+ newChatStarted: () => posthog.capture("new_chat_started"),
+ chatHistoryViewed: () => posthog.capture("chat_history_viewed"),
+ chatDeleted: () => posthog.capture("chat_deleted"),
+
+ viewModeChanged: (mode: "graph" | "list") =>
+ posthog.capture("view_mode_changed", { mode }),
+
+ documentCardClicked: () => posthog.capture("document_card_clicked"),
+
+ billingViewed: () => posthog.capture("billing_viewed"),
+ upgradeInitiated: () => posthog.capture("upgrade_initiated"),
+ upgradeCompleted: () => posthog.capture("upgrade_completed"),
+ billingPortalOpened: () => posthog.capture("billing_portal_opened"),
+
+ connectionAdded: (provider: string) =>
+ posthog.capture("connection_added", { provider }),
+ connectionDeleted: () => posthog.capture("connection_deleted"),
+ connectionAuthStarted: () => posthog.capture("connection_auth_started"),
+ connectionAuthCompleted: () => posthog.capture("connection_auth_completed"),
+ connectionAuthFailed: () => posthog.capture("connection_auth_failed"),
+
+ mcpViewOpened: () => posthog.capture("mcp_view_opened"),
+ mcpInstallCmdCopied: () => posthog.capture("mcp_install_cmd_copied"),
+} \ No newline at end of file
diff --git a/apps/web/lib/mobile-panel-context.tsx b/apps/web/lib/mobile-panel-context.tsx
new file mode 100644
index 00000000..3b0b1838
--- /dev/null
+++ b/apps/web/lib/mobile-panel-context.tsx
@@ -0,0 +1,32 @@
+"use client"
+
+import { createContext, type ReactNode, useContext, useState } from "react"
+
+type ActivePanel = "menu" | "chat" | null
+
+interface MobilePanelContextType {
+ activePanel: ActivePanel
+ setActivePanel: (panel: ActivePanel) => void
+}
+
+const MobilePanelContext = createContext<MobilePanelContextType | undefined>(
+ undefined,
+)
+
+export function MobilePanelProvider({ children }: { children: ReactNode }) {
+ const [activePanel, setActivePanel] = useState<ActivePanel>(null)
+
+ return (
+ <MobilePanelContext.Provider value={{ activePanel, setActivePanel }}>
+ {children}
+ </MobilePanelContext.Provider>
+ )
+}
+
+export function useMobilePanel() {
+ const context = useContext(MobilePanelContext)
+ if (!context) {
+ throw new Error("useMobilePanel must be used within a MobilePanelProvider")
+ }
+ return context
+}
diff --git a/apps/web/lib/tour-constants.ts b/apps/web/lib/tour-constants.ts
new file mode 100644
index 00000000..b61fc1e7
--- /dev/null
+++ b/apps/web/lib/tour-constants.ts
@@ -0,0 +1,23 @@
+// Tour step IDs - these should match the IDs added to elements in your app
+export const TOUR_STEP_IDS = {
+ LOGO: "tour-logo",
+ MENU_BUTTON: "tour-menu-button",
+ VIEW_TOGGLE: "tour-view-toggle",
+ MEMORY_GRAPH: "tour-memory-graph",
+ MEMORY_LIST: "tour-memory-list",
+ FLOATING_CHAT: "tour-floating-chat",
+ ADD_MEMORY: "tour-add-memory",
+ SPACES_DROPDOWN: "tour-spaces-dropdown",
+ SETTINGS: "tour-settings",
+ MENU_CONNECTIONS: "tour-connections",
+ // Menu items
+ MENU_ADD_MEMORY: "tour-menu-add-memory",
+ MENU_PROJECTS: "tour-menu-projects",
+ MENU_MCP: "tour-menu-mcp",
+ MENU_BILLING: "tour-menu-billing",
+ // Legend
+ LEGEND: "tour-legend",
+} as const
+
+// Tour storage key for localStorage
+export const TOUR_STORAGE_KEY = "supermemory-tour-completed"
diff --git a/apps/web/lib/view-mode-context.tsx b/apps/web/lib/view-mode-context.tsx
new file mode 100644
index 00000000..87c11da1
--- /dev/null
+++ b/apps/web/lib/view-mode-context.tsx
@@ -0,0 +1,96 @@
+"use client"
+
+import {
+ createContext,
+ type ReactNode,
+ useContext,
+ useEffect,
+ useState,
+} from "react"
+import { analytics } from "@/lib/analytics"
+
+type ViewMode = "graph" | "list"
+
+interface ViewModeContextType {
+ viewMode: ViewMode
+ setViewMode: (mode: ViewMode) => void
+ isInitialized: boolean
+}
+
+const ViewModeContext = createContext<ViewModeContextType | undefined>(
+ undefined,
+)
+
+// Cookie utility functions
+const setCookie = (name: string, value: string, days = 365) => {
+ if (typeof document === "undefined") return
+ const expires = new Date()
+ expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000)
+ document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`
+}
+
+const getCookie = (name: string): string | null => {
+ if (typeof document === "undefined") return null
+ const nameEQ = `${name}=`
+ const ca = document.cookie.split(";")
+ for (let i = 0; i < ca.length; i++) {
+ let c = ca[i]
+ if (!c) continue
+ while (c.charAt(0) === " ") c = c.substring(1, c.length)
+ if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length)
+ }
+ return null
+}
+
+const isMobileDevice = () => {
+ if (typeof window === "undefined") return false
+ return window.innerWidth < 768
+}
+
+export function ViewModeProvider({ children }: { children: ReactNode }) {
+ // Start with a default that works for SSR
+ const [viewMode, setViewModeState] = useState<ViewMode>("graph")
+ const [isInitialized, setIsInitialized] = useState(false)
+
+ // Load preferences on the client side
+ useEffect(() => {
+ if (!isInitialized) {
+ // Check for saved preference first
+ const savedMode = getCookie("memoryViewMode")
+ if (savedMode === "list" || savedMode === "graph") {
+ setViewModeState(savedMode)
+ } else {
+ // If no saved preference, default to list on mobile, graph on desktop
+ setViewModeState(isMobileDevice() ? "list" : "graph")
+ }
+ setIsInitialized(true)
+ }
+ }, [isInitialized])
+
+ // Save to cookie whenever view mode changes
+ const handleSetViewMode = (mode: ViewMode) => {
+ analytics.viewModeChanged(mode)
+ setViewModeState(mode)
+ setCookie("memoryViewMode", mode)
+ }
+
+ return (
+ <ViewModeContext.Provider
+ value={{
+ viewMode,
+ setViewMode: handleSetViewMode,
+ isInitialized,
+ }}
+ >
+ {children}
+ </ViewModeContext.Provider>
+ )
+}
+
+export function useViewMode() {
+ const context = useContext(ViewModeContext)
+ if (!context) {
+ throw new Error("useViewMode must be used within a ViewModeProvider")
+ }
+ return context
+}
diff --git a/apps/web/load-context.ts b/apps/web/load-context.ts
deleted file mode 100644
index 319b0bfa..00000000
--- a/apps/web/load-context.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { type PlatformProxy } from "wrangler";
-
-type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;
-
-declare module "@remix-run/cloudflare" {
- interface AppLoadContext {
- cloudflare: Cloudflare;
- }
-}
diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts
new file mode 100644
index 00000000..068b8859
--- /dev/null
+++ b/apps/web/middleware.ts
@@ -0,0 +1,50 @@
+import { $fetch } from "@lib/api"
+import { getSessionCookie } from "better-auth/cookies"
+import { NextResponse } from "next/server"
+
+export default async function middleware(request: Request) {
+ console.debug("[MIDDLEWARE] === MIDDLEWARE START ===")
+ const url = new URL(request.url)
+ console.debug("[MIDDLEWARE] Path:", url.pathname)
+ console.debug("[MIDDLEWARE] Method:", request.method)
+
+ const sessionCookie = getSessionCookie(request)
+ console.debug("[MIDDLEWARE] Session cookie exists:", !!sessionCookie)
+
+ // Always allow access to login and waitlist pages
+ const publicPaths = ["/login"]
+ if (publicPaths.includes(url.pathname)) {
+ console.debug("[MIDDLEWARE] Public path, allowing access")
+ return NextResponse.next()
+ }
+
+ // If no session cookie and not on a public path, redirect to login
+ if (!sessionCookie) {
+ console.debug(
+ "[MIDDLEWARE] No session cookie and not on public path, redirecting to /login",
+ )
+ return NextResponse.redirect(new URL("/login", request.url))
+ }
+
+ if (url.pathname !== "/waitlist") {
+ const response = await $fetch("@get/waitlist/status", {
+ headers: {
+ Authorization: `Bearer ${sessionCookie}`,
+ },
+ })
+ console.debug("[MIDDLEWARE] Waitlist status:", response.data)
+ if (response.data && !response.data.accessGranted) {
+ return NextResponse.redirect(new URL("/waitlist", request.url))
+ }
+ }
+
+ console.debug("[MIDDLEWARE] Passing through to next handler")
+ console.debug("[MIDDLEWARE] === MIDDLEWARE END ===")
+ return NextResponse.next()
+}
+
+export const config = {
+ matcher: [
+ "/((?!_next/static|_next/image|images|icon.png|monitoring|opengraph-image.png|ingest|api|login|api/emails).*)",
+ ],
+}
diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts
new file mode 100644
index 00000000..ba7cbc85
--- /dev/null
+++ b/apps/web/next.config.ts
@@ -0,0 +1,60 @@
+import { withSentryConfig } from "@sentry/nextjs"
+import type { NextConfig } from "next"
+
+const nextConfig: NextConfig = {
+ experimental: {
+ reactCompiler: true,
+ viewTransition: true,
+ },
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+ poweredByHeader: false,
+ async rewrites() {
+ return [
+ {
+ source: "/ingest/static/:path*",
+ destination: "https://us-assets.i.posthog.com/static/:path*",
+ },
+ {
+ source: "/ingest/:path*",
+ destination: "https://us.i.posthog.com/:path*",
+ },
+ ]
+ },
+ skipTrailingSlashRedirect: true,
+}
+
+export default withSentryConfig(nextConfig, {
+ // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
+ // See the following for more information:
+ // https://docs.sentry.io/product/crons/
+ // https://vercel.com/docs/cron-jobs
+ automaticVercelMonitors: true,
+
+ // Automatically tree-shake Sentry logger statements to reduce bundle size
+ disableLogger: true,
+ // For all available options, see:
+ // https://www.npmjs.com/package/@sentry/webpack-plugin#options
+
+ org: "supermemory",
+ project: "consumer",
+
+ // Only print logs for uploading source maps in CI
+ silent: !process.env.CI,
+
+ // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
+ // This can increase your server load as well as your hosting bill.
+ // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
+ // side errors will fail.
+ tunnelRoute: "/monitoring",
+
+ // For all available options, see:
+ // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
+
+ // Upload a larger set of source maps for prettier stack traces (increases build time)
+ widenClientFileUpload: true,
+})
+
+import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"
+initOpenNextCloudflareForDev()
diff --git a/apps/web/open-next.config.ts b/apps/web/open-next.config.ts
new file mode 100644
index 00000000..544307a9
--- /dev/null
+++ b/apps/web/open-next.config.ts
@@ -0,0 +1,6 @@
+import { defineCloudflareConfig } from "@opennextjs/cloudflare";
+import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
+
+export default defineCloudflareConfig({
+ incrementalCache: r2IncrementalCache,
+});
diff --git a/apps/web/package.json b/apps/web/package.json
index 51841eb6..6aca8ffb 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,176 +1,92 @@
{
- "name": "supermemory",
- "private": true,
- "sideEffects": true,
- "type": "module",
- "engineStrict": true,
- "packageManager": "[email protected]",
- "scripts": {
- "build": "remix vite:build",
- "cf-typegen": "wrangler types",
- "deploy": "cross-env NODE_ENV=production IS_DEPLOYING=true dotenv -- bun run build && wrangler pages deploy",
- "dev": "dotenv -- wrangler -v && remix vite:dev --port 3000",
- "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
- "preview": "dotenv -- bun run build && wrangler pages dev",
- "start": "dotenv -- wrangler pages dev ./build/client",
- "typecheck": "tsc",
- "typegen": "wrangler types",
- "tail": "bunx wrangler tail"
- },
- "dependencies": {
- "@ai-sdk/openai": "^0.0.72",
- "@ariakit/react": "^0.4.13",
- "@faker-js/faker": "^9.2.0",
- "@fontsource/geist-sans": "^5.1.0",
- "@million/lint": "^1.0.8",
- "@monaco-editor/react": "^4.6.0",
- "@radix-ui/colors": "^3.0.0",
- "@radix-ui/react-avatar": "^1.1.1",
- "@radix-ui/react-checkbox": "^1.1.2",
- "@radix-ui/react-context-menu": "^2.2.2",
- "@radix-ui/react-dialog": "^1.1.2",
- "@radix-ui/react-dropdown-menu": "^2.1.2",
- "@radix-ui/react-icons": "^1.3.0",
- "@radix-ui/react-label": "^2.1.0",
- "@radix-ui/react-popover": "^1.1.2",
- "@radix-ui/react-select": "^2.1.2",
- "@radix-ui/react-separator": "^1.1.0",
- "@radix-ui/react-slot": "^1.1.0",
- "@radix-ui/react-switch": "^1.1.1",
- "@radix-ui/react-tabs": "^1.1.1",
- "@radix-ui/react-toolbar": "^1.1.0",
- "@radix-ui/react-tooltip": "^1.1.4",
- "@remix-run/cloudflare": "^2.12.1",
- "@remix-run/cloudflare-pages": "^2.12.1",
- "@remix-run/css-bundle": "^2.12.1",
- "@remix-run/dev": "^2.12.1",
- "@remix-run/react": "^2.12.1",
- "@stripe/react-stripe-js": "^3.1.1",
- "@stripe/stripe-js": "^5.5.0",
- "@supermemory/db": "workspace:*",
- "@trivago/prettier-plugin-sort-imports": "^4.3.0",
- "@types/better-sqlite3": "^7.6.11",
- "@types/prismjs": "^1.26.5",
- "@types/react-virtualized": "^9.21.30",
- "@udecode/cn": "^39.0.0",
- "@udecode/plate-ai": "^40.0.0",
- "@udecode/plate-alignment": "^40.0.0",
- "@udecode/plate-autoformat": "^40.0.0",
- "@udecode/plate-basic-elements": "^41.0.0",
- "@udecode/plate-basic-marks": "^40.0.0",
- "@udecode/plate-block-quote": "^40.0.0",
- "@udecode/plate-break": "^40.0.0",
- "@udecode/plate-callout": "^40.0.0",
- "@udecode/plate-caption": "^40.0.0",
- "@udecode/plate-code-block": "^40.0.0",
- "@udecode/plate-combobox": "^40.0.0",
- "@udecode/plate-comments": "^40.0.0",
- "@udecode/plate-common": "^40.0.0",
- "@udecode/plate-date": "^40.0.0",
- "@udecode/plate-dnd": "^40.0.0",
- "@udecode/plate-docx": "^40.0.0",
- "@udecode/plate-excalidraw": "^40.0.0",
- "@udecode/plate-floating": "^40.0.0",
- "@udecode/plate-font": "^40.0.0",
- "@udecode/plate-heading": "^40.0.0",
- "@udecode/plate-highlight": "^40.0.0",
- "@udecode/plate-horizontal-rule": "^40.0.0",
- "@udecode/plate-indent": "^40.0.0",
- "@udecode/plate-indent-list": "^40.0.0",
- "@udecode/plate-juice": "^40.0.0",
- "@udecode/plate-kbd": "^40.0.0",
- "@udecode/plate-layout": "^40.0.0",
- "@udecode/plate-line-height": "^40.0.0",
- "@udecode/plate-link": "^40.0.0",
- "@udecode/plate-markdown": "^40.0.0",
- "@udecode/plate-math": "^40.0.0",
- "@udecode/plate-media": "^40.0.0",
- "@udecode/plate-mention": "^40.0.0",
- "@udecode/plate-node-id": "^40.0.0",
- "@udecode/plate-reset-node": "^40.0.0",
- "@udecode/plate-resizable": "^40.0.0",
- "@udecode/plate-select": "^40.0.0",
- "@udecode/plate-selection": "^40.0.0",
- "@udecode/plate-slash-command": "^40.0.0",
- "@udecode/plate-table": "^40.0.0",
- "@udecode/plate-toggle": "^40.0.0",
- "@udecode/plate-trailing-block": "^40.0.0",
- "@vitejs/plugin-react": "^4.3.2",
- "@vitejs/plugin-react-swc": "^3.7.1",
- "ai": "^4.0.18",
- "cheerio": "^1.0.0",
- "class-variance-authority": "^0.7.0",
- "clsx": "^2.1.1",
- "cmdk": "^1.0.4",
- "date-fns": "^4.1.0",
- "dotenv": "^16.4.7",
- "dotenv-cli": "^7.4.2",
- "drizzle-kit": "^0.24.2",
- "drizzle-orm": "^0.33.0",
- "esbuild-plugin-react-virtualized": "^1.0.4",
- "eslint-config-prettier": "^9.1.0",
- "eslint-plugin-drizzle": "^0.2.3",
- "eslint-plugin-prettier": "^5.2.1",
- "fast-average-color": "^9.4.0",
- "framer-motion": "^11.0.0",
- "globals": "^15.10.0",
- "hono-remix-adapter": "^0.2.2",
- "iron-session": "^8.0.3",
- "isbot": "^5.1.17",
- "lucide-react": "^0.456.0",
- "masonic": "^4.0.1",
- "postgres": "^3.4.5",
- "prettier-eslint": "^16.3.0",
- "prismjs": "^1.29.0",
- "react": "^18.3.1",
- "react-day-picker": "8.10.1",
- "react-dnd": "^16.0.1",
- "react-dnd-html5-backend": "^16.0.1",
- "react-dom": "^18.3.1",
- "react-intersection-observer": "^9.13.1",
- "react-lite-youtube-embed": "^2.4.0",
- "react-resizable-panels": "^2.1.6",
- "react-tweet": "^3.2.1",
- "remix": "^2.15.2",
- "remix-utils": "^7.7.0",
- "slate": "^0.110.2",
- "slate-dom": "^0.111.0",
- "slate-history": "^0.110.3",
- "slate-hyperscript": "^0.100.0",
- "slate-react": "^0.111.0",
- "sonner": "^1.7.0",
- "stripe": "^17.5.0",
- "tailwind-merge": "^2.5.2",
- "tailwind-scrollbar-hide": "^1.1.7",
- "tailwindcss-animate": "^1.0.7",
- "tailwindcss-vite-plugin": "^0.0.7",
- "vaul": "^1.1.1",
- "zod": "^3.23.8"
- },
- "devDependencies": {
- "@cloudflare/workers-types": "^4.20240925.0",
- "@eslint/compat": "^1.1.1",
- "@typescript-eslint/eslint-plugin": "^8.8.0",
- "@typescript-eslint/parser": "^8.8.0",
- "autoprefixer": "^10.4.20",
- "cross-env": "^7.0.3",
- "eslint": "^9.11.1",
- "eslint-gitignore": "^0.1.0",
- "eslint-import-resolver-typescript": "^3.6.3",
- "eslint-plugin-import": "^2.31.0",
- "eslint-plugin-jsx-a11y": "^6.10.0",
- "eslint-plugin-react": "^7.37.1",
- "eslint-plugin-react-hooks": "^4.6.2",
- "postcss": "^8.4.47",
- "prettier": "^3.3.3",
- "prettier-plugin-tailwindcss": "^0.6.8",
- "remix-flat-routes": "^0.6.5",
- "tailwindcss": "^3.4.13",
- "typescript": "^5.6.2",
- "vite": "^5.4.8",
- "vite-tsconfig-paths": "^5.0.1"
- },
- "engines": {
- "node": ">=20.0.0"
- }
+ "name": "@repo/web",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint",
+ "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
+ "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
+ "upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
+ "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
+ },
+ "dependencies": {
+ "@ai-sdk/google": "^2.0.0-beta.13",
+ "@ai-sdk/react": "2.0.0-beta.24",
+ "@better-fetch/fetch": "^1.1.18",
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/modifiers": "^9.0.0",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
+ "@opennextjs/cloudflare": "^1.6.1",
+ "@radix-ui/react-accordion": "^1.2.11",
+ "@radix-ui/react-alert-dialog": "^1.1.14",
+ "@radix-ui/react-avatar": "^1.1.10",
+ "@radix-ui/react-checkbox": "^1.3.2",
+ "@radix-ui/react-dialog": "^1.1.14",
+ "@radix-ui/react-dropdown-menu": "^2.1.15",
+ "@radix-ui/react-label": "^2.1.7",
+ "@radix-ui/react-progress": "^1.1.7",
+ "@radix-ui/react-select": "^2.2.5",
+ "@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-tabs": "^1.1.12",
+ "@radix-ui/react-toggle": "^1.1.9",
+ "@radix-ui/react-toggle-group": "^1.1.10",
+ "@radix-ui/react-tooltip": "^1.2.7",
+ "@react-router/fs-routes": "^7.6.2",
+ "@react-router/node": "^7.6.2",
+ "@react-router/serve": "^7.6.2",
+ "@sentry/nextjs": "^9.33.0",
+ "@tabler/icons-react": "^3.34.0",
+ "@tailwindcss/typography": "^0.5.16",
+ "@tanstack/react-form": "^1.12.4",
+ "@tanstack/react-query": "^5.81.2",
+ "@tanstack/react-query-devtools": "^5.84.2",
+ "@tanstack/react-table": "^8.21.3",
+ "@tanstack/react-virtual": "^3.13.12",
+ "ai": "5.0.0-beta.24",
+ "autumn-js": "0.0.116",
+ "babel-plugin-react-compiler": "^19.1.0-rc.2",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "date-fns": "^4.1.0",
+ "dotenv": "^16.6.0",
+ "embla-carousel-autoplay": "^8.6.0",
+ "embla-carousel-react": "^8.6.0",
+ "isbot": "^5.1.28",
+ "lucide-react": "^0.525.0",
+ "motion": "^12.19.2",
+ "next": "15.3.0",
+ "next-themes": "^0.4.6",
+ "nuqs": "^2.4.3",
+ "posthog-js": "^1.257.0",
+ "random-word-slugs": "^0.1.7",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0",
+ "react-dropzone": "^14.3.8",
+ "react-error-boundary": "^6.0.0",
+ "react-markdown": "^10.1.0",
+ "recharts": "2",
+ "remark-gfm": "^4.0.1",
+ "shadcn-dropzone": "^0.2.1",
+ "sonner": "^2.0.5",
+ "tailwind-merge": "^3.3.1",
+ "tw-animate-css": "^1.3.4",
+ "valibot": "^1.1.0",
+ "vaul": "^1.1.2",
+ "zustand": "^5.0.7"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4.1.11",
+ "@total-typescript/tsconfig": "^1.0.4",
+ "@types/node": "^24.0.4",
+ "@types/react": "^19.1.8",
+ "@types/react-dom": "^19.1.6",
+ "tailwindcss": "^4.1.11",
+ "typescript": "^5.8.3",
+ "wrangler": "^4.26.0"
+ }
}
diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js
deleted file mode 100644
index 7b75c83a..00000000
--- a/apps/web/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-};
diff --git a/apps/web/postcss.config.mjs b/apps/web/postcss.config.mjs
new file mode 100644
index 00000000..78452aad
--- /dev/null
+++ b/apps/web/postcss.config.mjs
@@ -0,0 +1,5 @@
+const config = {
+ plugins: ["@tailwindcss/postcss"],
+}
+
+export default config
diff --git a/apps/web/public/_headers b/apps/web/public/_headers
index d6624656..e6320ab1 100644
--- a/apps/web/public/_headers
+++ b/apps/web/public/_headers
@@ -1,6 +1,2 @@
-/favicon.ico
- Cache-Control: public, max-age=3600, s-maxage=3600
-/assets/*
- Cache-Control: public, max-age=31536000, immutable
-/public/*
- Cache-Control: public, max-age=31536000, immutable
+/_next/static/*
+ Cache-Control: public,max-age=31536000,immutable \ No newline at end of file
diff --git a/apps/web/public/_routes.json b/apps/web/public/_routes.json
deleted file mode 100644
index 9d54863e..00000000
--- a/apps/web/public/_routes.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "version": 1,
- "include": ["/*"],
- "exclude": [
- "/favicon.ico",
- "/assets/*",
- "/robots.txt",
- "/site.webmanifest",
- "/logo-dark.svg",
- "/apple-touch-icon.png",
- "/favicon-16x16.png",
- "/favicon-32x32.png",
- "/android-chrome-192x192.png",
- "/android-chrome-512x512.png"
- ]
-}
diff --git a/apps/web/public/android-chrome-192x192.png b/apps/web/public/android-chrome-192x192.png
deleted file mode 100644
index 13f7b2e6..00000000
--- a/apps/web/public/android-chrome-192x192.png
+++ /dev/null
Binary files differ
diff --git a/apps/web/public/android-chrome-512x512.png b/apps/web/public/android-chrome-512x512.png
deleted file mode 100644
index 3cf4720f..00000000
--- a/apps/web/public/android-chrome-512x512.png
+++ /dev/null
Binary files differ
diff --git a/apps/web/public/apple-touch-icon.png b/apps/web/public/apple-touch-icon.png
deleted file mode 100644
index fd3dd8d3..00000000
--- a/apps/web/public/apple-touch-icon.png
+++ /dev/null
Binary files differ
diff --git a/apps/web/public/favicon-16x16.png b/apps/web/public/favicon-16x16.png
deleted file mode 100644
index 782df488..00000000
--- a/apps/web/public/favicon-16x16.png
+++ /dev/null
Binary files differ
diff --git a/apps/web/public/favicon-32x32.png b/apps/web/public/favicon-32x32.png
deleted file mode 100644
index 2f3572c5..00000000
--- a/apps/web/public/favicon-32x32.png
+++ /dev/null
Binary files differ
diff --git a/apps/web/public/favicon.ico b/apps/web/public/favicon.ico
deleted file mode 100644
index 90d7aafc..00000000
--- a/apps/web/public/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/apps/web/public/images/login-carousel-1.png b/apps/web/public/images/login-carousel-1.png
new file mode 100644
index 00000000..0126a573
--- /dev/null
+++ b/apps/web/public/images/login-carousel-1.png
Binary files differ
diff --git a/apps/web/public/images/login-carousel-2.png b/apps/web/public/images/login-carousel-2.png
new file mode 100644
index 00000000..15c9eb63
--- /dev/null
+++ b/apps/web/public/images/login-carousel-2.png
Binary files differ
diff --git a/apps/web/public/images/logo.png b/apps/web/public/images/logo.png
new file mode 100644
index 00000000..55a25aa8
--- /dev/null
+++ b/apps/web/public/images/logo.png
Binary files differ
diff --git a/apps/web/public/images/sidebar.png b/apps/web/public/images/sidebar.png
new file mode 100644
index 00000000..bf29880f
--- /dev/null
+++ b/apps/web/public/images/sidebar.png
Binary files differ
diff --git a/apps/web/public/landing-page.jpeg b/apps/web/public/landing-page.jpeg
new file mode 100644
index 00000000..48e138d9
--- /dev/null
+++ b/apps/web/public/landing-page.jpeg
Binary files differ
diff --git a/apps/web/public/mcp-supported-tools/claude.png b/apps/web/public/mcp-supported-tools/claude.png
new file mode 100644
index 00000000..00d2d63e
--- /dev/null
+++ b/apps/web/public/mcp-supported-tools/claude.png
Binary files differ
diff --git a/apps/web/public/mcp-supported-tools/cline.png b/apps/web/public/mcp-supported-tools/cline.png
new file mode 100644
index 00000000..1223f889
--- /dev/null
+++ b/apps/web/public/mcp-supported-tools/cline.png
Binary files differ
diff --git a/apps/web/public/mcp-supported-tools/cursor.png b/apps/web/public/mcp-supported-tools/cursor.png
new file mode 100644
index 00000000..e2562174
--- /dev/null
+++ b/apps/web/public/mcp-supported-tools/cursor.png
Binary files differ
diff --git a/apps/web/public/mcp-supported-tools/gemini-cli.png b/apps/web/public/mcp-supported-tools/gemini-cli.png
new file mode 100644
index 00000000..04217c09
--- /dev/null
+++ b/apps/web/public/mcp-supported-tools/gemini-cli.png
Binary files differ
diff --git a/apps/web/public/mcp-supported-tools/vscode.png b/apps/web/public/mcp-supported-tools/vscode.png
new file mode 100644
index 00000000..7bf539c8
--- /dev/null
+++ b/apps/web/public/mcp-supported-tools/vscode.png
Binary files differ
diff --git a/apps/web/public/og-image.png b/apps/web/public/og-image.png
deleted file mode 100644
index 62e30675..00000000
--- a/apps/web/public/og-image.png
+++ /dev/null
Binary files differ
diff --git a/apps/web/public/product-of-the-day.png b/apps/web/public/product-of-the-day.png
deleted file mode 100644
index 9381c6a6..00000000
--- a/apps/web/public/product-of-the-day.png
+++ /dev/null
Binary files differ
diff --git a/apps/web/public/siri.webp b/apps/web/public/siri.webp
deleted file mode 100644
index da1b3452..00000000
--- a/apps/web/public/siri.webp
+++ /dev/null
Binary files differ
diff --git a/apps/web/public/site.webmanifest b/apps/web/public/site.webmanifest
deleted file mode 100644
index 45dc8a20..00000000
--- a/apps/web/public/site.webmanifest
+++ /dev/null
@@ -1 +0,0 @@
-{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file
diff --git a/apps/web/server/index.ts b/apps/web/server/index.ts
deleted file mode 100644
index 0cea72e7..00000000
--- a/apps/web/server/index.ts
+++ /dev/null
@@ -1,420 +0,0 @@
-import { AppLoadContext } from "@remix-run/cloudflare";
-
-import * as cheerio from "cheerio";
-import { createOpenAI } from "@ai-sdk/openai";
-import { zValidator } from "@hono/zod-validator";
-import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
-import { convertToCoreMessages, generateObject, generateText, streamText } from "ai";
-import { putEncryptedKV } from "encrypt-workers-kv";
-import { Hono } from "hono";
-import { z } from "zod";
-
-const app = new Hono<{ Bindings: Env }>();
-
-app.onError(async (err, c) => {
- return c.text(err.message, { status: 500 });
-});
-
-app.get("/api/metadata", async (c) => {
- const url = c.req.query("url");
- if (!url) {
- return c.text("URL is required", { status: 400 });
- }
-
- const cacheKey = `metadata:${url}`;
-
- // Try to get cached metadata
- const cachedMetadata = await c.env.METADATA_KV.get(cacheKey, "json");
- if (cachedMetadata) {
- return c.json(cachedMetadata);
- }
-
- // If not cached, fetch and parse metadata
- try {
- const response = await fetch(url);
- const html = await response.text();
- const $ = cheerio.load(html);
-
- // Try multiple image selectors in order of preference
- const image =
- $('meta[property="og:image"]').attr("content") ||
- $('meta[property="twitter:image"]').attr("content") ||
- $('meta[name="thumbnail"]').attr("content") ||
- $('link[rel="image_src"]').attr("href") ||
- $('img[itemprop="image"]').attr("src") ||
- $("img").first().attr("src") ||
- "";
-
- // Convert relative image URLs to absolute
- const absoluteImage = image ? new URL(image, url).toString() : "";
-
- const metadata = {
- title: $("title").text() || $('meta[property="og:title"]').attr("content") || "",
- description:
- $('meta[name="description"]').attr("content") ||
- $('meta[property="og:description"]').attr("content") ||
- "",
- image: absoluteImage,
- };
-
- // Cache the metadata
- await c.env.METADATA_KV.put(cacheKey, JSON.stringify(metadata), {
- expirationTtl: 7 * 24 * 60 * 60,
- }); // 7 days TTL
-
- return c.json(metadata);
- } catch (error) {
- console.error("Error fetching metadata:", error);
- return c.json({ error: "Failed to fetch metadata" }, 500);
- }
-});
-
-app.get("/api/session", async (c) => {
- const fakeContext = {
- cloudflare: {
- env: c.env,
- },
- };
- const session = await getSessionFromRequest(c.req.raw, fakeContext as AppLoadContext);
- if (!session) {
- return c.json({ error: "No session found" }, 401);
- }
- return c.json(session);
-});
-
-app.all("/backend/*", async (c) => {
- const backendUrl = c.env.BACKEND_URL ?? "https://supermemory-backend.dhravya.workers.dev";
- const path = c.req.path.replace("/backend", "");
- const searchParams = new URL(c.req.url).searchParams.toString();
- const queryString = searchParams ? `?${searchParams}` : "";
- const url = `${backendUrl}${path}${queryString}`;
-
- const headers = new Headers(c.req.raw.headers);
- headers.delete("host");
-
- let body;
- if (c.req.raw.body) {
- try {
- // Use tee() to create a copy of the body stream
- const [stream1, stream2] = c.req.raw.body.tee();
-
- const reader = stream2.getReader();
- const chunks = [];
- let done = false;
-
- while (!done) {
- const { value, done: isDone } = await reader.read();
- if (value) {
- chunks.push(value);
- }
- done = isDone;
- }
-
- const bodyText = new TextDecoder().decode(
- chunks.reduce((acc, chunk) => {
- const tmp = new Uint8Array(acc.length + chunk.length);
- tmp.set(acc);
- tmp.set(chunk, acc.length);
- return tmp;
- }, new Uint8Array(0)),
- );
-
- if (c.req.method === "POST") {
- try {
- const parsedBody = JSON.parse(bodyText);
- body = JSON.stringify(parsedBody);
- } catch (e) {
- console.error("Invalid JSON in request body:", bodyText);
- return c.json(
- {
- error: "Invalid JSON in request body",
- details: bodyText.substring(0, 100) + "...", // Show partial body for debugging
- },
- 400,
- );
- }
- } else {
- body = bodyText;
- }
- } catch (error) {
- console.error("Error reading request body:", error);
- return c.json(
- {
- error: "Failed to process request body",
- details: error instanceof Error ? error.message : "Unknown error",
- path: path,
- },
- 400,
- );
- }
- }
-
- const fetchOptions: RequestInit = {
- method: c.req.method,
- headers: headers,
- ...(body && { body }),
- };
-
- if (c.req.method !== "GET" && c.req.method !== "HEAD") {
- (fetchOptions as any).duplex = "half";
- }
-
- try {
- const response = await fetch(url, fetchOptions);
-
- const newHeaders = new Headers(response.headers);
- newHeaders.delete("set-cookie");
-
- const setCookieHeaders = response.headers.get("set-cookie");
- if (setCookieHeaders) {
- setCookieHeaders.split(", ").forEach((cookie: string) => {
- c.header("set-cookie", cookie);
- });
- }
-
- if (response.headers.get("content-type")?.includes("text/event-stream")) {
- return new Response(response.body, {
- status: response.status,
- statusText: response.statusText,
- headers: {
- ...newHeaders,
- "content-type": "text/event-stream",
- "cache-control": "no-cache",
- connection: "keep-alive",
- },
- });
- }
-
- if (response.body && response.headers.get("content-type")?.includes("text/x-unknown")) {
- return new Response(response.body, {
- status: response.status,
- statusText: response.statusText,
- headers: newHeaders,
- });
- }
-
- let responseBody;
- try {
- const responseText = await response.text();
- try {
- responseBody = JSON.parse(responseText);
- } catch {
- responseBody = responseText;
- }
- } catch (error) {
- console.error("Error reading response:", error);
- return c.json(
- {
- error: "Failed to read backend response",
- details: error instanceof Error ? error.message : "Unknown error",
- path: path,
- },
- 502,
- );
- }
-
- return new Response(JSON.stringify(responseBody), {
- status: response.status,
- statusText: response.statusText,
- headers: {
- ...newHeaders,
- "content-type": "application/json",
- },
- });
- } catch (error) {
- console.error("Error proxying request:", error);
- return c.json(
- {
- error: "Failed to proxy request to backend",
- details: error instanceof Error ? error.message : "Unknown error",
- path: path,
- url: url,
- },
- 502,
- );
- }
-});
-
-app.post("/api/ai/command", async (c) => {
- const { messages, model = "gpt-4o-mini", system } = await c.req.json();
-
- const apiKey = c.env.OPENAI_API_KEY;
-
- if (!apiKey) {
- return c.json({ error: "Missing OpenAI API key." }, 401);
- }
-
- const openai = createOpenAI({ apiKey });
-
- try {
- const result = await streamText({
- maxTokens: 2048,
- messages: convertToCoreMessages(messages),
- model: openai(model),
- system: system,
- });
-
- return result.toDataStreamResponse();
- } catch (error) {
- console.error("Failed to process AI request:", error);
- return c.json({ error: "Failed to process AI request" }, 500);
- }
-});
-
-app.post("/api/ai/copilot", async (c) => {
- const { apiKey: key, model = "gpt-4o-mini", prompt, system } = await c.req.json();
-
- const apiKey = key || c.env.OPENAI_API_KEY;
-
- if (!apiKey) {
- return c.json({ error: "Missing OpenAI API key." }, 401);
- }
-
- const openai = createOpenAI({ apiKey });
-
- try {
- const result = await generateText({
- maxTokens: 50,
- model: openai(model),
- prompt: prompt,
- system,
- temperature: 0.7,
- });
-
- return c.json(result);
- } catch (error: any) {
- if (error.name === "AbortError") {
- return c.json(null, { status: 408 });
- }
-
- console.error("Failed to process AI request:", error);
- return c.json({ error: "Failed to process AI request" }, 500);
- }
-});
-
-app.post("/api/ai/update", async (c) => {
- const { caption, document } = await c.req.json();
-
- const apiKey = c.env.OPENAI_API_KEY;
-
- if (!apiKey) {
- return c.json({ error: "Missing OpenAI API key." }, 401);
- }
-
- const openai = createOpenAI({ apiKey });
-
- const result = await generateObject({
- model: openai("gpt-4o-mini"),
- schema: z.object({
- action: z.enum(["edit", "delete", "append", "ignore"]),
- blockId: z.string().optional(),
- content: z.string().optional(),
- }),
- prompt: `You are a professional technical document editor.
-
-You are given a document and a new caption that was transcribed from a recording.
-
-Your job is to analyze how to update the document to reflect the new caption.
-
-Given this document structure:
-${JSON.stringify(document)}
-
-And this new caption that was transcribed:
-${caption}
-
-Analyze how to update the document. Choose one:
-1. Edit an existing block (provide blockId and new content)
-2. Delete a block (provide blockId)
-3. Append new content (provide content)
-4. Ignore the new content if it doesn't meaningfully improve the document
-
-You can be strict in ignoring. if the transcript is not related to the content of the document, you should ignore it.
-
-Sometimes you may need to edit things below the headers, or move things around. Do what you think is best. That's all right.
-
-For eg, if I am talking about the features, add it to the block ID BELOW the features header. Don't change the heading blogs
-
-I will first talk about "what supermemory is", you need to add it to the block id that is right below the heading "what is supermemory?".
-Then, I will talk about our charter. I will say somethuing like "we want to help companies write and read better documentation". now, you need to add it to the block id that is right below the heading "our charter".
-I will talk about features like "you can actually edit the docs along with AI, seamlessly integrate it into your workflow". now, you need to add it to the block id that is right below the heading "features".
-I will also talk about connections with existing documentation platforms. now, you need to add it to the block id that is right below the heading "how we will do it".
-
-there should not be any repetitive stuff. it should be professional.
-Make sure that the document you write is a good, accurate, and up-to-date document.`,
- });
-
- return c.json(result.object);
-});
-
-app.all("/auth/notion/callback", zValidator("query", z.object({ code: z.string() })), async (c) => {
- const { code } = c.req.valid("query");
-
- const notionCredentials = btoa(`${c.env.NOTION_CLIENT_ID}:${c.env.NOTION_CLIENT_SECRET}`);
-
- const redirectUri = c.env.NODE_ENV === "development"
- ? "http://localhost:3000/auth/notion/callback"
- : "https://supermemory.ai/auth/notion/callback";
- console.log(redirectUri)
-
- const response = await fetch("https://api.notion.com/v1/oauth/token", {
- method: "POST",
- headers: {
- Authorization: `Basic ${notionCredentials}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- grant_type: "authorization_code",
- code: code,
- redirect_uri: redirectUri,
- }),
- });
-
- const data = await response.json();
-
- const fakeContext = {
- cloudflare: {
- env: c.env,
- },
- };
-
- const currentUser = await getSessionFromRequest(c.req.raw, fakeContext as AppLoadContext);
-
- console.log(currentUser?.user.id);
- const success = !(data as any).error;
-
- if (!success) {
- console.error("Failed to get Notion access token:", data, (data as any).error);
- return c.redirect(`/?error=${(data as any).error}`);
- }
-
- const accessToken = (data as any).access_token;
-
- // const key = await crypto.subtle.importKey(
- // "raw",
- // new TextEncoder().encode(c.env.WORKOS_COOKIE_PASSWORD),
- // { name: "AES-GCM" },
- // false,
- // ["encrypt", "decrypt"],
- // );
-
- // const encrypted = await crypto.subtle.encrypt(
- // { name: "AES-GCM", iv: new Uint8Array(20) },
- // key,
- // new TextEncoder().encode(accessToken),
- // );
-
- // const encryptedString = btoa(String(encrypted));
-
- // await c.env.ENCRYPTED_TOKENS.put(`${currentUser?.user.id}-notion`, encryptedString);
-
- await putEncryptedKV(
- c.env.ENCRYPTED_TOKENS,
- `${currentUser?.user.id}-notion`,
- accessToken,
- `${c.env.WORKOS_COOKIE_PASSWORD}-${currentUser?.user.id}`,
- );
-
- return c.redirect(`/?success=${success}&integration=notion`);
-});
-
-export default app;
diff --git a/apps/web/server/proxy.ts b/apps/web/server/proxy.ts
deleted file mode 100644
index 4fc152da..00000000
--- a/apps/web/server/proxy.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { AppLoadContext } from "@remix-run/cloudflare";
-
-export const proxy = async (
- path: string,
- requestOptions: RequestInit,
- originalRequest: Request,
- context: AppLoadContext,
-) => {
- try {
- // update the request headers to add the cookie
- const requestHeaders = new Headers(requestOptions.headers);
- requestHeaders.set("Cookie", originalRequest.headers.get("Cookie") || "");
-
- const backendUrl =
- context.cloudflare.env.BACKEND_URL ?? "https://supermemory-backend.dhravya.workers.dev";
-
- const response = await fetch(`${backendUrl}${path}`, {
- ...requestOptions,
- headers: requestHeaders,
- });
-
- if (!response.ok) {
- console.error("Proxy request failed", await response.text());
- return new Response(JSON.stringify({ error: "Proxy request failed" }), {
- status: response.status,
- headers: {
- "Content-Type": "application/json",
- },
- });
- }
-
- return response;
- } catch (error) {
- console.error("Proxy request failed:", error);
- return new Response(JSON.stringify({ error: "Proxy request failed" }), {
- status: 500,
- headers: {
- "Content-Type": "application/json",
- },
- });
- }
-};
diff --git a/apps/web/stores/chat.ts b/apps/web/stores/chat.ts
new file mode 100644
index 00000000..fcb5c9f0
--- /dev/null
+++ b/apps/web/stores/chat.ts
@@ -0,0 +1,182 @@
+import { create } from "zustand"
+import { persist } from "zustand/middleware"
+import type { UIMessage } from "@ai-sdk/react"
+
+export interface ConversationSummary {
+ id: string
+ title?: string
+ lastUpdated: string
+}
+
+interface ConversationRecord {
+ messages: UIMessage[]
+ title?: string
+ lastUpdated: string
+}
+
+interface ProjectConversationsState {
+ currentChatId: string | null
+ conversations: Record<string, ConversationRecord>
+}
+
+interface ConversationsStoreState {
+ byProject: Record<string, ProjectConversationsState>
+ setCurrentChatId: (projectId: string, chatId: string | null) => void
+ setConversation: (projectId: string, chatId: string, messages: UIMessage[]) => void
+ deleteConversation: (projectId: string, chatId: string) => void
+ setConversationTitle: (projectId: string, chatId: string, title: string | undefined) => void
+}
+
+export const usePersistentChatStore = create<ConversationsStoreState>()(
+ persist(
+ (set, get) => ({
+ byProject: {},
+
+ setCurrentChatId(projectId, chatId) {
+ set((state) => {
+ const project = state.byProject[projectId] ?? { currentChatId: null, conversations: {} }
+ return {
+ byProject: {
+ ...state.byProject,
+ [projectId]: { ...project, currentChatId: chatId },
+ },
+ }
+ })
+ },
+
+ setConversation(projectId, chatId, messages) {
+ const now = new Date().toISOString()
+ set((state) => {
+ const project = state.byProject[projectId] ?? { currentChatId: null, conversations: {} }
+ const existing = project.conversations[chatId]
+ const shouldTouchLastUpdated = (() => {
+ if (!existing) return messages.length > 0
+ const previousLength = existing.messages?.length ?? 0
+ return messages.length > previousLength
+ })()
+
+ const record: ConversationRecord = {
+ messages,
+ title: existing?.title,
+ lastUpdated: shouldTouchLastUpdated ? now : existing?.lastUpdated ?? now,
+ }
+ return {
+ byProject: {
+ ...state.byProject,
+ [projectId]: {
+ currentChatId: project.currentChatId,
+ conversations: {
+ ...project.conversations,
+ [chatId]: record,
+ },
+ },
+ },
+ }
+ })
+ },
+
+ deleteConversation(projectId, chatId) {
+ set((state) => {
+ const project = state.byProject[projectId] ?? { currentChatId: null, conversations: {} }
+ const { [chatId]: _, ...rest } = project.conversations
+ const nextCurrent = project.currentChatId === chatId ? null : project.currentChatId
+ return {
+ byProject: {
+ ...state.byProject,
+ [projectId]: { currentChatId: nextCurrent, conversations: rest },
+ },
+ }
+ })
+ },
+
+ setConversationTitle(projectId, chatId, title) {
+ const now = new Date().toISOString()
+ set((state) => {
+ const project = state.byProject[projectId] ?? { currentChatId: null, conversations: {} }
+ const existing = project.conversations[chatId]
+ if (!existing) return { byProject: state.byProject }
+ return {
+ byProject: {
+ ...state.byProject,
+ [projectId]: {
+ currentChatId: project.currentChatId,
+ conversations: {
+ ...project.conversations,
+ [chatId]: { ...existing, title, lastUpdated: now },
+ },
+ },
+ },
+ }
+ })
+ },
+ }),
+ {
+ name: "supermemory-chats",
+ },
+ ),
+)
+
+// Always scoped to the current project via useProject
+import { useProject } from ".";
+
+export function usePersistentChat() {
+ const { selectedProject } = useProject()
+ const projectId = selectedProject
+
+ const projectState = usePersistentChatStore((s) => s.byProject[projectId])
+ const setCurrentChatIdRaw = usePersistentChatStore((s) => s.setCurrentChatId)
+ const setConversationRaw = usePersistentChatStore((s) => s.setConversation)
+ const deleteConversationRaw = usePersistentChatStore((s) => s.deleteConversation)
+ const setConversationTitleRaw = usePersistentChatStore((s) => s.setConversationTitle)
+
+ const conversations: ConversationSummary[] = (() => {
+ const convs = projectState?.conversations ?? {}
+ return Object.entries(convs).map(([id, rec]) => ({ id, title: rec.title, lastUpdated: rec.lastUpdated }))
+ })()
+
+ const currentChatId = projectState?.currentChatId ?? null
+
+ function setCurrentChatId(chatId: string | null): void {
+ setCurrentChatIdRaw(projectId, chatId)
+ }
+
+ function setConversation(chatId: string, messages: UIMessage[]): void {
+ setConversationRaw(projectId, chatId, messages)
+ }
+
+ function deleteConversation(chatId: string): void {
+ deleteConversationRaw(projectId, chatId)
+ }
+
+ function setConversationTitle(chatId: string, title: string | undefined): void {
+ setConversationTitleRaw(projectId, chatId, title)
+ }
+
+ function getCurrentConversation(): UIMessage[] | undefined {
+ const convs = projectState?.conversations ?? {}
+ const id = currentChatId
+ if (!id) return undefined
+ return convs[id]?.messages
+ }
+
+ function getCurrentChat(): ConversationSummary | undefined {
+ const id = currentChatId
+ if (!id) return undefined
+ const rec = projectState?.conversations?.[id]
+ if (!rec) return undefined
+ return { id, title: rec.title, lastUpdated: rec.lastUpdated }
+ }
+
+ return {
+ conversations,
+ currentChatId,
+ setCurrentChatId,
+ setConversation,
+ deleteConversation,
+ setConversationTitle,
+ getCurrentConversation,
+ getCurrentChat,
+ }
+}
+
+
diff --git a/apps/web/stores/highlights.ts b/apps/web/stores/highlights.ts
new file mode 100644
index 00000000..81d75fa0
--- /dev/null
+++ b/apps/web/stores/highlights.ts
@@ -0,0 +1,32 @@
+import { create } from "zustand"
+
+interface GraphHighlightsState {
+ documentIds: string[]
+ lastUpdated: number
+ setDocumentIds: (ids: string[]) => void
+ clear: () => void
+}
+
+export const useGraphHighlightsStore = create<GraphHighlightsState>()((set, get) => ({
+ documentIds: [],
+ lastUpdated: 0,
+ setDocumentIds: (ids) => {
+ const next = Array.from(new Set(ids))
+ const prev = get().documentIds
+ if (prev.length === next.length && prev.every((id) => next.includes(id))) {
+ return
+ }
+ set({ documentIds: next, lastUpdated: Date.now() })
+ },
+ clear: () => set({ documentIds: [], lastUpdated: Date.now() }),
+}))
+
+export function useGraphHighlights() {
+ const documentIds = useGraphHighlightsStore((s) => s.documentIds)
+ const lastUpdated = useGraphHighlightsStore((s) => s.lastUpdated)
+ const setDocumentIds = useGraphHighlightsStore((s) => s.setDocumentIds)
+ const clear = useGraphHighlightsStore((s) => s.clear)
+ return { documentIds, lastUpdated, setDocumentIds, clear }
+}
+
+
diff --git a/apps/web/stores/index.ts b/apps/web/stores/index.ts
new file mode 100644
index 00000000..0cc8e65f
--- /dev/null
+++ b/apps/web/stores/index.ts
@@ -0,0 +1,78 @@
+import { create } from 'zustand'
+import { persist } from 'zustand/middleware'
+
+interface ProjectState {
+ selectedProject: string
+ setSelectedProject: (projectId: string) => void
+}
+
+export const useProjectStore = create<ProjectState>()(
+ persist(
+ (set) => ({
+ selectedProject: 'sm_project_default',
+ setSelectedProject: (projectId) => set({ selectedProject: projectId }),
+ }),
+ {
+ name: 'selectedProject',
+ }
+ )
+)
+
+interface MemoryGraphState {
+ positionX: number
+ positionY: number
+ setPositionX: (x: number) => void
+ setPositionY: (y: number) => void
+ setPosition: (x: number, y: number) => void
+}
+
+export const useMemoryGraphStore = create<MemoryGraphState>()((set) => ({
+ positionX: 0,
+ positionY: 0,
+ setPositionX: (x) => set({ positionX: x }),
+ setPositionY: (y) => set({ positionY: y }),
+ setPosition: (x, y) => set({ positionX: x, positionY: y }),
+}))
+
+interface ChatState {
+ isOpen: boolean
+ setIsOpen: (isOpen: boolean) => void
+ toggleChat: () => void
+}
+
+export const useChatStore = create<ChatState>()((set, get) => ({
+ isOpen: false,
+ setIsOpen: (isOpen) => set({ isOpen }),
+ toggleChat: () => set({ isOpen: !get().isOpen }),
+}))
+
+export function useProject() {
+ const selectedProject = useProjectStore(state => state.selectedProject)
+ const setSelectedProject = useProjectStore(state => state.setSelectedProject)
+ return { selectedProject, setSelectedProject }
+}
+
+export function useMemoryGraphPosition() {
+ const positionX = useMemoryGraphStore(state => state.positionX)
+ const positionY = useMemoryGraphStore(state => state.positionY)
+ const setPositionX = useMemoryGraphStore(state => state.setPositionX)
+ const setPositionY = useMemoryGraphStore(state => state.setPositionY)
+ const setPosition = useMemoryGraphStore(state => state.setPosition)
+
+ return {
+ x: positionX,
+ y: positionY,
+ setX: setPositionX,
+ setY: setPositionY,
+ setPosition
+ }
+}
+
+export function useChatOpen() {
+ const isOpen = useChatStore(state => state.isOpen)
+ const setIsOpen = useChatStore(state => state.setIsOpen)
+ const toggleChat = useChatStore(state => state.toggleChat)
+ return { isOpen, setIsOpen, toggleChat }
+}
+
+export { usePersistentChatStore, usePersistentChat } from "./chat"
diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts
deleted file mode 100644
index 971503f9..00000000
--- a/apps/web/tailwind.config.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import type { Config } from "tailwindcss";
-
-export default {
- darkMode: ["class"],
- content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"],
- theme: {
- extend: {
- borderRadius: {
- lg: "var(--radius)",
- md: "calc(var(--radius) - 2px)",
- sm: "calc(var(--radius) - 4px)",
- },
- fontFamily: {
- geist: ["Geist Sans", "sans-serif"],
- },
- colors: {
- rgray: {
- "1": "var(--gray-1)",
- "2": "var(--gray-2)",
- "3": "var(--gray-3)",
- "4": "var(--gray-4)",
- "5": "var(--gray-5)",
- "6": "var(--gray-6)",
- "7": "var(--gray-7)",
- "8": "var(--gray-8)",
- "9": "var(--gray-9)",
- "10": "var(--gray-10)",
- "11": "var(--gray-11)",
- "12": "var(--gray-12)",
- },
- background: "hsl(var(--background))",
- foreground: "hsl(var(--foreground))",
- card: {
- DEFAULT: "hsl(var(--card))",
- foreground: "hsl(var(--card-foreground))",
- },
- popover: {
- DEFAULT: "hsl(var(--popover))",
- foreground: "hsl(var(--popover-foreground))",
- },
- primary: {
- DEFAULT: "hsl(var(--primary))",
- foreground: "hsl(var(--primary-foreground))",
- },
- secondary: {
- DEFAULT: "hsl(var(--secondary))",
- foreground: "hsl(var(--secondary-foreground))",
- },
- muted: {
- DEFAULT: "hsl(var(--muted))",
- foreground: "hsl(var(--muted-foreground))",
- },
- accent: {
- DEFAULT: "hsl(var(--accent))",
- foreground: "hsl(var(--accent-foreground))",
- },
- destructive: {
- DEFAULT: "hsl(var(--destructive))",
- foreground: "hsl(var(--destructive-foreground))",
- },
- border: "hsl(var(--border))",
- input: "hsl(var(--input))",
- ring: "hsl(var(--ring))",
- chart: {
- "1": "hsl(var(--chart-1))",
- "2": "hsl(var(--chart-2))",
- "3": "hsl(var(--chart-3))",
- "4": "hsl(var(--chart-4))",
- "5": "hsl(var(--chart-5))",
- },
- brand: {
- DEFAULT: "hsl(var(--brand))",
- foreground: "hsl(var(--brand-foreground))",
- },
- highlight: {
- DEFAULT: "hsl(var(--highlight))",
- foreground: "hsl(var(--highlight-foreground))",
- },
- },
- screens: {
- "main-hover": {
- raw: "(hover: hover)",
- },
- },
- },
- },
- plugins: [
- require("tailwindcss-animate"),
- require("@tailwindcss/typography"),
- require("tailwind-scrollbar-hide"),
- ],
-} satisfies Config;
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index 47889f62..bdd64662 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -1,34 +1,20 @@
{
- "include": [
- "**/*.ts",
- "**/*.tsx",
- "**/.server/**/*.ts",
- "**/.server/**/*.tsx",
- "**/.client/**/*.ts",
- "**/.client/**/*.tsx",
- "**/*.json"
- ],
"compilerOptions": {
- "lib": ["DOM", "DOM.Iterable", "ES2022"],
- "types": ["@remix-run/cloudflare", "vite/client", "@cloudflare/workers-types/2023-07-01", "chrome"],
- "isolatedModules": true,
- "esModuleInterop": true,
- "jsx": "react-jsx",
- "module": "ESNext",
- "moduleResolution": "Bundler",
- "resolveJsonModule": true,
- "target": "ES2022",
- "strict": true,
- "allowJs": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
- "baseUrl": ".",
+ "incremental": true,
+ "jsx": "preserve",
"paths": {
- "~/*": ["./app/*"]
+ "@/*": ["./*"],
+ "@ui/*": ["../../packages/ui/*"],
+ "@lib/*": ["../../packages/lib/*"],
+ "@hooks/*": ["../../packages/hooks/*"]
},
-
- // Vite takes care of building everything, not tsc.
- "noEmit": true,
- "moduleDetection": "force"
- }
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ]
+ },
+ "exclude": ["node_modules"],
+ "extends": "@total-typescript/tsconfig/bundler/dom/app",
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"]
}
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
deleted file mode 100644
index bed2b173..00000000
--- a/apps/web/vite.config.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import {
- vitePlugin as remix,
- cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
-} from "@remix-run/dev";
-
-import adapter from "@hono/vite-dev-server/cloudflare";
-import serverAdapter from "hono-remix-adapter/vite";
-import path from "path";
-import { flatRoutes } from "remix-flat-routes";
-import { UserConfig, defineConfig, loadEnv } from "vite";
-import tsconfigPaths from "vite-tsconfig-paths";
-
-const _plugins = [
- remixCloudflareDevProxy({
- persist: true,
- }),
- remix({
- future: {
- v3_fetcherPersist: true,
- v3_relativeSplatPath: true,
- v3_throwAbortReason: true,
- unstable_optimizeDeps: true,
- },
- ignoredRouteFiles: ["**/*"],
- routes: async (defineRoutes) => {
- return flatRoutes("routes", defineRoutes);
- },
- }),
- serverAdapter({
- adapter,
- entry: "server/index.ts",
- }),
- tsconfigPaths(),
-];
-// _plugins.unshift(MillionLint.vite());
-
-export default defineConfig((mode) => {
- return {
- plugins: _plugins,
- resolve: {
- alias: {
- ...(mode.mode === "development" && {
- postgres: path.resolve(__dirname, "../../node_modules/postgres/src/index.js"),
- }),
- },
- extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".css"],
- },
- ssr: {
- target: "node",
- noExternal:
- mode.mode === "development"
- ? ["@udecode/plate-math", "katex", "prismjs", "react-tweet", "drizzle-orm"]
- : ["@udecode/plate-math", "katex", "prismjs"],
- },
- css: {
- modules: {
- scopeBehaviour: "local",
- generateScopedName: "[name]__[local]___[hash:base64:5]",
- localsConvention: "camelCaseOnly",
- },
- },
- build: {
- rollupOptions: {
- onwarn(warning, warn) {
- if (warning.code === "UNUSED_EXTERNAL_IMPORT") return;
- if (warning.code === "IGNORED_BARE_IMPORT") return;
- warn(warning);
- },
- onLog(level, log, handler) {
- // @ts-expect-error
- if (log.cause?.message?.includes("Can't resolve original location of error.")) return;
- handler(level, log);
- },
- },
- },
- optimizeDeps: {
- include: ["react-tweet"],
- },
- publicDir: "public",
- } satisfies UserConfig;
-});
diff --git a/apps/web/worker-configuration.d.ts b/apps/web/worker-configuration.d.ts
deleted file mode 100644
index d1327931..00000000
--- a/apps/web/worker-configuration.d.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-// Generated by Wrangler on Mon Sep 30 2024 16:07:38 GMT-0700 (Mountain Standard Time)
-// by running `wrangler types`
-
-interface Env {
- METADATA_KV: KVNamespace;
- WORKOS_CLIENT_ID: string;
- WORKOS_API_KEY: string;
- WORKOS_REDIRECT_URI: string;
- WORKOS_COOKIE_PASSWORD: string;
- PORT: string;
- DATABASE_URL: string;
- CLOUDFLARE_ACCOUNT_ID: string;
- R2_ACCESS_KEY_ID: string;
- R2_SECRET_ACCESS_KEY: string;
- IMAGES_BUCKET: R2Bucket;
- BACKEND_URL: string;
- HYPERDRIVE: Hyperdrive;
- OPENAI_API_KEY: string;
- NOTION_CLIENT_ID: string;
- NOTION_CLIENT_SECRET: string;
- ENCRYPTED_TOKENS: KVNamespace;
- NODE_ENV: string;
- STRIPE_CHECKOUT_KEY: string;
- STRIPE_WEBHOOK_SECRET: string;
- STATIC_ASSETS: Fetcher
-}
-
-// global .env
-declare global {
- namespace NodeJS {
- interface ProcessEnv extends Env {}
- }
-}
diff --git a/apps/web/wrangler.jsonc b/apps/web/wrangler.jsonc
new file mode 100644
index 00000000..bdf74f19
--- /dev/null
+++ b/apps/web/wrangler.jsonc
@@ -0,0 +1,31 @@
+{
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "main": ".open-next/worker.js",
+ "name": "supermemory-consumer",
+ "compatibility_date": "2024-12-30",
+ "compatibility_flags": [
+ // Enable Node.js API
+ // see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag
+ "nodejs_compat",
+ // Allow to fetch URLs in your app
+ // see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-fetch-strictly-public
+ "global_fetch_strictly_public"
+ ],
+ "assets": {
+ "directory": ".open-next/assets",
+ "binding": "ASSETS"
+ },
+ "services": [
+ {
+ "binding": "WORKER_SELF_REFERENCE",
+ // The service should match the "name" of your worker
+ "service": "supermemory-consumer"
+ }
+ ],
+ "r2_buckets": [
+ {
+ "binding": "NEXT_INC_CACHE_R2_BUCKET",
+ "bucket_name": "supermemory-console-cache"
+ }
+ ]
+}
diff --git a/apps/web/wrangler.toml b/apps/web/wrangler.toml
deleted file mode 100644
index 7bffa41c..00000000
--- a/apps/web/wrangler.toml
+++ /dev/null
@@ -1,27 +0,0 @@
-#:schema node_modules/wrangler/config-schema.json
-name = "supermemory-web"
-compatibility_date = "2025-03-12"
-pages_build_output_dir = "./build/client"
-compatibility_flags = ["nodejs_compat"]
-
-[[kv_namespaces]]
-binding = "METADATA_KV"
-id = "4c9f38f1c4264f7b8b56a48555129571"
-
-[[kv_namespaces]]
-binding = "ENCRYPTED_TOKENS"
-id = "a1f048ee14644468ad63b817b5648a31"
-preview_id = "a1f048ee14644468ad63b817b5648a31"
-
-[[r2_buckets]]
-binding = "IMAGES_BUCKET"
-bucket_name = "supermemory-images"
-
-
-[[hyperdrive]]
-binding = "HYPERDRIVE"
-id = "3a377d1b9c084e698ee201f10dfa8131"
-localConnectionString = "postgres://postgres:postgres@localhost:5432/supermemorylocal?sslmode=require"
-
-[placement]
-mode = "smart"