@pylonsync/create-pylon 0.19.0 → 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.
Files changed (675) hide show
  1. package/bin/create-pylon.js +30 -7
  2. package/package.json +1 -1
  3. package/templates/agency/.env.example +7 -0
  4. package/templates/agency/README.md +28 -3
  5. package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
  6. package/templates/agency/app/(marketing)/page.tsx +3 -3
  7. package/templates/agency/app/(marketing)/work/page.tsx +3 -3
  8. package/templates/agency/app/auth-form.tsx +4 -4
  9. package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
  10. package/templates/agency/app/dashboard/page.tsx +14 -16
  11. package/templates/agency/app/icon.svg +3 -0
  12. package/templates/agency/app/login/page.tsx +12 -14
  13. package/templates/agency/components/marketing.tsx +12 -5
  14. package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
  15. package/templates/agency/lib/agency.ts +22 -0
  16. package/templates/agency/lib/demo.ts +118 -0
  17. package/templates/agency/lib/site.config.ts +19 -13
  18. package/templates/agency/public/favicon.ico +0 -0
  19. package/templates/agency/tests/agency.test.ts +57 -0
  20. package/templates/ai-chat/.env.example +13 -27
  21. package/templates/ai-chat/README.md +53 -60
  22. package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
  23. package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
  24. package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
  25. package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
  26. package/templates/ai-chat/app/(chat)/page.tsx +7 -11
  27. package/templates/ai-chat/app/auth-form.tsx +45 -38
  28. package/templates/ai-chat/app/error.tsx +9 -12
  29. package/templates/ai-chat/app/globals.css +464 -102
  30. package/templates/ai-chat/app/layout.tsx +11 -19
  31. package/templates/ai-chat/app/login/page.tsx +39 -24
  32. package/templates/ai-chat/app.ts +118 -43
  33. package/templates/ai-chat/components/chat/composer.tsx +193 -0
  34. package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
  35. package/templates/ai-chat/components/chat/icons.tsx +198 -0
  36. package/templates/ai-chat/components/chat/markdown.tsx +164 -0
  37. package/templates/ai-chat/components/chat/message.tsx +183 -0
  38. package/templates/ai-chat/components/chat/setup.tsx +71 -0
  39. package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
  40. package/templates/ai-chat/functions/_beginTurn.ts +145 -0
  41. package/templates/ai-chat/functions/_finishTurn.ts +55 -0
  42. package/templates/ai-chat/functions/assistantStatus.ts +13 -0
  43. package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
  44. package/templates/ai-chat/functions/createConversation.ts +28 -0
  45. package/templates/ai-chat/functions/deleteConversation.ts +15 -20
  46. package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
  47. package/templates/ai-chat/functions/renameConversation.ts +19 -0
  48. package/templates/ai-chat/functions/respond.ts +88 -0
  49. package/templates/ai-chat/functions/seedExamples.ts +46 -0
  50. package/templates/ai-chat/functions/stopResponse.ts +30 -0
  51. package/templates/ai-chat/lib/brand-icon.ts +7 -0
  52. package/templates/ai-chat/lib/chat.ts +277 -0
  53. package/templates/ai-chat/lib/claim.ts +15 -0
  54. package/templates/ai-chat/lib/examples.ts +179 -0
  55. package/templates/ai-chat/lib/highlight.ts +107 -0
  56. package/templates/ai-chat/lib/markdown.ts +317 -0
  57. package/templates/ai-chat/lib/provider.ts +38 -0
  58. package/templates/ai-chat/lib/site.config.ts +55 -39
  59. package/templates/ai-chat/tests/chat.test.ts +174 -0
  60. package/templates/ai-chat/tests/components.test.tsx +57 -0
  61. package/templates/ai-chat/tests/markdown.test.ts +88 -0
  62. package/templates/ai-chat/tests/provider.test.ts +32 -0
  63. package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
  64. package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
  65. package/templates/backend/consumer/apps/api/app.ts +166 -0
  66. package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
  67. package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
  68. package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
  69. package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
  70. package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
  71. package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
  72. package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
  73. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
  74. package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
  75. package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
  76. package/templates/backend/consumer/apps/api/package.json +8 -2
  77. package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
  78. package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
  79. package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
  80. package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
  81. package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
  82. package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
  83. package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
  84. package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
  85. package/templates/chat/README.md +43 -21
  86. package/templates/chat/app/chat-client.tsx +495 -85
  87. package/templates/chat/app/error.tsx +1 -1
  88. package/templates/chat/app/globals.css +170 -54
  89. package/templates/chat/app/layout.tsx +12 -9
  90. package/templates/chat/app/not-found.tsx +4 -2
  91. package/templates/chat/app/page.tsx +9 -19
  92. package/templates/chat/app.ts +96 -17
  93. package/templates/chat/components/chat/avatar.tsx +45 -0
  94. package/templates/chat/components/chat/composer.tsx +110 -0
  95. package/templates/chat/components/chat/message-list.tsx +285 -0
  96. package/templates/chat/components/chat/sidebar.tsx +190 -0
  97. package/templates/chat/functions/joinChat.ts +83 -0
  98. package/templates/chat/functions/renameMember.ts +25 -0
  99. package/templates/chat/lib/chat.ts +188 -0
  100. package/templates/chat/lib/seed.ts +78 -0
  101. package/templates/chat/tests/chat.test.ts +159 -0
  102. package/templates/chat/tests/composer.test.tsx +54 -0
  103. package/templates/consumer/README.md +3 -3
  104. package/templates/consumer/app/globals.css +10 -3
  105. package/templates/consumer/app/icon.svg +1 -0
  106. package/templates/consumer/components/social/avatar.tsx +25 -11
  107. package/templates/consumer/functions/seedFeed.ts +3 -3
  108. package/templates/consumer/lib/seed.ts +43 -85
  109. package/templates/consumer/public/favicon.ico +0 -0
  110. package/templates/consumer/public/images/posts/arch.jpg +0 -0
  111. package/templates/consumer/public/images/posts/coffee.jpg +0 -0
  112. package/templates/consumer/public/images/posts/dog.jpg +0 -0
  113. package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
  114. package/templates/consumer/public/images/posts/ramen.jpg +0 -0
  115. package/templates/consumer/public/images/posts/run.jpg +0 -0
  116. package/templates/consumer/public/images/posts/sunset.jpg +0 -0
  117. package/templates/consumer/tests/seed.test.ts +39 -0
  118. package/templates/creator/.env.example +7 -0
  119. package/templates/creator/README.md +24 -5
  120. package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
  121. package/templates/creator/app/auth-form.tsx +7 -7
  122. package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
  123. package/templates/creator/app/dashboard/page.tsx +18 -18
  124. package/templates/creator/app/icon.svg +2 -0
  125. package/templates/creator/app/login/page.tsx +14 -14
  126. package/templates/creator/app.ts +4 -4
  127. package/templates/creator/functions/seedDemo.ts +32 -0
  128. package/templates/creator/functions/subscribe.ts +4 -15
  129. package/templates/creator/lib/demo.ts +94 -0
  130. package/templates/creator/lib/site.config.ts +15 -10
  131. package/templates/creator/lib/stats.ts +35 -0
  132. package/templates/creator/public/favicon.ico +0 -0
  133. package/templates/creator/tests/counter.test.ts +92 -0
  134. package/templates/creator/tests/live-count.test.tsx +45 -0
  135. package/templates/crm/README.md +22 -1
  136. package/templates/crm/app/apple-icon.png +0 -0
  137. package/templates/crm/app/companies/companies-view.tsx +32 -14
  138. package/templates/crm/app/companies/page.tsx +2 -1
  139. package/templates/crm/app/contacts/contacts-view.tsx +34 -10
  140. package/templates/crm/app/contacts/page.tsx +2 -1
  141. package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
  142. package/templates/crm/app/deals/[id]/page.tsx +2 -1
  143. package/templates/crm/app/globals.css +29 -14
  144. package/templates/crm/app/icon.svg +1 -0
  145. package/templates/crm/app/layout.tsx +7 -5
  146. package/templates/crm/app/page.tsx +2 -1
  147. package/templates/crm/app/pipeline-view.tsx +5 -5
  148. package/templates/crm/app/workspace.tsx +29 -17
  149. package/templates/crm/app.ts +5 -5
  150. package/templates/crm/components/activity-timeline.tsx +14 -4
  151. package/templates/crm/components/app-shell.tsx +94 -0
  152. package/templates/crm/components/auth-form.tsx +35 -5
  153. package/templates/crm/components/avatar.tsx +5 -1
  154. package/templates/crm/components/board-skeleton.tsx +5 -5
  155. package/templates/crm/components/brand-mark.tsx +25 -0
  156. package/templates/crm/components/command-palette.tsx +17 -17
  157. package/templates/crm/components/data-table.tsx +97 -36
  158. package/templates/crm/components/deal-card.tsx +23 -27
  159. package/templates/crm/components/filter-tabs.tsx +101 -0
  160. package/templates/crm/components/metrics-bar.tsx +6 -6
  161. package/templates/crm/components/nav.tsx +15 -0
  162. package/templates/crm/components/page-header.tsx +43 -7
  163. package/templates/crm/components/pipeline-board.tsx +16 -10
  164. package/templates/crm/components/require-auth.tsx +24 -26
  165. package/templates/crm/components/sidebar.tsx +54 -49
  166. package/templates/crm/components/stage-badge.tsx +4 -2
  167. package/templates/crm/components/ui/button.tsx +5 -4
  168. package/templates/crm/components/ui/dialog.tsx +1 -1
  169. package/templates/crm/components/ui/input.tsx +1 -1
  170. package/templates/crm/components/ui/select.tsx +1 -1
  171. package/templates/crm/components/ui/textarea.tsx +1 -1
  172. package/templates/crm/functions/seedWorkspace.ts +32 -17
  173. package/templates/crm/lib/brand.ts +9 -0
  174. package/templates/crm/lib/demo-team.ts +51 -0
  175. package/templates/crm/lib/pipeline.ts +37 -0
  176. package/templates/crm/lib/search.ts +4 -3
  177. package/templates/crm/lib/seed.ts +53 -8
  178. package/templates/crm/public/favicon.ico +0 -0
  179. package/templates/crm/tests/components.test.tsx +5 -5
  180. package/templates/crm/tests/demo-team.test.ts +18 -0
  181. package/templates/crm/tests/pipeline.test.ts +59 -3
  182. package/templates/directory/.env.example +7 -0
  183. package/templates/directory/README.md +22 -3
  184. package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
  185. package/templates/directory/app/(marketing)/page.tsx +6 -4
  186. package/templates/directory/app/auth-form.tsx +4 -4
  187. package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
  188. package/templates/directory/app/dashboard/page.tsx +16 -14
  189. package/templates/directory/app/icon.svg +4 -0
  190. package/templates/directory/app/login/page.tsx +16 -15
  191. package/templates/directory/functions/seedDemo.ts +43 -0
  192. package/templates/directory/lib/demo.ts +145 -0
  193. package/templates/directory/lib/directory.ts +27 -0
  194. package/templates/directory/lib/site.config.ts +21 -16
  195. package/templates/directory/public/favicon.ico +0 -0
  196. package/templates/directory/tests/directory.test.ts +46 -0
  197. package/templates/expo/consumer/apps/expo/App.tsx +360 -288
  198. package/templates/expo/consumer/apps/expo/app.json +9 -1
  199. package/templates/expo/consumer/apps/expo/package.json +2 -1
  200. package/templates/helpdesk/README.md +21 -0
  201. package/templates/helpdesk/app/apple-icon.png +0 -0
  202. package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
  203. package/templates/helpdesk/app/customers/page.tsx +2 -1
  204. package/templates/helpdesk/app/globals.css +29 -14
  205. package/templates/helpdesk/app/icon.svg +1 -0
  206. package/templates/helpdesk/app/inbox-view.tsx +66 -47
  207. package/templates/helpdesk/app/layout.tsx +7 -5
  208. package/templates/helpdesk/app/page.tsx +4 -7
  209. package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
  210. package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
  211. package/templates/helpdesk/app/workspace.tsx +33 -22
  212. package/templates/helpdesk/app.ts +2 -2
  213. package/templates/helpdesk/components/app-shell.tsx +94 -0
  214. package/templates/helpdesk/components/auth-form.tsx +35 -5
  215. package/templates/helpdesk/components/avatar.tsx +5 -1
  216. package/templates/helpdesk/components/brand-mark.tsx +28 -0
  217. package/templates/helpdesk/components/command-palette.tsx +17 -17
  218. package/templates/helpdesk/components/data-table.tsx +97 -36
  219. package/templates/helpdesk/components/filter-tabs.tsx +101 -0
  220. package/templates/helpdesk/components/nav.tsx +14 -0
  221. package/templates/helpdesk/components/page-header.tsx +43 -7
  222. package/templates/helpdesk/components/priority-badge.tsx +49 -6
  223. package/templates/helpdesk/components/require-auth.tsx +24 -26
  224. package/templates/helpdesk/components/sidebar.tsx +54 -48
  225. package/templates/helpdesk/components/sla-indicator.tsx +5 -8
  226. package/templates/helpdesk/components/thread.tsx +7 -7
  227. package/templates/helpdesk/components/ticket-list.tsx +79 -19
  228. package/templates/helpdesk/components/ui/button.tsx +5 -4
  229. package/templates/helpdesk/components/ui/dialog.tsx +1 -1
  230. package/templates/helpdesk/components/ui/input.tsx +1 -1
  231. package/templates/helpdesk/components/ui/select.tsx +1 -1
  232. package/templates/helpdesk/components/ui/textarea.tsx +1 -1
  233. package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
  234. package/templates/helpdesk/lib/brand.ts +9 -0
  235. package/templates/helpdesk/lib/demo-team.ts +51 -0
  236. package/templates/helpdesk/lib/search.ts +4 -3
  237. package/templates/helpdesk/lib/seed.ts +43 -2
  238. package/templates/helpdesk/lib/tickets.ts +68 -20
  239. package/templates/helpdesk/public/favicon.ico +0 -0
  240. package/templates/helpdesk/tests/components.test.tsx +10 -0
  241. package/templates/helpdesk/tests/demo-team.test.ts +18 -0
  242. package/templates/helpdesk/tests/tickets.test.ts +70 -0
  243. package/templates/imessage/.env.example +49 -0
  244. package/templates/imessage/AGENTS.md +153 -0
  245. package/templates/imessage/CLAUDE.md +7 -0
  246. package/templates/imessage/README.md +187 -0
  247. package/templates/imessage/app/console.tsx +382 -0
  248. package/templates/imessage/app/contacts/page.tsx +13 -0
  249. package/templates/imessage/app/error.tsx +16 -0
  250. package/templates/imessage/app/gate.tsx +31 -0
  251. package/templates/imessage/app/globals.css +183 -0
  252. package/templates/imessage/app/layout.tsx +17 -0
  253. package/templates/imessage/app/login/login-form.tsx +115 -0
  254. package/templates/imessage/app/login/page.tsx +30 -0
  255. package/templates/imessage/app/not-found.tsx +13 -0
  256. package/templates/imessage/app/page.tsx +14 -0
  257. package/templates/imessage/app/robots.ts +6 -0
  258. package/templates/imessage/app/setup/page.tsx +13 -0
  259. package/templates/imessage/app/sign-out.tsx +20 -0
  260. package/templates/imessage/app.ts +299 -0
  261. package/templates/imessage/bunfig +4 -0
  262. package/templates/imessage/components/avatar.tsx +46 -0
  263. package/templates/imessage/components/contact-panel.tsx +263 -0
  264. package/templates/imessage/components/contacts-view.tsx +172 -0
  265. package/templates/imessage/components/conversation-list.tsx +129 -0
  266. package/templates/imessage/components/nav-rail.tsx +67 -0
  267. package/templates/imessage/components/setup-view.tsx +408 -0
  268. package/templates/imessage/components/thread.tsx +229 -0
  269. package/templates/imessage/components/ui/badge.tsx +37 -0
  270. package/templates/imessage/components/ui/button.tsx +56 -0
  271. package/templates/imessage/components/ui/card.tsx +90 -0
  272. package/templates/imessage/components/ui/dialog.tsx +110 -0
  273. package/templates/imessage/components/ui/input.tsx +30 -0
  274. package/templates/imessage/components/ui/label.tsx +21 -0
  275. package/templates/imessage/components/ui/select.tsx +39 -0
  276. package/templates/imessage/components/ui/table.tsx +124 -0
  277. package/templates/imessage/components/ui/textarea.tsx +24 -0
  278. package/templates/imessage/components.json +20 -0
  279. package/templates/imessage/functions/answerNow.ts +34 -0
  280. package/templates/imessage/functions/assistant.ts +96 -0
  281. package/templates/imessage/functions/beginTurn.ts +101 -0
  282. package/templates/imessage/functions/cancelReminder.ts +13 -0
  283. package/templates/imessage/functions/claimOutbound.ts +44 -0
  284. package/templates/imessage/functions/clearDemo.ts +18 -0
  285. package/templates/imessage/functions/deleteNote.ts +11 -0
  286. package/templates/imessage/functions/deliverOutbound.ts +55 -0
  287. package/templates/imessage/functions/finishTurn.ts +84 -0
  288. package/templates/imessage/functions/fireReminder.ts +42 -0
  289. package/templates/imessage/functions/ingestInbound.ts +22 -0
  290. package/templates/imessage/functions/ownerAccount.ts +13 -0
  291. package/templates/imessage/functions/processTurn.ts +74 -0
  292. package/templates/imessage/functions/relayApply.ts +102 -0
  293. package/templates/imessage/functions/relaySync.ts +63 -0
  294. package/templates/imessage/functions/releaseOutbound.ts +18 -0
  295. package/templates/imessage/functions/resolveTurn.ts +25 -0
  296. package/templates/imessage/functions/saveContact.ts +27 -0
  297. package/templates/imessage/functions/seedDemo.ts +70 -0
  298. package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
  299. package/templates/imessage/functions/sendblueWebhook.ts +49 -0
  300. package/templates/imessage/functions/setContactStatus.ts +17 -0
  301. package/templates/imessage/functions/settleOutbound.ts +35 -0
  302. package/templates/imessage/functions/setupStatus.ts +33 -0
  303. package/templates/imessage/functions/sweep.ts +58 -0
  304. package/templates/imessage/functions/toolCancelReminder.ts +17 -0
  305. package/templates/imessage/functions/toolForgetNote.ts +15 -0
  306. package/templates/imessage/functions/toolListNotes.ts +16 -0
  307. package/templates/imessage/functions/toolListReminders.ts +17 -0
  308. package/templates/imessage/functions/toolSaveNote.ts +22 -0
  309. package/templates/imessage/functions/toolSetReminder.ts +55 -0
  310. package/templates/imessage/functions/updateSettings.ts +48 -0
  311. package/templates/imessage/gitignore +11 -0
  312. package/templates/imessage/lib/agent-call.ts +251 -0
  313. package/templates/imessage/lib/assistant.ts +74 -0
  314. package/templates/imessage/lib/chunk.ts +135 -0
  315. package/templates/imessage/lib/demo.ts +68 -0
  316. package/templates/imessage/lib/format.ts +170 -0
  317. package/templates/imessage/lib/gate.ts +104 -0
  318. package/templates/imessage/lib/handles.ts +67 -0
  319. package/templates/imessage/lib/provider.ts +38 -0
  320. package/templates/imessage/lib/relay-protocol.ts +95 -0
  321. package/templates/imessage/lib/reminders.ts +41 -0
  322. package/templates/imessage/lib/secrets.ts +59 -0
  323. package/templates/imessage/lib/sendblue.ts +173 -0
  324. package/templates/imessage/lib/store.ts +407 -0
  325. package/templates/imessage/lib/transport.ts +135 -0
  326. package/templates/imessage/lib/utils.ts +10 -0
  327. package/templates/imessage/package.json +40 -0
  328. package/templates/imessage/relay/.env.example +14 -0
  329. package/templates/imessage/relay/applescript.ts +70 -0
  330. package/templates/imessage/relay/chatdb.ts +165 -0
  331. package/templates/imessage/relay/client.ts +41 -0
  332. package/templates/imessage/relay/main.ts +137 -0
  333. package/templates/imessage/relay/state.ts +35 -0
  334. package/templates/imessage/tests/agent-call.test.ts +112 -0
  335. package/templates/imessage/tests/applescript.test.ts +88 -0
  336. package/templates/imessage/tests/chatdb.test.ts +139 -0
  337. package/templates/imessage/tests/chunk.test.ts +61 -0
  338. package/templates/imessage/tests/fake-ctx.ts +88 -0
  339. package/templates/imessage/tests/ingest.test.ts +141 -0
  340. package/templates/imessage/tests/lib.test.ts +99 -0
  341. package/templates/imessage/tests/review-fixes.test.ts +138 -0
  342. package/templates/imessage/tests/secrets.test.ts +36 -0
  343. package/templates/imessage/tests/sendblue.test.ts +143 -0
  344. package/templates/imessage/tests/setup.ts +5 -0
  345. package/templates/imessage/tsconfig.json +18 -0
  346. package/templates/inventory/README.md +21 -0
  347. package/templates/inventory/app/apple-icon.png +0 -0
  348. package/templates/inventory/app/globals.css +29 -14
  349. package/templates/inventory/app/icon.svg +1 -0
  350. package/templates/inventory/app/layout.tsx +7 -5
  351. package/templates/inventory/app/movements/movements-view.tsx +32 -10
  352. package/templates/inventory/app/movements/page.tsx +2 -1
  353. package/templates/inventory/app/page.tsx +4 -6
  354. package/templates/inventory/app/products/[id]/page.tsx +2 -1
  355. package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
  356. package/templates/inventory/app/products-view.tsx +74 -33
  357. package/templates/inventory/app/workspace.tsx +34 -16
  358. package/templates/inventory/app.ts +10 -2
  359. package/templates/inventory/components/app-shell.tsx +94 -0
  360. package/templates/inventory/components/auth-form.tsx +35 -5
  361. package/templates/inventory/components/avatar.tsx +5 -1
  362. package/templates/inventory/components/brand-mark.tsx +25 -0
  363. package/templates/inventory/components/category-tile.tsx +49 -0
  364. package/templates/inventory/components/command-palette.tsx +17 -17
  365. package/templates/inventory/components/data-table.tsx +97 -36
  366. package/templates/inventory/components/filter-tabs.tsx +101 -0
  367. package/templates/inventory/components/nav.tsx +14 -0
  368. package/templates/inventory/components/page-header.tsx +43 -7
  369. package/templates/inventory/components/require-auth.tsx +24 -26
  370. package/templates/inventory/components/sidebar.tsx +54 -48
  371. package/templates/inventory/components/stock-level.tsx +30 -14
  372. package/templates/inventory/components/summary-bar.tsx +8 -15
  373. package/templates/inventory/components/ui/button.tsx +5 -4
  374. package/templates/inventory/components/ui/dialog.tsx +1 -1
  375. package/templates/inventory/components/ui/input.tsx +1 -1
  376. package/templates/inventory/components/ui/select.tsx +1 -1
  377. package/templates/inventory/components/ui/textarea.tsx +1 -1
  378. package/templates/inventory/functions/seedWorkspace.ts +18 -8
  379. package/templates/inventory/lib/brand.ts +9 -0
  380. package/templates/inventory/lib/demo-team.ts +51 -0
  381. package/templates/inventory/lib/search.ts +4 -3
  382. package/templates/inventory/lib/seed.ts +29 -2
  383. package/templates/inventory/lib/stock.ts +32 -0
  384. package/templates/inventory/public/favicon.ico +0 -0
  385. package/templates/inventory/tests/demo-team.test.ts +18 -0
  386. package/templates/inventory/tests/stock.test.ts +29 -0
  387. package/templates/invoices/README.md +16 -0
  388. package/templates/invoices/app/apple-icon.png +0 -0
  389. package/templates/invoices/app/clients/clients-view.tsx +18 -6
  390. package/templates/invoices/app/clients/page.tsx +2 -1
  391. package/templates/invoices/app/globals.css +29 -14
  392. package/templates/invoices/app/icon.svg +1 -0
  393. package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
  394. package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
  395. package/templates/invoices/app/invoices-view.tsx +81 -32
  396. package/templates/invoices/app/layout.tsx +7 -5
  397. package/templates/invoices/app/page.tsx +4 -6
  398. package/templates/invoices/app/workspace.tsx +26 -16
  399. package/templates/invoices/app.ts +2 -2
  400. package/templates/invoices/components/app-shell.tsx +94 -0
  401. package/templates/invoices/components/auth-form.tsx +35 -5
  402. package/templates/invoices/components/avatar.tsx +5 -1
  403. package/templates/invoices/components/brand-mark.tsx +26 -0
  404. package/templates/invoices/components/command-palette.tsx +17 -17
  405. package/templates/invoices/components/data-table.tsx +97 -36
  406. package/templates/invoices/components/filter-tabs.tsx +101 -0
  407. package/templates/invoices/components/line-items.tsx +19 -14
  408. package/templates/invoices/components/nav.tsx +14 -0
  409. package/templates/invoices/components/page-header.tsx +43 -7
  410. package/templates/invoices/components/require-auth.tsx +24 -26
  411. package/templates/invoices/components/sidebar.tsx +54 -48
  412. package/templates/invoices/components/status-badge.tsx +7 -7
  413. package/templates/invoices/components/summary-bar.tsx +5 -5
  414. package/templates/invoices/components/ui/button.tsx +5 -4
  415. package/templates/invoices/components/ui/dialog.tsx +1 -1
  416. package/templates/invoices/components/ui/input.tsx +1 -1
  417. package/templates/invoices/components/ui/select.tsx +1 -1
  418. package/templates/invoices/components/ui/textarea.tsx +1 -1
  419. package/templates/invoices/functions/recordPayment.ts +3 -0
  420. package/templates/invoices/lib/billing.ts +40 -0
  421. package/templates/invoices/lib/brand.ts +9 -0
  422. package/templates/invoices/lib/search.ts +4 -3
  423. package/templates/invoices/public/favicon.ico +0 -0
  424. package/templates/invoices/tests/billing.test.ts +27 -0
  425. package/templates/ios/consumer/apps/ios/Package.swift +4 -3
  426. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
  427. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
  428. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
  429. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
  430. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
  431. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
  432. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
  433. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
  434. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
  435. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
  436. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
  437. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
  438. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
  439. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
  440. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
  441. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
  442. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
  443. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
  444. package/templates/ios/consumer/apps/ios/project.yml +13 -9
  445. package/templates/local-service/.env.example +7 -0
  446. package/templates/local-service/README.md +26 -2
  447. package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
  448. package/templates/local-service/app/(marketing)/page.tsx +66 -42
  449. package/templates/local-service/app/auth-form.tsx +8 -8
  450. package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
  451. package/templates/local-service/app/dashboard/page.tsx +14 -14
  452. package/templates/local-service/app/icon.svg +2 -0
  453. package/templates/local-service/app/login/page.tsx +15 -15
  454. package/templates/local-service/functions/seedDemo.ts +49 -0
  455. package/templates/local-service/lib/demo.ts +135 -0
  456. package/templates/local-service/lib/hours.ts +44 -0
  457. package/templates/local-service/lib/site.config.ts +8 -3
  458. package/templates/local-service/lib/slots.ts +3 -0
  459. package/templates/local-service/public/favicon.ico +0 -0
  460. package/templates/local-service/tests/booking.test.ts +104 -0
  461. package/templates/marketplace/README.md +23 -15
  462. package/templates/marketplace/app/globals.css +172 -114
  463. package/templates/marketplace/app/layout.tsx +75 -30
  464. package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
  465. package/templates/marketplace/app/me/page.tsx +4 -4
  466. package/templates/marketplace/app/page.tsx +221 -298
  467. package/templates/marketplace/app/sell/page.tsx +8 -11
  468. package/templates/marketplace/app.ts +32 -7
  469. package/templates/marketplace/client/ActivityFeed.tsx +311 -0
  470. package/templates/marketplace/client/AuthNav.tsx +24 -16
  471. package/templates/marketplace/client/ListingCard.tsx +118 -0
  472. package/templates/marketplace/client/LiveGrid.tsx +92 -0
  473. package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
  474. package/templates/marketplace/client/LoginCard.tsx +114 -136
  475. package/templates/marketplace/client/MarketProvider.tsx +3 -5
  476. package/templates/marketplace/client/MyMarket.tsx +371 -185
  477. package/templates/marketplace/client/OfferPanel.tsx +421 -287
  478. package/templates/marketplace/client/SellForm.tsx +271 -192
  479. package/templates/marketplace/client/ThemeToggle.tsx +1 -0
  480. package/templates/marketplace/client/WatchButton.tsx +16 -16
  481. package/templates/marketplace/client/market.ts +61 -14
  482. package/templates/marketplace/functions/seedMarket.ts +143 -36
  483. package/templates/marketplace/functions/seedOffers.ts +101 -0
  484. package/templates/marketplace/lib/catalog.ts +72 -1
  485. package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
  486. package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
  487. package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
  488. package/templates/marketplace/public/images/listings/duffel.webp +0 -0
  489. package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
  490. package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
  491. package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
  492. package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
  493. package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
  494. package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
  495. package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
  496. package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
  497. package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
  498. package/templates/marketplace/public/images/listings/turntable.webp +0 -0
  499. package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
  500. package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
  501. package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
  502. package/templates/marketplace/public/images/listings/watch.webp +0 -0
  503. package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
  504. package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
  505. package/templates/marketplace/tests/example.test.ts +64 -2
  506. package/templates/marketplace/tests/listing-card.test.tsx +61 -0
  507. package/templates/projects/README.md +24 -3
  508. package/templates/projects/app/apple-icon.png +0 -0
  509. package/templates/projects/app/clients/clients-view.tsx +20 -6
  510. package/templates/projects/app/clients/page.tsx +2 -1
  511. package/templates/projects/app/globals.css +29 -14
  512. package/templates/projects/app/icon.svg +1 -0
  513. package/templates/projects/app/layout.tsx +7 -5
  514. package/templates/projects/app/page.tsx +3 -5
  515. package/templates/projects/app/projects/[id]/page.tsx +2 -1
  516. package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
  517. package/templates/projects/app/projects-view.tsx +105 -52
  518. package/templates/projects/app/workspace.tsx +41 -20
  519. package/templates/projects/app.ts +2 -2
  520. package/templates/projects/components/app-shell.tsx +94 -0
  521. package/templates/projects/components/auth-form.tsx +35 -5
  522. package/templates/projects/components/avatar.tsx +5 -1
  523. package/templates/projects/components/board-skeleton.tsx +3 -3
  524. package/templates/projects/components/brand-mark.tsx +25 -0
  525. package/templates/projects/components/command-palette.tsx +17 -17
  526. package/templates/projects/components/data-table.tsx +97 -36
  527. package/templates/projects/components/filter-tabs.tsx +101 -0
  528. package/templates/projects/components/meter.tsx +48 -0
  529. package/templates/projects/components/nav.tsx +14 -0
  530. package/templates/projects/components/page-header.tsx +43 -7
  531. package/templates/projects/components/require-auth.tsx +24 -26
  532. package/templates/projects/components/sidebar.tsx +54 -48
  533. package/templates/projects/components/summary-bar.tsx +49 -0
  534. package/templates/projects/components/task-board.tsx +10 -9
  535. package/templates/projects/components/time-dialog.tsx +29 -5
  536. package/templates/projects/components/ui/button.tsx +5 -4
  537. package/templates/projects/components/ui/dialog.tsx +1 -1
  538. package/templates/projects/components/ui/input.tsx +1 -1
  539. package/templates/projects/components/ui/select.tsx +1 -1
  540. package/templates/projects/components/ui/textarea.tsx +1 -1
  541. package/templates/projects/functions/seedWorkspace.ts +26 -12
  542. package/templates/projects/lib/brand.ts +9 -0
  543. package/templates/projects/lib/demo-team.ts +51 -0
  544. package/templates/projects/lib/search.ts +4 -3
  545. package/templates/projects/lib/seed.ts +35 -1
  546. package/templates/projects/lib/work.ts +63 -0
  547. package/templates/projects/public/favicon.ico +0 -0
  548. package/templates/projects/tests/demo-team.test.ts +18 -0
  549. package/templates/projects/tests/work.test.ts +65 -0
  550. package/templates/restaurant/.env.example +7 -0
  551. package/templates/restaurant/README.md +29 -3
  552. package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
  553. package/templates/restaurant/app/(marketing)/page.tsx +47 -23
  554. package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
  555. package/templates/restaurant/app/auth-form.tsx +8 -8
  556. package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
  557. package/templates/restaurant/app/dashboard/page.tsx +14 -14
  558. package/templates/restaurant/app/icon.svg +4 -0
  559. package/templates/restaurant/app/login/page.tsx +15 -15
  560. package/templates/restaurant/functions/seedDemo.ts +44 -0
  561. package/templates/restaurant/lib/demo.ts +144 -0
  562. package/templates/restaurant/lib/hours.ts +45 -0
  563. package/templates/restaurant/lib/site.config.ts +3 -3
  564. package/templates/restaurant/public/favicon.ico +0 -0
  565. package/templates/restaurant/tests/reservations.test.ts +67 -0
  566. package/templates/saas/README.md +54 -35
  567. package/templates/saas/app/(auth)/auth-form.tsx +25 -23
  568. package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
  569. package/templates/saas/app/(auth)/layout.tsx +27 -33
  570. package/templates/saas/app/(auth)/login/page.tsx +1 -1
  571. package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
  572. package/templates/saas/app/(auth)/signup/page.tsx +1 -1
  573. package/templates/saas/app/(marketing)/layout.tsx +127 -253
  574. package/templates/saas/app/(marketing)/page.tsx +133 -372
  575. package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
  576. package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
  577. package/templates/saas/app/dashboard/board-client.tsx +416 -0
  578. package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
  579. package/templates/saas/app/dashboard/layout.tsx +27 -13
  580. package/templates/saas/app/dashboard/overview-client.tsx +332 -0
  581. package/templates/saas/app/dashboard/page.tsx +29 -35
  582. package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
  583. package/templates/saas/app/dashboard/projects/page.tsx +11 -7
  584. package/templates/saas/app/dashboard/projects-client.tsx +398 -0
  585. package/templates/saas/app/globals.css +5 -4
  586. package/templates/saas/app/icon.svg +1 -0
  587. package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
  588. package/templates/saas/app/onboarding/page.tsx +6 -2
  589. package/templates/saas/app.ts +68 -13
  590. package/templates/saas/components/dashboard-shell.tsx +212 -132
  591. package/templates/saas/components/marketing.tsx +137 -274
  592. package/templates/saas/components/pricing-table.tsx +12 -11
  593. package/templates/saas/components/setup-checklist.tsx +31 -29
  594. package/templates/saas/components/task-ui.tsx +199 -0
  595. package/templates/saas/functions/createProject.ts +7 -13
  596. package/templates/saas/functions/createTask.ts +67 -0
  597. package/templates/saas/functions/deleteProject.ts +17 -0
  598. package/templates/saas/functions/deleteTask.ts +14 -0
  599. package/templates/saas/functions/seedWorkspace.ts +46 -0
  600. package/templates/saas/functions/setProjectStatus.ts +41 -0
  601. package/templates/saas/functions/updateProfile.ts +13 -0
  602. package/templates/saas/functions/updateProject.ts +4 -5
  603. package/templates/saas/functions/updateTask.ts +88 -0
  604. package/templates/saas/lib/members.ts +53 -0
  605. package/templates/saas/lib/names.ts +53 -0
  606. package/templates/saas/lib/plans.ts +11 -0
  607. package/templates/saas/lib/sample-workspace.ts +117 -0
  608. package/templates/saas/lib/site.config.ts +200 -463
  609. package/templates/saas/lib/tasks.ts +101 -0
  610. package/templates/saas/lib/use-live.ts +25 -0
  611. package/templates/saas/public/favicon.ico +0 -0
  612. package/templates/saas/public/screenshots/board.png +0 -0
  613. package/templates/saas/public/screenshots/members.png +0 -0
  614. package/templates/saas/public/screenshots/overview.png +0 -0
  615. package/templates/saas/public/screenshots/projects.png +0 -0
  616. package/templates/saas/scripts/capture-screenshots.mjs +156 -0
  617. package/templates/saas/tests/names.test.ts +35 -0
  618. package/templates/saas/tests/plans.test.ts +12 -0
  619. package/templates/saas/tests/sample-workspace.test.ts +46 -0
  620. package/templates/saas/tests/tasks.test.ts +81 -0
  621. package/templates/shop/README.md +25 -13
  622. package/templates/shop/app/(marketing)/layout.tsx +94 -67
  623. package/templates/shop/app/(marketing)/not-found.tsx +13 -12
  624. package/templates/shop/app/(marketing)/page.tsx +120 -59
  625. package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
  626. package/templates/shop/app/(marketing)/success/page.tsx +75 -22
  627. package/templates/shop/app/globals.css +94 -1
  628. package/templates/shop/app/layout.tsx +4 -4
  629. package/templates/shop/app/login/page.tsx +4 -4
  630. package/templates/shop/app.ts +18 -6
  631. package/templates/shop/components/cart-button.tsx +33 -0
  632. package/templates/shop/components/marketing.tsx +17 -87
  633. package/templates/shop/components/shelf-count.tsx +36 -0
  634. package/templates/shop/functions/checkout.ts +17 -1
  635. package/templates/shop/lib/cart-store.ts +57 -0
  636. package/templates/shop/lib/shelf-store.ts +35 -0
  637. package/templates/shop/lib/shop.ts +27 -0
  638. package/templates/shop/lib/site.config.ts +108 -98
  639. package/templates/shop/public/images/hero.jpg +0 -0
  640. package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
  641. package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
  642. package/templates/shop/public/images/products/linen.jpg +0 -0
  643. package/templates/shop/public/images/products/sea-salt.jpg +0 -0
  644. package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
  645. package/templates/shop/tests/shop.test.ts +64 -0
  646. package/templates/waitlist/.env.example +7 -0
  647. package/templates/waitlist/README.md +29 -13
  648. package/templates/waitlist/app/(marketing)/page.tsx +9 -7
  649. package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
  650. package/templates/waitlist/app/auth-form.tsx +8 -8
  651. package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
  652. package/templates/waitlist/app/dashboard/page.tsx +17 -16
  653. package/templates/waitlist/app/icon.svg +2 -0
  654. package/templates/waitlist/app/login/page.tsx +14 -14
  655. package/templates/waitlist/app.ts +6 -6
  656. package/templates/waitlist/functions/joinWaitlist.ts +6 -18
  657. package/templates/waitlist/functions/seedDemo.ts +32 -0
  658. package/templates/waitlist/lib/demo.ts +94 -0
  659. package/templates/waitlist/lib/launch.ts +19 -0
  660. package/templates/waitlist/lib/site.config.ts +14 -9
  661. package/templates/waitlist/lib/stats.ts +35 -0
  662. package/templates/waitlist/public/favicon.ico +0 -0
  663. package/templates/waitlist/tests/counter.test.ts +107 -0
  664. package/templates/waitlist/tests/live-count.test.tsx +48 -0
  665. package/templates/ai-chat/tests/example.test.ts +0 -12
  666. package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
  667. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
  668. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
  669. package/templates/backend/consumer/apps/api/schema.ts +0 -143
  670. package/templates/chat/functions/_keep.ts +0 -13
  671. package/templates/chat/tests/example.test.ts +0 -12
  672. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
  673. package/templates/marketplace/client/LiveTicker.tsx +0 -116
  674. package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
  675. 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
- // deleteConversation — remove a conversation AND all its messages in one
4
- // transaction. The client could delete the Conversation row itself (it's
5
- // owner-scoped), but that would orphan the messages; this cascades. Gated to the
6
- // owner: it verifies the conversation belongs to the caller before deleting.
7
- export default mutation<{ conversationId: string }, { ok: boolean; deleted: number }>({
8
- auth: "user",
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
- const messages = (await ctx.db.unsafe.list("Message")) as unknown as {
20
- id: string;
21
- conversationId: string;
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.conversationId === args.conversationId) {
26
- await ctx.db.unsafe.delete("Message", m.id);
27
- deleted++;
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
+ });