@pylonsync/create-pylon 0.19.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-pylon.js +30 -7
- package/package.json +1 -1
- package/templates/agency/.env.example +7 -0
- package/templates/agency/README.md +28 -3
- package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
- package/templates/agency/app/(marketing)/page.tsx +3 -3
- package/templates/agency/app/(marketing)/work/page.tsx +3 -3
- package/templates/agency/app/auth-form.tsx +4 -4
- package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
- package/templates/agency/app/dashboard/page.tsx +14 -16
- package/templates/agency/app/icon.svg +3 -0
- package/templates/agency/app/login/page.tsx +12 -14
- package/templates/agency/components/marketing.tsx +12 -5
- package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
- package/templates/agency/lib/agency.ts +22 -0
- package/templates/agency/lib/demo.ts +118 -0
- package/templates/agency/lib/site.config.ts +19 -13
- package/templates/agency/public/favicon.ico +0 -0
- package/templates/agency/tests/agency.test.ts +57 -0
- package/templates/ai-chat/.env.example +13 -27
- package/templates/ai-chat/README.md +53 -60
- package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
- package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
- package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
- package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
- package/templates/ai-chat/app/(chat)/page.tsx +7 -11
- package/templates/ai-chat/app/auth-form.tsx +45 -38
- package/templates/ai-chat/app/error.tsx +9 -12
- package/templates/ai-chat/app/globals.css +464 -102
- package/templates/ai-chat/app/layout.tsx +11 -19
- package/templates/ai-chat/app/login/page.tsx +39 -24
- package/templates/ai-chat/app.ts +118 -43
- package/templates/ai-chat/components/chat/composer.tsx +193 -0
- package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
- package/templates/ai-chat/components/chat/icons.tsx +198 -0
- package/templates/ai-chat/components/chat/markdown.tsx +164 -0
- package/templates/ai-chat/components/chat/message.tsx +183 -0
- package/templates/ai-chat/components/chat/setup.tsx +71 -0
- package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
- package/templates/ai-chat/functions/_beginTurn.ts +145 -0
- package/templates/ai-chat/functions/_finishTurn.ts +55 -0
- package/templates/ai-chat/functions/assistantStatus.ts +13 -0
- package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
- package/templates/ai-chat/functions/createConversation.ts +28 -0
- package/templates/ai-chat/functions/deleteConversation.ts +15 -20
- package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
- package/templates/ai-chat/functions/renameConversation.ts +19 -0
- package/templates/ai-chat/functions/respond.ts +88 -0
- package/templates/ai-chat/functions/seedExamples.ts +46 -0
- package/templates/ai-chat/functions/stopResponse.ts +30 -0
- package/templates/ai-chat/lib/brand-icon.ts +7 -0
- package/templates/ai-chat/lib/chat.ts +277 -0
- package/templates/ai-chat/lib/claim.ts +15 -0
- package/templates/ai-chat/lib/examples.ts +179 -0
- package/templates/ai-chat/lib/highlight.ts +107 -0
- package/templates/ai-chat/lib/markdown.ts +317 -0
- package/templates/ai-chat/lib/provider.ts +38 -0
- package/templates/ai-chat/lib/site.config.ts +55 -39
- package/templates/ai-chat/tests/chat.test.ts +174 -0
- package/templates/ai-chat/tests/components.test.tsx +57 -0
- package/templates/ai-chat/tests/markdown.test.ts +88 -0
- package/templates/ai-chat/tests/provider.test.ts +32 -0
- package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
- package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
- package/templates/backend/consumer/apps/api/app.ts +166 -0
- package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
- package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
- package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
- package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
- package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
- package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
- package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
- package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
- package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
- package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
- package/templates/backend/consumer/apps/api/package.json +8 -2
- package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
- package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
- package/templates/chat/README.md +43 -21
- package/templates/chat/app/chat-client.tsx +495 -85
- package/templates/chat/app/error.tsx +1 -1
- package/templates/chat/app/globals.css +170 -54
- package/templates/chat/app/layout.tsx +12 -9
- package/templates/chat/app/not-found.tsx +4 -2
- package/templates/chat/app/page.tsx +9 -19
- package/templates/chat/app.ts +96 -17
- package/templates/chat/components/chat/avatar.tsx +45 -0
- package/templates/chat/components/chat/composer.tsx +110 -0
- package/templates/chat/components/chat/message-list.tsx +285 -0
- package/templates/chat/components/chat/sidebar.tsx +190 -0
- package/templates/chat/functions/joinChat.ts +83 -0
- package/templates/chat/functions/renameMember.ts +25 -0
- package/templates/chat/lib/chat.ts +188 -0
- package/templates/chat/lib/seed.ts +78 -0
- package/templates/chat/tests/chat.test.ts +159 -0
- package/templates/chat/tests/composer.test.tsx +54 -0
- package/templates/consumer/README.md +3 -3
- package/templates/consumer/app/globals.css +10 -3
- package/templates/consumer/app/icon.svg +1 -0
- package/templates/consumer/components/social/avatar.tsx +25 -11
- package/templates/consumer/functions/seedFeed.ts +3 -3
- package/templates/consumer/lib/seed.ts +43 -85
- package/templates/consumer/public/favicon.ico +0 -0
- package/templates/consumer/public/images/posts/arch.jpg +0 -0
- package/templates/consumer/public/images/posts/coffee.jpg +0 -0
- package/templates/consumer/public/images/posts/dog.jpg +0 -0
- package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
- package/templates/consumer/public/images/posts/ramen.jpg +0 -0
- package/templates/consumer/public/images/posts/run.jpg +0 -0
- package/templates/consumer/public/images/posts/sunset.jpg +0 -0
- package/templates/consumer/tests/seed.test.ts +39 -0
- package/templates/creator/.env.example +7 -0
- package/templates/creator/README.md +24 -5
- package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
- package/templates/creator/app/auth-form.tsx +7 -7
- package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
- package/templates/creator/app/dashboard/page.tsx +18 -18
- package/templates/creator/app/icon.svg +2 -0
- package/templates/creator/app/login/page.tsx +14 -14
- package/templates/creator/app.ts +4 -4
- package/templates/creator/functions/seedDemo.ts +32 -0
- package/templates/creator/functions/subscribe.ts +4 -15
- package/templates/creator/lib/demo.ts +94 -0
- package/templates/creator/lib/site.config.ts +15 -10
- package/templates/creator/lib/stats.ts +35 -0
- package/templates/creator/public/favicon.ico +0 -0
- package/templates/creator/tests/counter.test.ts +92 -0
- package/templates/creator/tests/live-count.test.tsx +45 -0
- package/templates/crm/README.md +22 -1
- package/templates/crm/app/apple-icon.png +0 -0
- package/templates/crm/app/companies/companies-view.tsx +32 -14
- package/templates/crm/app/companies/page.tsx +2 -1
- package/templates/crm/app/contacts/contacts-view.tsx +34 -10
- package/templates/crm/app/contacts/page.tsx +2 -1
- package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
- package/templates/crm/app/deals/[id]/page.tsx +2 -1
- package/templates/crm/app/globals.css +29 -14
- package/templates/crm/app/icon.svg +1 -0
- package/templates/crm/app/layout.tsx +7 -5
- package/templates/crm/app/page.tsx +2 -1
- package/templates/crm/app/pipeline-view.tsx +5 -5
- package/templates/crm/app/workspace.tsx +29 -17
- package/templates/crm/app.ts +5 -5
- package/templates/crm/components/activity-timeline.tsx +14 -4
- package/templates/crm/components/app-shell.tsx +94 -0
- package/templates/crm/components/auth-form.tsx +35 -5
- package/templates/crm/components/avatar.tsx +5 -1
- package/templates/crm/components/board-skeleton.tsx +5 -5
- package/templates/crm/components/brand-mark.tsx +25 -0
- package/templates/crm/components/command-palette.tsx +17 -17
- package/templates/crm/components/data-table.tsx +97 -36
- package/templates/crm/components/deal-card.tsx +23 -27
- package/templates/crm/components/filter-tabs.tsx +101 -0
- package/templates/crm/components/metrics-bar.tsx +6 -6
- package/templates/crm/components/nav.tsx +15 -0
- package/templates/crm/components/page-header.tsx +43 -7
- package/templates/crm/components/pipeline-board.tsx +16 -10
- package/templates/crm/components/require-auth.tsx +24 -26
- package/templates/crm/components/sidebar.tsx +54 -49
- package/templates/crm/components/stage-badge.tsx +4 -2
- package/templates/crm/components/ui/button.tsx +5 -4
- package/templates/crm/components/ui/dialog.tsx +1 -1
- package/templates/crm/components/ui/input.tsx +1 -1
- package/templates/crm/components/ui/select.tsx +1 -1
- package/templates/crm/components/ui/textarea.tsx +1 -1
- package/templates/crm/functions/seedWorkspace.ts +32 -17
- package/templates/crm/lib/brand.ts +9 -0
- package/templates/crm/lib/demo-team.ts +51 -0
- package/templates/crm/lib/pipeline.ts +37 -0
- package/templates/crm/lib/search.ts +4 -3
- package/templates/crm/lib/seed.ts +53 -8
- package/templates/crm/public/favicon.ico +0 -0
- package/templates/crm/tests/components.test.tsx +5 -5
- package/templates/crm/tests/demo-team.test.ts +18 -0
- package/templates/crm/tests/pipeline.test.ts +59 -3
- package/templates/directory/.env.example +7 -0
- package/templates/directory/README.md +22 -3
- package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
- package/templates/directory/app/(marketing)/page.tsx +6 -4
- package/templates/directory/app/auth-form.tsx +4 -4
- package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
- package/templates/directory/app/dashboard/page.tsx +16 -14
- package/templates/directory/app/icon.svg +4 -0
- package/templates/directory/app/login/page.tsx +16 -15
- package/templates/directory/functions/seedDemo.ts +43 -0
- package/templates/directory/lib/demo.ts +145 -0
- package/templates/directory/lib/directory.ts +27 -0
- package/templates/directory/lib/site.config.ts +21 -16
- package/templates/directory/public/favicon.ico +0 -0
- package/templates/directory/tests/directory.test.ts +46 -0
- package/templates/expo/consumer/apps/expo/App.tsx +360 -288
- package/templates/expo/consumer/apps/expo/app.json +9 -1
- package/templates/expo/consumer/apps/expo/package.json +2 -1
- package/templates/helpdesk/README.md +21 -0
- package/templates/helpdesk/app/apple-icon.png +0 -0
- package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
- package/templates/helpdesk/app/customers/page.tsx +2 -1
- package/templates/helpdesk/app/globals.css +29 -14
- package/templates/helpdesk/app/icon.svg +1 -0
- package/templates/helpdesk/app/inbox-view.tsx +66 -47
- package/templates/helpdesk/app/layout.tsx +7 -5
- package/templates/helpdesk/app/page.tsx +4 -7
- package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
- package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
- package/templates/helpdesk/app/workspace.tsx +33 -22
- package/templates/helpdesk/app.ts +2 -2
- package/templates/helpdesk/components/app-shell.tsx +94 -0
- package/templates/helpdesk/components/auth-form.tsx +35 -5
- package/templates/helpdesk/components/avatar.tsx +5 -1
- package/templates/helpdesk/components/brand-mark.tsx +28 -0
- package/templates/helpdesk/components/command-palette.tsx +17 -17
- package/templates/helpdesk/components/data-table.tsx +97 -36
- package/templates/helpdesk/components/filter-tabs.tsx +101 -0
- package/templates/helpdesk/components/nav.tsx +14 -0
- package/templates/helpdesk/components/page-header.tsx +43 -7
- package/templates/helpdesk/components/priority-badge.tsx +49 -6
- package/templates/helpdesk/components/require-auth.tsx +24 -26
- package/templates/helpdesk/components/sidebar.tsx +54 -48
- package/templates/helpdesk/components/sla-indicator.tsx +5 -8
- package/templates/helpdesk/components/thread.tsx +7 -7
- package/templates/helpdesk/components/ticket-list.tsx +79 -19
- package/templates/helpdesk/components/ui/button.tsx +5 -4
- package/templates/helpdesk/components/ui/dialog.tsx +1 -1
- package/templates/helpdesk/components/ui/input.tsx +1 -1
- package/templates/helpdesk/components/ui/select.tsx +1 -1
- package/templates/helpdesk/components/ui/textarea.tsx +1 -1
- package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
- package/templates/helpdesk/lib/brand.ts +9 -0
- package/templates/helpdesk/lib/demo-team.ts +51 -0
- package/templates/helpdesk/lib/search.ts +4 -3
- package/templates/helpdesk/lib/seed.ts +43 -2
- package/templates/helpdesk/lib/tickets.ts +68 -20
- package/templates/helpdesk/public/favicon.ico +0 -0
- package/templates/helpdesk/tests/components.test.tsx +10 -0
- package/templates/helpdesk/tests/demo-team.test.ts +18 -0
- package/templates/helpdesk/tests/tickets.test.ts +70 -0
- package/templates/imessage/.env.example +49 -0
- package/templates/imessage/AGENTS.md +153 -0
- package/templates/imessage/CLAUDE.md +7 -0
- package/templates/imessage/README.md +187 -0
- package/templates/imessage/app/console.tsx +382 -0
- package/templates/imessage/app/contacts/page.tsx +13 -0
- package/templates/imessage/app/error.tsx +16 -0
- package/templates/imessage/app/gate.tsx +31 -0
- package/templates/imessage/app/globals.css +183 -0
- package/templates/imessage/app/layout.tsx +17 -0
- package/templates/imessage/app/login/login-form.tsx +115 -0
- package/templates/imessage/app/login/page.tsx +30 -0
- package/templates/imessage/app/not-found.tsx +13 -0
- package/templates/imessage/app/page.tsx +14 -0
- package/templates/imessage/app/robots.ts +6 -0
- package/templates/imessage/app/setup/page.tsx +13 -0
- package/templates/imessage/app/sign-out.tsx +20 -0
- package/templates/imessage/app.ts +299 -0
- package/templates/imessage/bunfig +4 -0
- package/templates/imessage/components/avatar.tsx +46 -0
- package/templates/imessage/components/contact-panel.tsx +263 -0
- package/templates/imessage/components/contacts-view.tsx +172 -0
- package/templates/imessage/components/conversation-list.tsx +129 -0
- package/templates/imessage/components/nav-rail.tsx +67 -0
- package/templates/imessage/components/setup-view.tsx +408 -0
- package/templates/imessage/components/thread.tsx +229 -0
- package/templates/imessage/components/ui/badge.tsx +37 -0
- package/templates/imessage/components/ui/button.tsx +56 -0
- package/templates/imessage/components/ui/card.tsx +90 -0
- package/templates/imessage/components/ui/dialog.tsx +110 -0
- package/templates/imessage/components/ui/input.tsx +30 -0
- package/templates/imessage/components/ui/label.tsx +21 -0
- package/templates/imessage/components/ui/select.tsx +39 -0
- package/templates/imessage/components/ui/table.tsx +124 -0
- package/templates/imessage/components/ui/textarea.tsx +24 -0
- package/templates/imessage/components.json +20 -0
- package/templates/imessage/functions/answerNow.ts +34 -0
- package/templates/imessage/functions/assistant.ts +96 -0
- package/templates/imessage/functions/beginTurn.ts +101 -0
- package/templates/imessage/functions/cancelReminder.ts +13 -0
- package/templates/imessage/functions/claimOutbound.ts +44 -0
- package/templates/imessage/functions/clearDemo.ts +18 -0
- package/templates/imessage/functions/deleteNote.ts +11 -0
- package/templates/imessage/functions/deliverOutbound.ts +55 -0
- package/templates/imessage/functions/finishTurn.ts +84 -0
- package/templates/imessage/functions/fireReminder.ts +42 -0
- package/templates/imessage/functions/ingestInbound.ts +22 -0
- package/templates/imessage/functions/ownerAccount.ts +13 -0
- package/templates/imessage/functions/processTurn.ts +74 -0
- package/templates/imessage/functions/relayApply.ts +102 -0
- package/templates/imessage/functions/relaySync.ts +63 -0
- package/templates/imessage/functions/releaseOutbound.ts +18 -0
- package/templates/imessage/functions/resolveTurn.ts +25 -0
- package/templates/imessage/functions/saveContact.ts +27 -0
- package/templates/imessage/functions/seedDemo.ts +70 -0
- package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
- package/templates/imessage/functions/sendblueWebhook.ts +49 -0
- package/templates/imessage/functions/setContactStatus.ts +17 -0
- package/templates/imessage/functions/settleOutbound.ts +35 -0
- package/templates/imessage/functions/setupStatus.ts +33 -0
- package/templates/imessage/functions/sweep.ts +58 -0
- package/templates/imessage/functions/toolCancelReminder.ts +17 -0
- package/templates/imessage/functions/toolForgetNote.ts +15 -0
- package/templates/imessage/functions/toolListNotes.ts +16 -0
- package/templates/imessage/functions/toolListReminders.ts +17 -0
- package/templates/imessage/functions/toolSaveNote.ts +22 -0
- package/templates/imessage/functions/toolSetReminder.ts +55 -0
- package/templates/imessage/functions/updateSettings.ts +48 -0
- package/templates/imessage/gitignore +11 -0
- package/templates/imessage/lib/agent-call.ts +251 -0
- package/templates/imessage/lib/assistant.ts +74 -0
- package/templates/imessage/lib/chunk.ts +135 -0
- package/templates/imessage/lib/demo.ts +68 -0
- package/templates/imessage/lib/format.ts +170 -0
- package/templates/imessage/lib/gate.ts +104 -0
- package/templates/imessage/lib/handles.ts +67 -0
- package/templates/imessage/lib/provider.ts +38 -0
- package/templates/imessage/lib/relay-protocol.ts +95 -0
- package/templates/imessage/lib/reminders.ts +41 -0
- package/templates/imessage/lib/secrets.ts +59 -0
- package/templates/imessage/lib/sendblue.ts +173 -0
- package/templates/imessage/lib/store.ts +407 -0
- package/templates/imessage/lib/transport.ts +135 -0
- package/templates/imessage/lib/utils.ts +10 -0
- package/templates/imessage/package.json +40 -0
- package/templates/imessage/relay/.env.example +14 -0
- package/templates/imessage/relay/applescript.ts +70 -0
- package/templates/imessage/relay/chatdb.ts +165 -0
- package/templates/imessage/relay/client.ts +41 -0
- package/templates/imessage/relay/main.ts +137 -0
- package/templates/imessage/relay/state.ts +35 -0
- package/templates/imessage/tests/agent-call.test.ts +112 -0
- package/templates/imessage/tests/applescript.test.ts +88 -0
- package/templates/imessage/tests/chatdb.test.ts +139 -0
- package/templates/imessage/tests/chunk.test.ts +61 -0
- package/templates/imessage/tests/fake-ctx.ts +88 -0
- package/templates/imessage/tests/ingest.test.ts +141 -0
- package/templates/imessage/tests/lib.test.ts +99 -0
- package/templates/imessage/tests/review-fixes.test.ts +138 -0
- package/templates/imessage/tests/secrets.test.ts +36 -0
- package/templates/imessage/tests/sendblue.test.ts +143 -0
- package/templates/imessage/tests/setup.ts +5 -0
- package/templates/imessage/tsconfig.json +18 -0
- package/templates/inventory/README.md +21 -0
- package/templates/inventory/app/apple-icon.png +0 -0
- package/templates/inventory/app/globals.css +29 -14
- package/templates/inventory/app/icon.svg +1 -0
- package/templates/inventory/app/layout.tsx +7 -5
- package/templates/inventory/app/movements/movements-view.tsx +32 -10
- package/templates/inventory/app/movements/page.tsx +2 -1
- package/templates/inventory/app/page.tsx +4 -6
- package/templates/inventory/app/products/[id]/page.tsx +2 -1
- package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
- package/templates/inventory/app/products-view.tsx +74 -33
- package/templates/inventory/app/workspace.tsx +34 -16
- package/templates/inventory/app.ts +10 -2
- package/templates/inventory/components/app-shell.tsx +94 -0
- package/templates/inventory/components/auth-form.tsx +35 -5
- package/templates/inventory/components/avatar.tsx +5 -1
- package/templates/inventory/components/brand-mark.tsx +25 -0
- package/templates/inventory/components/category-tile.tsx +49 -0
- package/templates/inventory/components/command-palette.tsx +17 -17
- package/templates/inventory/components/data-table.tsx +97 -36
- package/templates/inventory/components/filter-tabs.tsx +101 -0
- package/templates/inventory/components/nav.tsx +14 -0
- package/templates/inventory/components/page-header.tsx +43 -7
- package/templates/inventory/components/require-auth.tsx +24 -26
- package/templates/inventory/components/sidebar.tsx +54 -48
- package/templates/inventory/components/stock-level.tsx +30 -14
- package/templates/inventory/components/summary-bar.tsx +8 -15
- package/templates/inventory/components/ui/button.tsx +5 -4
- package/templates/inventory/components/ui/dialog.tsx +1 -1
- package/templates/inventory/components/ui/input.tsx +1 -1
- package/templates/inventory/components/ui/select.tsx +1 -1
- package/templates/inventory/components/ui/textarea.tsx +1 -1
- package/templates/inventory/functions/seedWorkspace.ts +18 -8
- package/templates/inventory/lib/brand.ts +9 -0
- package/templates/inventory/lib/demo-team.ts +51 -0
- package/templates/inventory/lib/search.ts +4 -3
- package/templates/inventory/lib/seed.ts +29 -2
- package/templates/inventory/lib/stock.ts +32 -0
- package/templates/inventory/public/favicon.ico +0 -0
- package/templates/inventory/tests/demo-team.test.ts +18 -0
- package/templates/inventory/tests/stock.test.ts +29 -0
- package/templates/invoices/README.md +16 -0
- package/templates/invoices/app/apple-icon.png +0 -0
- package/templates/invoices/app/clients/clients-view.tsx +18 -6
- package/templates/invoices/app/clients/page.tsx +2 -1
- package/templates/invoices/app/globals.css +29 -14
- package/templates/invoices/app/icon.svg +1 -0
- package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
- package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
- package/templates/invoices/app/invoices-view.tsx +81 -32
- package/templates/invoices/app/layout.tsx +7 -5
- package/templates/invoices/app/page.tsx +4 -6
- package/templates/invoices/app/workspace.tsx +26 -16
- package/templates/invoices/app.ts +2 -2
- package/templates/invoices/components/app-shell.tsx +94 -0
- package/templates/invoices/components/auth-form.tsx +35 -5
- package/templates/invoices/components/avatar.tsx +5 -1
- package/templates/invoices/components/brand-mark.tsx +26 -0
- package/templates/invoices/components/command-palette.tsx +17 -17
- package/templates/invoices/components/data-table.tsx +97 -36
- package/templates/invoices/components/filter-tabs.tsx +101 -0
- package/templates/invoices/components/line-items.tsx +19 -14
- package/templates/invoices/components/nav.tsx +14 -0
- package/templates/invoices/components/page-header.tsx +43 -7
- package/templates/invoices/components/require-auth.tsx +24 -26
- package/templates/invoices/components/sidebar.tsx +54 -48
- package/templates/invoices/components/status-badge.tsx +7 -7
- package/templates/invoices/components/summary-bar.tsx +5 -5
- package/templates/invoices/components/ui/button.tsx +5 -4
- package/templates/invoices/components/ui/dialog.tsx +1 -1
- package/templates/invoices/components/ui/input.tsx +1 -1
- package/templates/invoices/components/ui/select.tsx +1 -1
- package/templates/invoices/components/ui/textarea.tsx +1 -1
- package/templates/invoices/functions/recordPayment.ts +3 -0
- package/templates/invoices/lib/billing.ts +40 -0
- package/templates/invoices/lib/brand.ts +9 -0
- package/templates/invoices/lib/search.ts +4 -3
- package/templates/invoices/public/favicon.ico +0 -0
- package/templates/invoices/tests/billing.test.ts +27 -0
- package/templates/ios/consumer/apps/ios/Package.swift +4 -3
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
- package/templates/ios/consumer/apps/ios/project.yml +13 -9
- package/templates/local-service/.env.example +7 -0
- package/templates/local-service/README.md +26 -2
- package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
- package/templates/local-service/app/(marketing)/page.tsx +66 -42
- package/templates/local-service/app/auth-form.tsx +8 -8
- package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
- package/templates/local-service/app/dashboard/page.tsx +14 -14
- package/templates/local-service/app/icon.svg +2 -0
- package/templates/local-service/app/login/page.tsx +15 -15
- package/templates/local-service/functions/seedDemo.ts +49 -0
- package/templates/local-service/lib/demo.ts +135 -0
- package/templates/local-service/lib/hours.ts +44 -0
- package/templates/local-service/lib/site.config.ts +8 -3
- package/templates/local-service/lib/slots.ts +3 -0
- package/templates/local-service/public/favicon.ico +0 -0
- package/templates/local-service/tests/booking.test.ts +104 -0
- package/templates/marketplace/README.md +23 -15
- package/templates/marketplace/app/globals.css +172 -114
- package/templates/marketplace/app/layout.tsx +75 -30
- package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
- package/templates/marketplace/app/me/page.tsx +4 -4
- package/templates/marketplace/app/page.tsx +221 -298
- package/templates/marketplace/app/sell/page.tsx +8 -11
- package/templates/marketplace/app.ts +32 -7
- package/templates/marketplace/client/ActivityFeed.tsx +311 -0
- package/templates/marketplace/client/AuthNav.tsx +24 -16
- package/templates/marketplace/client/ListingCard.tsx +118 -0
- package/templates/marketplace/client/LiveGrid.tsx +92 -0
- package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
- package/templates/marketplace/client/LoginCard.tsx +114 -136
- package/templates/marketplace/client/MarketProvider.tsx +3 -5
- package/templates/marketplace/client/MyMarket.tsx +371 -185
- package/templates/marketplace/client/OfferPanel.tsx +421 -287
- package/templates/marketplace/client/SellForm.tsx +271 -192
- package/templates/marketplace/client/ThemeToggle.tsx +1 -0
- package/templates/marketplace/client/WatchButton.tsx +16 -16
- package/templates/marketplace/client/market.ts +61 -14
- package/templates/marketplace/functions/seedMarket.ts +143 -36
- package/templates/marketplace/functions/seedOffers.ts +101 -0
- package/templates/marketplace/lib/catalog.ts +72 -1
- package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
- package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
- package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
- package/templates/marketplace/public/images/listings/duffel.webp +0 -0
- package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
- package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
- package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
- package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
- package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
- package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
- package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
- package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
- package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
- package/templates/marketplace/public/images/listings/turntable.webp +0 -0
- package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
- package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
- package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
- package/templates/marketplace/public/images/listings/watch.webp +0 -0
- package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
- package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
- package/templates/marketplace/tests/example.test.ts +64 -2
- package/templates/marketplace/tests/listing-card.test.tsx +61 -0
- package/templates/projects/README.md +24 -3
- package/templates/projects/app/apple-icon.png +0 -0
- package/templates/projects/app/clients/clients-view.tsx +20 -6
- package/templates/projects/app/clients/page.tsx +2 -1
- package/templates/projects/app/globals.css +29 -14
- package/templates/projects/app/icon.svg +1 -0
- package/templates/projects/app/layout.tsx +7 -5
- package/templates/projects/app/page.tsx +3 -5
- package/templates/projects/app/projects/[id]/page.tsx +2 -1
- package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
- package/templates/projects/app/projects-view.tsx +105 -52
- package/templates/projects/app/workspace.tsx +41 -20
- package/templates/projects/app.ts +2 -2
- package/templates/projects/components/app-shell.tsx +94 -0
- package/templates/projects/components/auth-form.tsx +35 -5
- package/templates/projects/components/avatar.tsx +5 -1
- package/templates/projects/components/board-skeleton.tsx +3 -3
- package/templates/projects/components/brand-mark.tsx +25 -0
- package/templates/projects/components/command-palette.tsx +17 -17
- package/templates/projects/components/data-table.tsx +97 -36
- package/templates/projects/components/filter-tabs.tsx +101 -0
- package/templates/projects/components/meter.tsx +48 -0
- package/templates/projects/components/nav.tsx +14 -0
- package/templates/projects/components/page-header.tsx +43 -7
- package/templates/projects/components/require-auth.tsx +24 -26
- package/templates/projects/components/sidebar.tsx +54 -48
- package/templates/projects/components/summary-bar.tsx +49 -0
- package/templates/projects/components/task-board.tsx +10 -9
- package/templates/projects/components/time-dialog.tsx +29 -5
- package/templates/projects/components/ui/button.tsx +5 -4
- package/templates/projects/components/ui/dialog.tsx +1 -1
- package/templates/projects/components/ui/input.tsx +1 -1
- package/templates/projects/components/ui/select.tsx +1 -1
- package/templates/projects/components/ui/textarea.tsx +1 -1
- package/templates/projects/functions/seedWorkspace.ts +26 -12
- package/templates/projects/lib/brand.ts +9 -0
- package/templates/projects/lib/demo-team.ts +51 -0
- package/templates/projects/lib/search.ts +4 -3
- package/templates/projects/lib/seed.ts +35 -1
- package/templates/projects/lib/work.ts +63 -0
- package/templates/projects/public/favicon.ico +0 -0
- package/templates/projects/tests/demo-team.test.ts +18 -0
- package/templates/projects/tests/work.test.ts +65 -0
- package/templates/restaurant/.env.example +7 -0
- package/templates/restaurant/README.md +29 -3
- package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
- package/templates/restaurant/app/(marketing)/page.tsx +47 -23
- package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
- package/templates/restaurant/app/auth-form.tsx +8 -8
- package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
- package/templates/restaurant/app/dashboard/page.tsx +14 -14
- package/templates/restaurant/app/icon.svg +4 -0
- package/templates/restaurant/app/login/page.tsx +15 -15
- package/templates/restaurant/functions/seedDemo.ts +44 -0
- package/templates/restaurant/lib/demo.ts +144 -0
- package/templates/restaurant/lib/hours.ts +45 -0
- package/templates/restaurant/lib/site.config.ts +3 -3
- package/templates/restaurant/public/favicon.ico +0 -0
- package/templates/restaurant/tests/reservations.test.ts +67 -0
- package/templates/saas/README.md +54 -35
- package/templates/saas/app/(auth)/auth-form.tsx +25 -23
- package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
- package/templates/saas/app/(auth)/layout.tsx +27 -33
- package/templates/saas/app/(auth)/login/page.tsx +1 -1
- package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
- package/templates/saas/app/(auth)/signup/page.tsx +1 -1
- package/templates/saas/app/(marketing)/layout.tsx +127 -253
- package/templates/saas/app/(marketing)/page.tsx +133 -372
- package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
- package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
- package/templates/saas/app/dashboard/board-client.tsx +416 -0
- package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
- package/templates/saas/app/dashboard/layout.tsx +27 -13
- package/templates/saas/app/dashboard/overview-client.tsx +332 -0
- package/templates/saas/app/dashboard/page.tsx +29 -35
- package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
- package/templates/saas/app/dashboard/projects/page.tsx +11 -7
- package/templates/saas/app/dashboard/projects-client.tsx +398 -0
- package/templates/saas/app/globals.css +5 -4
- package/templates/saas/app/icon.svg +1 -0
- package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
- package/templates/saas/app/onboarding/page.tsx +6 -2
- package/templates/saas/app.ts +68 -13
- package/templates/saas/components/dashboard-shell.tsx +212 -132
- package/templates/saas/components/marketing.tsx +137 -274
- package/templates/saas/components/pricing-table.tsx +12 -11
- package/templates/saas/components/setup-checklist.tsx +31 -29
- package/templates/saas/components/task-ui.tsx +199 -0
- package/templates/saas/functions/createProject.ts +7 -13
- package/templates/saas/functions/createTask.ts +67 -0
- package/templates/saas/functions/deleteProject.ts +17 -0
- package/templates/saas/functions/deleteTask.ts +14 -0
- package/templates/saas/functions/seedWorkspace.ts +46 -0
- package/templates/saas/functions/setProjectStatus.ts +41 -0
- package/templates/saas/functions/updateProfile.ts +13 -0
- package/templates/saas/functions/updateProject.ts +4 -5
- package/templates/saas/functions/updateTask.ts +88 -0
- package/templates/saas/lib/members.ts +53 -0
- package/templates/saas/lib/names.ts +53 -0
- package/templates/saas/lib/plans.ts +11 -0
- package/templates/saas/lib/sample-workspace.ts +117 -0
- package/templates/saas/lib/site.config.ts +200 -463
- package/templates/saas/lib/tasks.ts +101 -0
- package/templates/saas/lib/use-live.ts +25 -0
- package/templates/saas/public/favicon.ico +0 -0
- package/templates/saas/public/screenshots/board.png +0 -0
- package/templates/saas/public/screenshots/members.png +0 -0
- package/templates/saas/public/screenshots/overview.png +0 -0
- package/templates/saas/public/screenshots/projects.png +0 -0
- package/templates/saas/scripts/capture-screenshots.mjs +156 -0
- package/templates/saas/tests/names.test.ts +35 -0
- package/templates/saas/tests/plans.test.ts +12 -0
- package/templates/saas/tests/sample-workspace.test.ts +46 -0
- package/templates/saas/tests/tasks.test.ts +81 -0
- package/templates/shop/README.md +25 -13
- package/templates/shop/app/(marketing)/layout.tsx +94 -67
- package/templates/shop/app/(marketing)/not-found.tsx +13 -12
- package/templates/shop/app/(marketing)/page.tsx +120 -59
- package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
- package/templates/shop/app/(marketing)/success/page.tsx +75 -22
- package/templates/shop/app/globals.css +94 -1
- package/templates/shop/app/layout.tsx +4 -4
- package/templates/shop/app/login/page.tsx +4 -4
- package/templates/shop/app.ts +18 -6
- package/templates/shop/components/cart-button.tsx +33 -0
- package/templates/shop/components/marketing.tsx +17 -87
- package/templates/shop/components/shelf-count.tsx +36 -0
- package/templates/shop/functions/checkout.ts +17 -1
- package/templates/shop/lib/cart-store.ts +57 -0
- package/templates/shop/lib/shelf-store.ts +35 -0
- package/templates/shop/lib/shop.ts +27 -0
- package/templates/shop/lib/site.config.ts +108 -98
- package/templates/shop/public/images/hero.jpg +0 -0
- package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
- package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
- package/templates/shop/public/images/products/linen.jpg +0 -0
- package/templates/shop/public/images/products/sea-salt.jpg +0 -0
- package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
- package/templates/shop/tests/shop.test.ts +64 -0
- package/templates/waitlist/.env.example +7 -0
- package/templates/waitlist/README.md +29 -13
- package/templates/waitlist/app/(marketing)/page.tsx +9 -7
- package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
- package/templates/waitlist/app/auth-form.tsx +8 -8
- package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
- package/templates/waitlist/app/dashboard/page.tsx +17 -16
- package/templates/waitlist/app/icon.svg +2 -0
- package/templates/waitlist/app/login/page.tsx +14 -14
- package/templates/waitlist/app.ts +6 -6
- package/templates/waitlist/functions/joinWaitlist.ts +6 -18
- package/templates/waitlist/functions/seedDemo.ts +32 -0
- package/templates/waitlist/lib/demo.ts +94 -0
- package/templates/waitlist/lib/launch.ts +19 -0
- package/templates/waitlist/lib/site.config.ts +14 -9
- package/templates/waitlist/lib/stats.ts +35 -0
- package/templates/waitlist/public/favicon.ico +0 -0
- package/templates/waitlist/tests/counter.test.ts +107 -0
- package/templates/waitlist/tests/live-count.test.tsx +48 -0
- package/templates/ai-chat/tests/example.test.ts +0 -12
- package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
- package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
- package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
- package/templates/backend/consumer/apps/api/schema.ts +0 -143
- package/templates/chat/functions/_keep.ts +0 -13
- package/templates/chat/tests/example.test.ts +0 -12
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
- package/templates/marketplace/client/LiveTicker.tsx +0 -116
- package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
- package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useState } from "react";
|
|
4
|
+
import { siteConfig } from "@/lib/site.config";
|
|
5
|
+
import { groupConversations, type ConversationRow } from "@/lib/chat";
|
|
6
|
+
import { BrandMark, CloseIcon, ComposeIcon, PencilIcon, SearchIcon, SignOutIcon, TrashIcon } from "./icons";
|
|
7
|
+
|
|
8
|
+
export type Account = { kind: "guest" } | { kind: "user"; email: string | null };
|
|
9
|
+
|
|
10
|
+
export interface SidebarProps {
|
|
11
|
+
conversations: ConversationRow[];
|
|
12
|
+
currentId: string | null;
|
|
13
|
+
now: number;
|
|
14
|
+
account: Account;
|
|
15
|
+
onSelect: (id: string) => void;
|
|
16
|
+
onNew: () => void;
|
|
17
|
+
onRename: (id: string, title: string) => void;
|
|
18
|
+
onDelete: (id: string) => void;
|
|
19
|
+
onSignIn: () => void;
|
|
20
|
+
onSignOut: () => void;
|
|
21
|
+
// Mobile drawer: shows a close button when set.
|
|
22
|
+
onClose?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Sidebar({
|
|
26
|
+
conversations,
|
|
27
|
+
currentId,
|
|
28
|
+
now,
|
|
29
|
+
account,
|
|
30
|
+
onSelect,
|
|
31
|
+
onNew,
|
|
32
|
+
onRename,
|
|
33
|
+
onDelete,
|
|
34
|
+
onSignIn,
|
|
35
|
+
onSignOut,
|
|
36
|
+
onClose,
|
|
37
|
+
}: SidebarProps) {
|
|
38
|
+
const [filter, setFilter] = useState("");
|
|
39
|
+
const groups = useMemo(() => {
|
|
40
|
+
const f = filter.trim().toLowerCase();
|
|
41
|
+
const rows = f ? conversations.filter((c) => c.title.toLowerCase().includes(f)) : conversations;
|
|
42
|
+
return groupConversations(rows, now);
|
|
43
|
+
}, [conversations, filter, now]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex h-full flex-col">
|
|
47
|
+
<div className="flex h-14 items-center justify-between px-3">
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onClick={onNew}
|
|
51
|
+
className="press flex items-center gap-2.5 rounded-xl px-1.5 py-1 transition-colors duration-200 hover:bg-hover"
|
|
52
|
+
>
|
|
53
|
+
<BrandMark size={26} />
|
|
54
|
+
<span className="text-[15.5px] font-semibold tracking-[-0.015em] text-ink">{siteConfig.brand.name}</span>
|
|
55
|
+
</button>
|
|
56
|
+
{onClose ? (
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={onClose}
|
|
60
|
+
aria-label="Close sidebar"
|
|
61
|
+
className="press flex size-9 items-center justify-center rounded-xl text-ink-2 hover:bg-hover"
|
|
62
|
+
>
|
|
63
|
+
<CloseIcon size={18} />
|
|
64
|
+
</button>
|
|
65
|
+
) : null}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div className="space-y-1 px-3 pb-2">
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
onClick={onNew}
|
|
72
|
+
className="press flex h-10 w-full items-center gap-2.5 rounded-xl px-3 text-[14px] font-medium text-ink transition-colors duration-200 hover:bg-hover"
|
|
73
|
+
>
|
|
74
|
+
<ComposeIcon size={17} />
|
|
75
|
+
New chat
|
|
76
|
+
</button>
|
|
77
|
+
<label className="flex h-10 items-center gap-2.5 rounded-xl px-3 text-ink-3 transition-colors duration-200 focus-within:bg-hover hover:bg-hover">
|
|
78
|
+
<SearchIcon size={17} />
|
|
79
|
+
<input
|
|
80
|
+
value={filter}
|
|
81
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
82
|
+
placeholder="Search chats"
|
|
83
|
+
aria-label="Search chats"
|
|
84
|
+
className="w-full bg-transparent text-[14px] text-ink outline-none placeholder:text-ink-3"
|
|
85
|
+
/>
|
|
86
|
+
</label>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<nav className="sidebar-scroll min-h-0 flex-1 overflow-y-auto px-3 pb-4" aria-label="Conversations">
|
|
90
|
+
{groups.length === 0 ? (
|
|
91
|
+
<p className="px-3 py-6 text-[13px] leading-relaxed text-ink-3">
|
|
92
|
+
{filter ? "No chats match that search." : "Your chats appear here."}
|
|
93
|
+
</p>
|
|
94
|
+
) : (
|
|
95
|
+
groups.map((g) => (
|
|
96
|
+
<section key={g.label} className="mt-4 first:mt-2">
|
|
97
|
+
<h3 className="px-3 pb-1 text-[12px] font-medium text-ink-3">{g.label}</h3>
|
|
98
|
+
<ul className="space-y-px">
|
|
99
|
+
{g.items.map((c) => (
|
|
100
|
+
<ConversationItem
|
|
101
|
+
key={c.id}
|
|
102
|
+
convo={c}
|
|
103
|
+
active={c.id === currentId}
|
|
104
|
+
onSelect={() => onSelect(c.id)}
|
|
105
|
+
onRename={(t) => onRename(c.id, t)}
|
|
106
|
+
onDelete={() => onDelete(c.id)}
|
|
107
|
+
/>
|
|
108
|
+
))}
|
|
109
|
+
</ul>
|
|
110
|
+
</section>
|
|
111
|
+
))
|
|
112
|
+
)}
|
|
113
|
+
</nav>
|
|
114
|
+
|
|
115
|
+
<AccountPanel account={account} onSignIn={onSignIn} onSignOut={onSignOut} />
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function ConversationItem({
|
|
121
|
+
convo,
|
|
122
|
+
active,
|
|
123
|
+
onSelect,
|
|
124
|
+
onRename,
|
|
125
|
+
onDelete,
|
|
126
|
+
}: {
|
|
127
|
+
convo: ConversationRow;
|
|
128
|
+
active: boolean;
|
|
129
|
+
onSelect: () => void;
|
|
130
|
+
onRename: (title: string) => void;
|
|
131
|
+
onDelete: () => void;
|
|
132
|
+
}) {
|
|
133
|
+
const [editing, setEditing] = useState(false);
|
|
134
|
+
const [draft, setDraft] = useState(convo.title);
|
|
135
|
+
const [confirming, setConfirming] = useState(false);
|
|
136
|
+
|
|
137
|
+
function commit() {
|
|
138
|
+
setEditing(false);
|
|
139
|
+
const t = draft.trim();
|
|
140
|
+
if (t && t !== convo.title) onRename(t);
|
|
141
|
+
else setDraft(convo.title);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (editing) {
|
|
145
|
+
return (
|
|
146
|
+
<li>
|
|
147
|
+
<input
|
|
148
|
+
autoFocus
|
|
149
|
+
value={draft}
|
|
150
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
151
|
+
onBlur={commit}
|
|
152
|
+
onKeyDown={(e) => {
|
|
153
|
+
if (e.key === "Enter") commit();
|
|
154
|
+
if (e.key === "Escape") {
|
|
155
|
+
setDraft(convo.title);
|
|
156
|
+
setEditing(false);
|
|
157
|
+
}
|
|
158
|
+
}}
|
|
159
|
+
aria-label="Chat title"
|
|
160
|
+
className="h-9 w-full rounded-xl bg-surface px-3 text-[14px] text-ink outline-none ring-1 ring-brand/40"
|
|
161
|
+
/>
|
|
162
|
+
</li>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<li className="group/item relative">
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
onClick={onSelect}
|
|
171
|
+
aria-current={active ? "page" : undefined}
|
|
172
|
+
title={convo.title}
|
|
173
|
+
className={
|
|
174
|
+
"flex h-9 w-full items-center gap-2 rounded-xl pl-3 pr-2 text-left text-[14px] transition-colors duration-200 " +
|
|
175
|
+
(active ? "bg-active text-ink" : "text-ink-2 hover:bg-hover hover:text-ink")
|
|
176
|
+
}
|
|
177
|
+
>
|
|
178
|
+
<span className="min-w-0 flex-1 truncate">{convo.title || "New chat"}</span>
|
|
179
|
+
{convo.example ? (
|
|
180
|
+
<span className="shrink-0 rounded-md bg-hover px-1.5 py-px text-[11px] text-ink-3 group-hover/item:hidden">
|
|
181
|
+
Example
|
|
182
|
+
</span>
|
|
183
|
+
) : null}
|
|
184
|
+
</button>
|
|
185
|
+
<div
|
|
186
|
+
className={
|
|
187
|
+
"absolute right-1 top-1/2 -translate-y-1/2 items-center gap-px rounded-lg pl-3 " +
|
|
188
|
+
(confirming ? "flex" : "hidden group-focus-within/item:flex group-hover/item:flex") +
|
|
189
|
+
(active ? " fade-active" : " fade-hover")
|
|
190
|
+
}
|
|
191
|
+
>
|
|
192
|
+
{confirming ? (
|
|
193
|
+
<>
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
onClick={() => {
|
|
197
|
+
setConfirming(false);
|
|
198
|
+
onDelete();
|
|
199
|
+
}}
|
|
200
|
+
className="h-7 rounded-lg px-2 text-[12.5px] font-medium text-danger hover:bg-danger/10"
|
|
201
|
+
>
|
|
202
|
+
Delete
|
|
203
|
+
</button>
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
onClick={() => setConfirming(false)}
|
|
207
|
+
className="h-7 rounded-lg px-2 text-[12.5px] text-ink-2 hover:bg-hover"
|
|
208
|
+
>
|
|
209
|
+
Cancel
|
|
210
|
+
</button>
|
|
211
|
+
</>
|
|
212
|
+
) : (
|
|
213
|
+
<>
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
onClick={() => {
|
|
217
|
+
setDraft(convo.title);
|
|
218
|
+
setEditing(true);
|
|
219
|
+
}}
|
|
220
|
+
aria-label="Rename chat"
|
|
221
|
+
className="flex size-7 items-center justify-center rounded-lg text-ink-3 hover:bg-hover hover:text-ink"
|
|
222
|
+
>
|
|
223
|
+
<PencilIcon size={15} />
|
|
224
|
+
</button>
|
|
225
|
+
<button
|
|
226
|
+
type="button"
|
|
227
|
+
onClick={() => setConfirming(true)}
|
|
228
|
+
aria-label="Delete chat"
|
|
229
|
+
className="flex size-7 items-center justify-center rounded-lg text-ink-3 hover:bg-hover hover:text-danger"
|
|
230
|
+
>
|
|
231
|
+
<TrashIcon size={15} />
|
|
232
|
+
</button>
|
|
233
|
+
</>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
</li>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function AccountPanel({
|
|
241
|
+
account,
|
|
242
|
+
onSignIn,
|
|
243
|
+
onSignOut,
|
|
244
|
+
}: {
|
|
245
|
+
account: Account;
|
|
246
|
+
onSignIn: () => void;
|
|
247
|
+
onSignOut: () => void;
|
|
248
|
+
}) {
|
|
249
|
+
if (account.kind === "guest") {
|
|
250
|
+
return (
|
|
251
|
+
<div className="border-t border-line p-3">
|
|
252
|
+
<div className="rounded-2xl bg-surface/70 p-3.5 ring-1 ring-line">
|
|
253
|
+
<p className="text-[13.5px] font-medium text-ink">You are using a guest session</p>
|
|
254
|
+
<p className="mt-1 text-[12.5px] leading-relaxed text-ink-3">
|
|
255
|
+
Chats are saved in this browser. Sign in to keep them on every device.
|
|
256
|
+
</p>
|
|
257
|
+
<button
|
|
258
|
+
type="button"
|
|
259
|
+
onClick={onSignIn}
|
|
260
|
+
className="press mt-3 flex h-9 w-full items-center justify-center rounded-xl bg-ink text-[13.5px] font-medium text-bg transition-opacity duration-200 hover:opacity-90"
|
|
261
|
+
>
|
|
262
|
+
Sign in or create account
|
|
263
|
+
</button>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const email = account.email ?? "Signed in";
|
|
269
|
+
return (
|
|
270
|
+
<div className="border-t border-line p-3">
|
|
271
|
+
<div className="flex items-center gap-3 rounded-xl px-2 py-1.5">
|
|
272
|
+
<span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-brand-soft text-[13px] font-semibold uppercase text-brand">
|
|
273
|
+
{email.slice(0, 1)}
|
|
274
|
+
</span>
|
|
275
|
+
<span className="min-w-0 flex-1 truncate text-[13.5px] text-ink">{email}</span>
|
|
276
|
+
<button
|
|
277
|
+
type="button"
|
|
278
|
+
onClick={onSignOut}
|
|
279
|
+
aria-label="Sign out"
|
|
280
|
+
title="Sign out"
|
|
281
|
+
className="press flex size-8 items-center justify-center rounded-lg text-ink-3 hover:bg-hover hover:text-ink"
|
|
282
|
+
>
|
|
283
|
+
<SignOutIcon size={17} />
|
|
284
|
+
</button>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { siteConfig } from "../lib/site.config";
|
|
3
|
+
import {
|
|
4
|
+
buildHistory,
|
|
5
|
+
isStaleStream,
|
|
6
|
+
STALE_STREAM_MS,
|
|
7
|
+
RATE_WINDOW_MS,
|
|
8
|
+
replyLimit,
|
|
9
|
+
sortMessages,
|
|
10
|
+
type LlmTurn,
|
|
11
|
+
type MessageRow,
|
|
12
|
+
} from "../lib/chat";
|
|
13
|
+
|
|
14
|
+
// Internal: the write half of starting a reply, called by `respond`. In one
|
|
15
|
+
// transaction it checks ownership, the hourly limits, and that the caller has
|
|
16
|
+
// no other generation running; stores the user's message (or, on
|
|
17
|
+
// regenerate, removes the last reply); and inserts the assistant message the
|
|
18
|
+
// stream fills. Returns the transcript to send to the model.
|
|
19
|
+
export default mutation({
|
|
20
|
+
internal: true,
|
|
21
|
+
auth: "guest",
|
|
22
|
+
args: {
|
|
23
|
+
conversationId: v.string(),
|
|
24
|
+
content: v.optional(v.string()),
|
|
25
|
+
regenerate: v.optional(v.boolean()),
|
|
26
|
+
model: v.string(),
|
|
27
|
+
streamId: v.optional(v.string()),
|
|
28
|
+
},
|
|
29
|
+
async handler(ctx, args): Promise<{ messageId: string; history: LlmTurn[] }> {
|
|
30
|
+
const userId = ctx.auth.userId;
|
|
31
|
+
if (!userId) throw ctx.error("UNAUTHENTICATED", "A session is required.");
|
|
32
|
+
|
|
33
|
+
const convo = (await ctx.db.unsafe.get("Conversation", args.conversationId)) as
|
|
34
|
+
| { userId: string; example?: boolean }
|
|
35
|
+
| null;
|
|
36
|
+
if (!convo || convo.userId !== userId) {
|
|
37
|
+
throw ctx.error("NOT_FOUND", "Conversation not found.");
|
|
38
|
+
}
|
|
39
|
+
if (convo.example) {
|
|
40
|
+
throw ctx.error("EXAMPLE_READ_ONLY", "Example conversations are read-only. Start a new chat.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Serializes turn starts, so two tabs (or two guest sessions) cannot both
|
|
44
|
+
// pass the limit and busy checks below at the same moment.
|
|
45
|
+
await ctx.db.advisoryLock(ctx.auth.isGuest ? "ai-chat:turn:guests" : `ai-chat:turn:${userId}`);
|
|
46
|
+
|
|
47
|
+
// Limits and the busy check read ReplyLog, which regenerate and delete
|
|
48
|
+
// never remove.
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const since = new Date(now - RATE_WINDOW_MS).toISOString();
|
|
51
|
+
const mine = (await ctx.db.unsafe.query("ReplyLog", {
|
|
52
|
+
userId,
|
|
53
|
+
createdAt: { $gt: since },
|
|
54
|
+
})) as { createdAt: string; finishedAt?: string | null }[];
|
|
55
|
+
if (mine.length >= replyLimit(ctx.auth.isGuest)) {
|
|
56
|
+
throw ctx.error(
|
|
57
|
+
"RATE_LIMITED",
|
|
58
|
+
ctx.auth.isGuest
|
|
59
|
+
? "Hourly limit reached. Sign in for a higher limit, or try again later."
|
|
60
|
+
: "Hourly limit reached. Try again later.",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (ctx.auth.isGuest) {
|
|
64
|
+
const guests = (await ctx.db.unsafe.query("ReplyLog", {
|
|
65
|
+
isGuest: true,
|
|
66
|
+
createdAt: { $gt: since },
|
|
67
|
+
$limit: siteConfig.chat.repliesPerHour.allGuests,
|
|
68
|
+
})) as unknown[];
|
|
69
|
+
if (guests.length >= siteConfig.chat.repliesPerHour.allGuests) {
|
|
70
|
+
throw ctx.error("RATE_LIMITED", "Guest replies are busy right now. Sign in, or try again later.");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// One generation per person at a time. A stopped reply still counts until
|
|
74
|
+
// its provider call ends, because the call keeps running on the server.
|
|
75
|
+
if (mine.some((r) => !r.finishedAt && now - Date.parse(r.createdAt) < STALE_STREAM_MS)) {
|
|
76
|
+
throw ctx.error("BUSY", "A reply is already being written. Wait for it to finish.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rows = sortMessages(
|
|
80
|
+
((await ctx.db.unsafe.query("Message", { conversationId: args.conversationId })) as unknown as MessageRow[]).filter(
|
|
81
|
+
(m) => m.userId === userId,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
if (rows.some((m) => m.status === "streaming" && !isStaleStream(m, now))) {
|
|
85
|
+
throw ctx.error("BUSY", "A reply is already being written in this conversation.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Message writes are denied to clients by policy. Every write below is
|
|
89
|
+
// scoped to the conversation the caller was just checked to own.
|
|
90
|
+
let transcript = rows;
|
|
91
|
+
if (args.regenerate) {
|
|
92
|
+
const last = rows[rows.length - 1];
|
|
93
|
+
if (last?.role === "assistant") {
|
|
94
|
+
await ctx.db.unsafe.delete("Message", last.id);
|
|
95
|
+
transcript = rows.slice(0, -1);
|
|
96
|
+
}
|
|
97
|
+
if (transcript[transcript.length - 1]?.role !== "user") {
|
|
98
|
+
throw ctx.error("INVALID_ARGS", "There is no message to answer.");
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
const text = (args.content ?? "").trim();
|
|
102
|
+
if (text.length === 0) throw ctx.error("INVALID_ARGS", "The message is empty.");
|
|
103
|
+
if (text.length > siteConfig.chat.maxInputChars) {
|
|
104
|
+
throw ctx.error("INVALID_ARGS", "The message is too long.");
|
|
105
|
+
}
|
|
106
|
+
const createdAt = new Date(now).toISOString();
|
|
107
|
+
const id = await ctx.db.unsafe.insert("Message", {
|
|
108
|
+
conversationId: args.conversationId,
|
|
109
|
+
userId,
|
|
110
|
+
role: "user",
|
|
111
|
+
content: text,
|
|
112
|
+
status: "done",
|
|
113
|
+
createdAt,
|
|
114
|
+
});
|
|
115
|
+
transcript = [
|
|
116
|
+
...rows,
|
|
117
|
+
{ id, conversationId: args.conversationId, userId, role: "user", content: text, status: "done", createdAt },
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// One millisecond after the user's message, so the pair sorts in order.
|
|
122
|
+
const messageId = await ctx.db.unsafe.insert("Message", {
|
|
123
|
+
conversationId: args.conversationId,
|
|
124
|
+
userId,
|
|
125
|
+
role: "assistant",
|
|
126
|
+
content: "",
|
|
127
|
+
status: "streaming",
|
|
128
|
+
streamId: args.streamId ?? null,
|
|
129
|
+
model: args.model,
|
|
130
|
+
createdAt: new Date(now + 1).toISOString(),
|
|
131
|
+
});
|
|
132
|
+
await ctx.db.unsafe.insert("ReplyLog", {
|
|
133
|
+
userId,
|
|
134
|
+
messageId,
|
|
135
|
+
isGuest: ctx.auth.isGuest,
|
|
136
|
+
model: args.model,
|
|
137
|
+
createdAt: new Date(now).toISOString(),
|
|
138
|
+
});
|
|
139
|
+
await ctx.db.unsafe.update("Conversation", args.conversationId, {
|
|
140
|
+
updatedAt: new Date(now).toISOString(),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return { messageId, history: buildHistory(transcript) };
|
|
144
|
+
},
|
|
145
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
// Internal: store the final text of a reply, called by `respond` when the
|
|
4
|
+
// provider call ends or fails. Ownership is checked against the ReplyLog row
|
|
5
|
+
// `_beginTurn` wrote for the same caller, so the reply still lands after a
|
|
6
|
+
// guest signs in and its messages move to the account.
|
|
7
|
+
//
|
|
8
|
+
// A reply the user stopped keeps the text they saw, but only when that text
|
|
9
|
+
// is a prefix of what the model produced. Otherwise the stored text is the
|
|
10
|
+
// model's own output cut to the same length, so a client cannot write words
|
|
11
|
+
// into the assistant's turn.
|
|
12
|
+
export default mutation({
|
|
13
|
+
internal: true,
|
|
14
|
+
auth: "guest",
|
|
15
|
+
args: {
|
|
16
|
+
messageId: v.string(),
|
|
17
|
+
content: v.string(),
|
|
18
|
+
status: v.string(),
|
|
19
|
+
errorCode: v.optional(v.string()),
|
|
20
|
+
},
|
|
21
|
+
async handler(ctx, args) {
|
|
22
|
+
// ReplyLog and Message are server-only for writes; the log row proves
|
|
23
|
+
// this caller started the generation.
|
|
24
|
+
const [log] = (await ctx.db.unsafe.query("ReplyLog", { messageId: args.messageId, $limit: 1 })) as {
|
|
25
|
+
id: string;
|
|
26
|
+
userId: string;
|
|
27
|
+
}[];
|
|
28
|
+
if (!log || log.userId !== ctx.auth.userId) return { ok: false };
|
|
29
|
+
await ctx.db.unsafe.update("ReplyLog", log.id, { finishedAt: new Date().toISOString() });
|
|
30
|
+
|
|
31
|
+
const msg = (await ctx.db.unsafe.get("Message", args.messageId)) as
|
|
32
|
+
| { status: string; content: string; conversationId: string }
|
|
33
|
+
| null;
|
|
34
|
+
if (!msg) return { ok: true };
|
|
35
|
+
|
|
36
|
+
if (msg.status === "stopped") {
|
|
37
|
+
const shown = msg.content;
|
|
38
|
+
const content = args.content.startsWith(shown) ? shown : args.content.slice(0, shown.length);
|
|
39
|
+
if (content !== shown) await ctx.db.unsafe.update("Message", args.messageId, { content });
|
|
40
|
+
return { ok: true };
|
|
41
|
+
}
|
|
42
|
+
if (msg.status !== "streaming") return { ok: true };
|
|
43
|
+
|
|
44
|
+
const status = args.status === "error" ? "error" : "done";
|
|
45
|
+
await ctx.db.unsafe.update("Message", args.messageId, {
|
|
46
|
+
content: args.content,
|
|
47
|
+
status,
|
|
48
|
+
errorCode: status === "error" ? (args.errorCode ?? "LLM_FAILED") : null,
|
|
49
|
+
});
|
|
50
|
+
await ctx.db.unsafe.update("Conversation", msg.conversationId, {
|
|
51
|
+
updatedAt: new Date().toISOString(),
|
|
52
|
+
});
|
|
53
|
+
return { ok: true };
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { query } from "@pylonsync/functions";
|
|
2
|
+
import { providerStatus } from "../lib/provider";
|
|
3
|
+
|
|
4
|
+
// Whether a model provider key is configured on the server. The UI uses it to
|
|
5
|
+
// show setup steps before the first message. It returns two fields and never
|
|
6
|
+
// any key material.
|
|
7
|
+
export default query({
|
|
8
|
+
auth: "guest",
|
|
9
|
+
args: {},
|
|
10
|
+
async handler(ctx) {
|
|
11
|
+
return providerStatus(ctx.env);
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { hashClaimCode } from "../lib/claim";
|
|
3
|
+
import { isGuestId } from "../lib/chat";
|
|
4
|
+
|
|
5
|
+
// Move a guest's conversations to the signed-in account. The code comes from
|
|
6
|
+
// `prepareGuestClaim`, which only the guest session could call, so holding it
|
|
7
|
+
// proves the caller was that guest. Each code works once.
|
|
8
|
+
export default mutation({
|
|
9
|
+
auth: "user",
|
|
10
|
+
args: { code: v.string() },
|
|
11
|
+
async handler(ctx, args) {
|
|
12
|
+
const userId = ctx.auth.userId;
|
|
13
|
+
if (!/^[0-9a-f]{64}$/.test(args.code)) return { moved: 0 };
|
|
14
|
+
|
|
15
|
+
// GuestClaim is server-only; the row is looked up by the code's hash.
|
|
16
|
+
const [claim] = (await ctx.db.unsafe.query("GuestClaim", {
|
|
17
|
+
codeHash: await hashClaimCode(args.code),
|
|
18
|
+
$limit: 1,
|
|
19
|
+
})) as { id: string; guestId: string; expiresAt: string }[];
|
|
20
|
+
if (!claim) return { moved: 0 };
|
|
21
|
+
await ctx.db.unsafe.delete("GuestClaim", claim.id);
|
|
22
|
+
if (Date.parse(claim.expiresAt) < Date.now() || !isGuestId(claim.guestId)) return { moved: 0 };
|
|
23
|
+
|
|
24
|
+
// Reassigns rows owned by the guest id the claim proved. Owner fields are
|
|
25
|
+
// readonly to clients; this server-side update is the one path that
|
|
26
|
+
// changes them.
|
|
27
|
+
const conversations = (await ctx.db.unsafe.query("Conversation", {
|
|
28
|
+
userId: claim.guestId,
|
|
29
|
+
})) as { id: string }[];
|
|
30
|
+
for (const c of conversations) {
|
|
31
|
+
await ctx.db.unsafe.update("Conversation", c.id, { userId });
|
|
32
|
+
}
|
|
33
|
+
const messages = (await ctx.db.unsafe.query("Message", { userId: claim.guestId })) as { id: string }[];
|
|
34
|
+
for (const m of messages) {
|
|
35
|
+
await ctx.db.unsafe.update("Message", m.id, { userId });
|
|
36
|
+
}
|
|
37
|
+
return { moved: conversations.length };
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { siteConfig } from "../lib/site.config";
|
|
3
|
+
import { titleFromMessage } from "../lib/chat";
|
|
4
|
+
|
|
5
|
+
// Start a conversation titled from its first message. Guests may call it, so a
|
|
6
|
+
// visitor can chat before signing in. `respond` then adds the messages.
|
|
7
|
+
export default mutation({
|
|
8
|
+
auth: "guest",
|
|
9
|
+
args: { firstMessage: v.string() },
|
|
10
|
+
async handler(ctx, args) {
|
|
11
|
+
const text = args.firstMessage.trim();
|
|
12
|
+
if (text.length === 0) throw ctx.error("INVALID_ARGS", "The message is empty.");
|
|
13
|
+
if (text.length > siteConfig.chat.maxInputChars) {
|
|
14
|
+
throw ctx.error("INVALID_ARGS", "The message is too long.");
|
|
15
|
+
}
|
|
16
|
+
const now = new Date().toISOString();
|
|
17
|
+
// Conversation inserts are denied to clients by policy; this function is
|
|
18
|
+
// the one place that creates them, stamped with the caller's id.
|
|
19
|
+
const id = await ctx.db.unsafe.insert("Conversation", {
|
|
20
|
+
userId: ctx.auth.userId,
|
|
21
|
+
title: titleFromMessage(text),
|
|
22
|
+
example: false,
|
|
23
|
+
createdAt: now,
|
|
24
|
+
updatedAt: now,
|
|
25
|
+
});
|
|
26
|
+
return { id };
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -1,31 +1,26 @@
|
|
|
1
1
|
import { mutation, v } from "@pylonsync/functions";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
args: { conversationId: v.id("Conversation") },
|
|
3
|
+
// Delete a conversation and its messages in one transaction. Only the owner
|
|
4
|
+
// can delete; a missing or foreign id returns `deleted: 0` without saying
|
|
5
|
+
// which.
|
|
6
|
+
export default mutation({
|
|
7
|
+
auth: "guest",
|
|
8
|
+
args: { conversationId: v.string() },
|
|
10
9
|
async handler(ctx, args) {
|
|
11
|
-
const convo = (await ctx.db.get("Conversation", args.conversationId)) as
|
|
10
|
+
const convo = (await ctx.db.unsafe.get("Conversation", args.conversationId)) as
|
|
12
11
|
| { userId: string }
|
|
13
12
|
| null;
|
|
14
|
-
if (!convo) return { ok: true, deleted: 0 };
|
|
15
|
-
if (convo.userId !== ctx.auth.userId) {
|
|
16
|
-
throw ctx.error("POLICY_DENIED", "You can only delete your own conversations.");
|
|
17
|
-
}
|
|
13
|
+
if (!convo || convo.userId !== ctx.auth.userId) return { ok: true, deleted: 0 };
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
conversationId:
|
|
22
|
-
}[];
|
|
15
|
+
// Reads by conversation id, then checks each row's owner as well.
|
|
16
|
+
const messages = (await ctx.db.unsafe.query("Message", {
|
|
17
|
+
conversationId: args.conversationId,
|
|
18
|
+
})) as { id: string; userId: string }[];
|
|
23
19
|
let deleted = 0;
|
|
24
20
|
for (const m of messages) {
|
|
25
|
-
if (m.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
21
|
+
if (m.userId !== ctx.auth.userId) continue;
|
|
22
|
+
await ctx.db.unsafe.delete("Message", m.id);
|
|
23
|
+
deleted++;
|
|
29
24
|
}
|
|
30
25
|
await ctx.db.unsafe.delete("Conversation", args.conversationId);
|
|
31
26
|
return { ok: true, deleted };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mutation } from "@pylonsync/functions";
|
|
2
|
+
import { hashClaimCode, newClaimCode, CLAIM_TTL_MS } from "../lib/claim";
|
|
3
|
+
|
|
4
|
+
// Called by a guest right before signing in. Returns a one-time code that
|
|
5
|
+
// `claimGuestHistory` accepts after sign-in to move this guest's
|
|
6
|
+
// conversations to the account. Only the SHA-256 hash of the code is stored,
|
|
7
|
+
// and the code expires after 15 minutes.
|
|
8
|
+
export default mutation({
|
|
9
|
+
auth: "guest",
|
|
10
|
+
args: {},
|
|
11
|
+
async handler(ctx) {
|
|
12
|
+
const guestId = ctx.auth.userId;
|
|
13
|
+
if (!ctx.auth.isGuest || !guestId) return { code: null };
|
|
14
|
+
|
|
15
|
+
const code = newClaimCode();
|
|
16
|
+
// GuestClaim is denied to clients in every direction by policy.
|
|
17
|
+
const stale = (await ctx.db.unsafe.query("GuestClaim", { guestId })) as { id: string }[];
|
|
18
|
+
for (const row of stale) await ctx.db.unsafe.delete("GuestClaim", row.id);
|
|
19
|
+
// Claims from guests who never signed in expire; clear a batch of them.
|
|
20
|
+
const expired = (await ctx.db.unsafe.query("GuestClaim", {
|
|
21
|
+
expiresAt: { $lt: new Date().toISOString() },
|
|
22
|
+
$limit: 50,
|
|
23
|
+
})) as { id: string }[];
|
|
24
|
+
for (const row of expired) await ctx.db.unsafe.delete("GuestClaim", row.id);
|
|
25
|
+
await ctx.db.unsafe.insert("GuestClaim", {
|
|
26
|
+
guestId,
|
|
27
|
+
codeHash: await hashClaimCode(code),
|
|
28
|
+
expiresAt: new Date(Date.now() + CLAIM_TTL_MS).toISOString(),
|
|
29
|
+
});
|
|
30
|
+
return { code };
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { cleanTitle } from "../lib/chat";
|
|
3
|
+
|
|
4
|
+
// Rename one of the caller's conversations. Titles are trimmed to 80
|
|
5
|
+
// characters.
|
|
6
|
+
export default mutation({
|
|
7
|
+
auth: "guest",
|
|
8
|
+
args: { conversationId: v.string(), title: v.string() },
|
|
9
|
+
async handler(ctx, args) {
|
|
10
|
+
const title = cleanTitle(args.title.slice(0, 400));
|
|
11
|
+
if (!title) throw ctx.error("INVALID_ARGS", "The title is empty.");
|
|
12
|
+
const convo = (await ctx.db.unsafe.get("Conversation", args.conversationId)) as { userId: string } | null;
|
|
13
|
+
if (!convo || convo.userId !== ctx.auth.userId) throw ctx.error("NOT_FOUND", "Conversation not found.");
|
|
14
|
+
// Conversation updates are denied to clients by policy; ownership is
|
|
15
|
+
// checked above.
|
|
16
|
+
await ctx.db.unsafe.update("Conversation", args.conversationId, { title });
|
|
17
|
+
return { ok: true, title };
|
|
18
|
+
},
|
|
19
|
+
});
|