@pylonsync/create-pylon 0.3.273 → 0.3.275

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 (323) hide show
  1. package/bin/create-pylon.js +80 -0
  2. package/package.json +1 -1
  3. package/templates/ARCHETYPES.md +339 -0
  4. package/templates/agency/.env.example +12 -0
  5. package/templates/agency/AGENTS.md +61 -0
  6. package/templates/agency/README.md +90 -0
  7. package/templates/agency/app/auth-form.tsx +129 -0
  8. package/templates/agency/app/contact-form.tsx +258 -0
  9. package/templates/agency/app/dashboard/dashboard-client.tsx +286 -0
  10. package/templates/agency/app/dashboard/page.tsx +70 -0
  11. package/templates/agency/app/error.tsx +26 -0
  12. package/templates/agency/app/globals.css +148 -0
  13. package/templates/agency/app/layout.tsx +174 -0
  14. package/templates/agency/app/login/page.tsx +39 -0
  15. package/templates/agency/app/not-found.tsx +19 -0
  16. package/templates/agency/app/page.tsx +207 -0
  17. package/templates/agency/app/robots.ts +12 -0
  18. package/templates/agency/app/sitemap.ts +9 -0
  19. package/templates/agency/app.ts +135 -0
  20. package/templates/agency/components/marketing.tsx +148 -0
  21. package/templates/agency/components/section-scroller.tsx +35 -0
  22. package/templates/agency/components/ui/button.tsx +56 -0
  23. package/templates/agency/components/ui/card.tsx +90 -0
  24. package/templates/agency/components.json +20 -0
  25. package/templates/agency/functions/bookInquiry.ts +42 -0
  26. package/templates/agency/functions/declineInquiry.ts +41 -0
  27. package/templates/agency/functions/inquiriesForOwner.ts +31 -0
  28. package/templates/agency/functions/seedCapacity.ts +26 -0
  29. package/templates/agency/functions/setCapacity.ts +32 -0
  30. package/templates/agency/functions/submitInquiry.ts +55 -0
  31. package/templates/agency/gitignore +10 -0
  32. package/templates/agency/lib/agency.ts +27 -0
  33. package/templates/agency/lib/owner.ts +26 -0
  34. package/templates/agency/lib/site.config.ts +239 -0
  35. package/templates/agency/lib/utils.ts +10 -0
  36. package/templates/agency/package.json +34 -0
  37. package/templates/agency/tsconfig.json +18 -0
  38. package/templates/ai-chat/.env.example +33 -0
  39. package/templates/ai-chat/AGENTS.md +61 -0
  40. package/templates/ai-chat/README.md +99 -0
  41. package/templates/ai-chat/app/auth-form.tsx +124 -0
  42. package/templates/ai-chat/app/chat-client.tsx +414 -0
  43. package/templates/ai-chat/app/error.tsx +26 -0
  44. package/templates/ai-chat/app/globals.css +148 -0
  45. package/templates/ai-chat/app/layout.tsx +75 -0
  46. package/templates/ai-chat/app/login/page.tsx +39 -0
  47. package/templates/ai-chat/app/not-found.tsx +19 -0
  48. package/templates/ai-chat/app/page.tsx +23 -0
  49. package/templates/ai-chat/app.ts +121 -0
  50. package/templates/ai-chat/components.json +20 -0
  51. package/templates/ai-chat/gitignore +10 -0
  52. package/templates/ai-chat/lib/site.config.ts +103 -0
  53. package/templates/ai-chat/lib/utils.ts +10 -0
  54. package/templates/ai-chat/package.json +34 -0
  55. package/templates/ai-chat/tsconfig.json +18 -0
  56. package/templates/ai-studio/.env.example +19 -0
  57. package/templates/ai-studio/AGENTS.md +61 -0
  58. package/templates/ai-studio/README.md +83 -0
  59. package/templates/ai-studio/app/auth-form.tsx +124 -0
  60. package/templates/ai-studio/app/error.tsx +26 -0
  61. package/templates/ai-studio/app/globals.css +148 -0
  62. package/templates/ai-studio/app/layout.tsx +75 -0
  63. package/templates/ai-studio/app/login/page.tsx +39 -0
  64. package/templates/ai-studio/app/not-found.tsx +19 -0
  65. package/templates/ai-studio/app/page.tsx +34 -0
  66. package/templates/ai-studio/app/studio-client.tsx +214 -0
  67. package/templates/ai-studio/app.ts +108 -0
  68. package/templates/ai-studio/components.json +20 -0
  69. package/templates/ai-studio/functions/_getGeneration.ts +25 -0
  70. package/templates/ai-studio/functions/_updateGeneration.ts +37 -0
  71. package/templates/ai-studio/functions/generate.ts +42 -0
  72. package/templates/ai-studio/functions/pollGeneration.ts +134 -0
  73. package/templates/ai-studio/gitignore +10 -0
  74. package/templates/ai-studio/lib/site.config.ts +80 -0
  75. package/templates/ai-studio/lib/studio.ts +52 -0
  76. package/templates/ai-studio/lib/utils.ts +10 -0
  77. package/templates/ai-studio/package.json +34 -0
  78. package/templates/ai-studio/tsconfig.json +18 -0
  79. package/templates/creator/.env.example +12 -0
  80. package/templates/creator/AGENTS.md +61 -0
  81. package/templates/creator/README.md +67 -0
  82. package/templates/creator/app/auth-form.tsx +129 -0
  83. package/templates/creator/app/dashboard/dashboard-client.tsx +297 -0
  84. package/templates/creator/app/dashboard/page.tsx +70 -0
  85. package/templates/creator/app/error.tsx +26 -0
  86. package/templates/creator/app/globals.css +148 -0
  87. package/templates/creator/app/layout.tsx +160 -0
  88. package/templates/creator/app/login/page.tsx +39 -0
  89. package/templates/creator/app/newsletter-signup.tsx +162 -0
  90. package/templates/creator/app/not-found.tsx +19 -0
  91. package/templates/creator/app/page.tsx +160 -0
  92. package/templates/creator/app/robots.ts +12 -0
  93. package/templates/creator/app/sitemap.ts +9 -0
  94. package/templates/creator/app.ts +134 -0
  95. package/templates/creator/components/marketing.tsx +148 -0
  96. package/templates/creator/components/section-scroller.tsx +35 -0
  97. package/templates/creator/components/ui/button.tsx +56 -0
  98. package/templates/creator/components/ui/card.tsx +90 -0
  99. package/templates/creator/components.json +20 -0
  100. package/templates/creator/functions/subscribe.ts +82 -0
  101. package/templates/creator/functions/subscriberStats.ts +75 -0
  102. package/templates/creator/gitignore +10 -0
  103. package/templates/creator/lib/owner.ts +26 -0
  104. package/templates/creator/lib/site.config.ts +173 -0
  105. package/templates/creator/lib/stats.ts +30 -0
  106. package/templates/creator/lib/utils.ts +10 -0
  107. package/templates/creator/package.json +34 -0
  108. package/templates/creator/tsconfig.json +18 -0
  109. package/templates/default/app/layout.tsx +26 -27
  110. package/templates/default/app/page.tsx +90 -274
  111. package/templates/default/lib/products.ts +9 -122
  112. package/templates/default/lib/site.config.ts +739 -0
  113. package/templates/default/lib/site.ts +14 -261
  114. package/templates/directory/.env.example +12 -0
  115. package/templates/directory/AGENTS.md +61 -0
  116. package/templates/directory/README.md +80 -0
  117. package/templates/directory/app/auth-form.tsx +129 -0
  118. package/templates/directory/app/dashboard/dashboard-client.tsx +205 -0
  119. package/templates/directory/app/dashboard/page.tsx +70 -0
  120. package/templates/directory/app/directory-browse.tsx +328 -0
  121. package/templates/directory/app/error.tsx +26 -0
  122. package/templates/directory/app/globals.css +148 -0
  123. package/templates/directory/app/layout.tsx +171 -0
  124. package/templates/directory/app/login/page.tsx +39 -0
  125. package/templates/directory/app/not-found.tsx +19 -0
  126. package/templates/directory/app/page.tsx +50 -0
  127. package/templates/directory/app/robots.ts +12 -0
  128. package/templates/directory/app/sitemap.ts +9 -0
  129. package/templates/directory/app/submit/page.tsx +30 -0
  130. package/templates/directory/app/submit-form.tsx +151 -0
  131. package/templates/directory/app.ts +146 -0
  132. package/templates/directory/components/marketing.tsx +148 -0
  133. package/templates/directory/components/section-scroller.tsx +35 -0
  134. package/templates/directory/components/ui/button.tsx +56 -0
  135. package/templates/directory/components/ui/card.tsx +90 -0
  136. package/templates/directory/components.json +20 -0
  137. package/templates/directory/functions/approveSubmission.ts +45 -0
  138. package/templates/directory/functions/rejectSubmission.ts +20 -0
  139. package/templates/directory/functions/seedListings.ts +33 -0
  140. package/templates/directory/functions/submissionsForOwner.ts +29 -0
  141. package/templates/directory/functions/submitListing.ts +63 -0
  142. package/templates/directory/functions/upvote.ts +24 -0
  143. package/templates/directory/gitignore +10 -0
  144. package/templates/directory/lib/directory.ts +45 -0
  145. package/templates/directory/lib/owner.ts +26 -0
  146. package/templates/directory/lib/site.config.ts +130 -0
  147. package/templates/directory/lib/utils.ts +10 -0
  148. package/templates/directory/package.json +34 -0
  149. package/templates/directory/tsconfig.json +18 -0
  150. package/templates/local-service/.env.example +12 -0
  151. package/templates/local-service/AGENTS.md +61 -0
  152. package/templates/local-service/README.md +82 -0
  153. package/templates/local-service/app/auth-form.tsx +129 -0
  154. package/templates/local-service/app/booking-widget.tsx +399 -0
  155. package/templates/local-service/app/dashboard/dashboard-client.tsx +304 -0
  156. package/templates/local-service/app/dashboard/page.tsx +63 -0
  157. package/templates/local-service/app/error.tsx +26 -0
  158. package/templates/local-service/app/globals.css +148 -0
  159. package/templates/local-service/app/layout.tsx +151 -0
  160. package/templates/local-service/app/login/page.tsx +39 -0
  161. package/templates/local-service/app/not-found.tsx +19 -0
  162. package/templates/local-service/app/page.tsx +233 -0
  163. package/templates/local-service/app/robots.ts +12 -0
  164. package/templates/local-service/app/sitemap.ts +9 -0
  165. package/templates/local-service/app.ts +131 -0
  166. package/templates/local-service/components/marketing.tsx +162 -0
  167. package/templates/local-service/components/section-scroller.tsx +35 -0
  168. package/templates/local-service/components/ui/button.tsx +56 -0
  169. package/templates/local-service/components/ui/card.tsx +90 -0
  170. package/templates/local-service/components.json +20 -0
  171. package/templates/local-service/functions/bookingsForOwner.ts +30 -0
  172. package/templates/local-service/functions/cancelBooking.ts +27 -0
  173. package/templates/local-service/functions/confirmBooking.ts +18 -0
  174. package/templates/local-service/functions/createBooking.ts +98 -0
  175. package/templates/local-service/gitignore +10 -0
  176. package/templates/local-service/lib/booking.ts +24 -0
  177. package/templates/local-service/lib/owner.ts +26 -0
  178. package/templates/local-service/lib/site.config.ts +232 -0
  179. package/templates/local-service/lib/slots.ts +97 -0
  180. package/templates/local-service/lib/utils.ts +10 -0
  181. package/templates/local-service/package.json +34 -0
  182. package/templates/local-service/tsconfig.json +18 -0
  183. package/templates/marketplace/.env.example +9 -0
  184. package/templates/marketplace/AGENTS.md +61 -0
  185. package/templates/marketplace/README.md +78 -0
  186. package/templates/marketplace/app/_components/CategoryIcon.tsx +40 -0
  187. package/templates/marketplace/app/error.tsx +26 -0
  188. package/templates/marketplace/app/globals.css +64 -0
  189. package/templates/marketplace/app/layout.tsx +60 -0
  190. package/templates/marketplace/app/listing/[id]/page.tsx +163 -0
  191. package/templates/marketplace/app/me/page.tsx +15 -0
  192. package/templates/marketplace/app/not-found.tsx +20 -0
  193. package/templates/marketplace/app/page.tsx +159 -0
  194. package/templates/marketplace/app/robots.ts +12 -0
  195. package/templates/marketplace/app/sell/page.tsx +26 -0
  196. package/templates/marketplace/app/sitemap.ts +14 -0
  197. package/templates/marketplace/app.ts +190 -0
  198. package/templates/marketplace/client/AuthNav.tsx +46 -0
  199. package/templates/marketplace/client/LiveTicker.tsx +104 -0
  200. package/templates/marketplace/client/LoginCard.tsx +130 -0
  201. package/templates/marketplace/client/MarketProvider.tsx +148 -0
  202. package/templates/marketplace/client/MyMarket.tsx +180 -0
  203. package/templates/marketplace/client/OfferPanel.tsx +355 -0
  204. package/templates/marketplace/client/SeedOnEmpty.tsx +26 -0
  205. package/templates/marketplace/client/SellForm.tsx +160 -0
  206. package/templates/marketplace/client/WatchButton.tsx +88 -0
  207. package/templates/marketplace/client/market.ts +341 -0
  208. package/templates/marketplace/functions/buyNow.ts +78 -0
  209. package/templates/marketplace/functions/makeOffer.ts +65 -0
  210. package/templates/marketplace/functions/respondToOffer.ts +62 -0
  211. package/templates/marketplace/functions/seedMarket.ts +90 -0
  212. package/templates/marketplace/gitignore +10 -0
  213. package/templates/marketplace/package.json +35 -0
  214. package/templates/marketplace/tsconfig.json +14 -0
  215. package/templates/marketplace/ui/badge.tsx +30 -0
  216. package/templates/marketplace/ui/button.tsx +49 -0
  217. package/templates/marketplace/ui/card.tsx +48 -0
  218. package/templates/marketplace/ui/input.tsx +17 -0
  219. package/templates/marketplace/ui/label.tsx +18 -0
  220. package/templates/marketplace/ui/textarea.tsx +17 -0
  221. package/templates/marketplace/ui/tokens.css +32 -0
  222. package/templates/marketplace/ui/utils.ts +6 -0
  223. package/templates/restaurant/.env.example +12 -0
  224. package/templates/restaurant/AGENTS.md +61 -0
  225. package/templates/restaurant/README.md +77 -0
  226. package/templates/restaurant/app/auth-form.tsx +129 -0
  227. package/templates/restaurant/app/dashboard/dashboard-client.tsx +263 -0
  228. package/templates/restaurant/app/dashboard/page.tsx +59 -0
  229. package/templates/restaurant/app/error.tsx +26 -0
  230. package/templates/restaurant/app/globals.css +148 -0
  231. package/templates/restaurant/app/layout.tsx +151 -0
  232. package/templates/restaurant/app/login/page.tsx +39 -0
  233. package/templates/restaurant/app/not-found.tsx +19 -0
  234. package/templates/restaurant/app/page.tsx +194 -0
  235. package/templates/restaurant/app/reservation-widget.tsx +359 -0
  236. package/templates/restaurant/app/robots.ts +12 -0
  237. package/templates/restaurant/app/sitemap.ts +9 -0
  238. package/templates/restaurant/app.ts +115 -0
  239. package/templates/restaurant/components/marketing.tsx +162 -0
  240. package/templates/restaurant/components/section-scroller.tsx +35 -0
  241. package/templates/restaurant/components/ui/button.tsx +56 -0
  242. package/templates/restaurant/components/ui/card.tsx +90 -0
  243. package/templates/restaurant/components.json +20 -0
  244. package/templates/restaurant/functions/cancelReservation.ts +26 -0
  245. package/templates/restaurant/functions/confirmReservation.ts +17 -0
  246. package/templates/restaurant/functions/createReservation.ts +92 -0
  247. package/templates/restaurant/functions/reservationsForOwner.ts +28 -0
  248. package/templates/restaurant/gitignore +10 -0
  249. package/templates/restaurant/lib/owner.ts +26 -0
  250. package/templates/restaurant/lib/reservation.ts +22 -0
  251. package/templates/restaurant/lib/site.config.ts +218 -0
  252. package/templates/restaurant/lib/slots.ts +55 -0
  253. package/templates/restaurant/lib/utils.ts +10 -0
  254. package/templates/restaurant/package.json +34 -0
  255. package/templates/restaurant/tsconfig.json +18 -0
  256. package/templates/shop/.env.example +32 -0
  257. package/templates/shop/AGENTS.md +61 -0
  258. package/templates/shop/README.md +102 -0
  259. package/templates/shop/app/auth-form.tsx +129 -0
  260. package/templates/shop/app/dashboard/dashboard-client.tsx +264 -0
  261. package/templates/shop/app/dashboard/page.tsx +59 -0
  262. package/templates/shop/app/error.tsx +26 -0
  263. package/templates/shop/app/globals.css +148 -0
  264. package/templates/shop/app/layout.tsx +160 -0
  265. package/templates/shop/app/login/page.tsx +39 -0
  266. package/templates/shop/app/not-found.tsx +19 -0
  267. package/templates/shop/app/page.tsx +95 -0
  268. package/templates/shop/app/robots.ts +12 -0
  269. package/templates/shop/app/shop-client.tsx +436 -0
  270. package/templates/shop/app/sitemap.ts +9 -0
  271. package/templates/shop/app/success/page.tsx +33 -0
  272. package/templates/shop/app.ts +134 -0
  273. package/templates/shop/components/marketing.tsx +96 -0
  274. package/templates/shop/components/section-scroller.tsx +35 -0
  275. package/templates/shop/components/ui/button.tsx +56 -0
  276. package/templates/shop/components/ui/card.tsx +90 -0
  277. package/templates/shop/components.json +20 -0
  278. package/templates/shop/functions/cancelOrder.ts +33 -0
  279. package/templates/shop/functions/checkout.ts +130 -0
  280. package/templates/shop/functions/fulfillOrder.ts +17 -0
  281. package/templates/shop/functions/markGroupPaid.ts +26 -0
  282. package/templates/shop/functions/ordersForOwner.ts +28 -0
  283. package/templates/shop/functions/releaseGroup.ts +36 -0
  284. package/templates/shop/functions/reserveCart.ts +87 -0
  285. package/templates/shop/functions/restockProduct.ts +23 -0
  286. package/templates/shop/functions/seedProducts.ts +30 -0
  287. package/templates/shop/functions/stripeWebhook.ts +72 -0
  288. package/templates/shop/gitignore +10 -0
  289. package/templates/shop/lib/owner.ts +26 -0
  290. package/templates/shop/lib/shop.ts +45 -0
  291. package/templates/shop/lib/site.config.ts +198 -0
  292. package/templates/shop/lib/utils.ts +10 -0
  293. package/templates/shop/package.json +35 -0
  294. package/templates/shop/tsconfig.json +18 -0
  295. package/templates/waitlist/.env.example +12 -0
  296. package/templates/waitlist/AGENTS.md +61 -0
  297. package/templates/waitlist/README.md +81 -0
  298. package/templates/waitlist/app/auth-form.tsx +129 -0
  299. package/templates/waitlist/app/dashboard/dashboard-client.tsx +297 -0
  300. package/templates/waitlist/app/dashboard/page.tsx +70 -0
  301. package/templates/waitlist/app/error.tsx +26 -0
  302. package/templates/waitlist/app/globals.css +148 -0
  303. package/templates/waitlist/app/layout.tsx +158 -0
  304. package/templates/waitlist/app/login/page.tsx +39 -0
  305. package/templates/waitlist/app/not-found.tsx +19 -0
  306. package/templates/waitlist/app/page.tsx +119 -0
  307. package/templates/waitlist/app/robots.ts +12 -0
  308. package/templates/waitlist/app/sitemap.ts +9 -0
  309. package/templates/waitlist/app/waitlist-hero.tsx +219 -0
  310. package/templates/waitlist/app.ts +134 -0
  311. package/templates/waitlist/components/marketing.tsx +96 -0
  312. package/templates/waitlist/components/ui/button.tsx +56 -0
  313. package/templates/waitlist/components/ui/card.tsx +90 -0
  314. package/templates/waitlist/components.json +20 -0
  315. package/templates/waitlist/functions/joinWaitlist.ts +82 -0
  316. package/templates/waitlist/functions/waitlistStats.ts +75 -0
  317. package/templates/waitlist/gitignore +10 -0
  318. package/templates/waitlist/lib/owner.ts +26 -0
  319. package/templates/waitlist/lib/site.config.ts +178 -0
  320. package/templates/waitlist/lib/stats.ts +30 -0
  321. package/templates/waitlist/lib/utils.ts +10 -0
  322. package/templates/waitlist/package.json +34 -0
  323. package/templates/waitlist/tsconfig.json +18 -0
@@ -0,0 +1,414 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useRef, useState } from "react";
4
+ import { db } from "@pylonsync/react";
5
+ import { siteConfig } from "@/lib/site.config";
6
+
7
+ // The chat app — a client island, rendered only for a SIGNED-IN user (the page
8
+ // redirects anyone else to /login). Conversations + messages are sync-backed
9
+ // owner-scoped entities (`db.useQuery`), so your history is private to your
10
+ // account and stays in lockstep across your tabs + devices. Sending streams
11
+ // tokens from the built-in `POST /api/ai/stream` (SSE) — your PYLON_AI_API_KEY
12
+ // never reaches the browser.
13
+ //
14
+ // All state lives in <ChatInner> (which owns `currentId`) — the thread is a
15
+ // presentational child. That's deliberate: creating a conversation mid-send
16
+ // changes `currentId`, and if the thread remounted on that change it would kill
17
+ // the in-flight stream. One owner, no remounts.
18
+
19
+ interface ConversationRow {
20
+ id: string;
21
+ userId: string;
22
+ title: string;
23
+ createdAt: string;
24
+ }
25
+ interface MessageRow {
26
+ id: string;
27
+ conversationId: string;
28
+ userId: string;
29
+ role: string;
30
+ content: string;
31
+ createdAt: string;
32
+ }
33
+
34
+ export function ChatApp() {
35
+ return <ChatInner />;
36
+ }
37
+
38
+ function ChatInner() {
39
+ const { chat } = siteConfig;
40
+ const { data: conversations } = db.useQuery<ConversationRow>("Conversation", {
41
+ orderBy: { createdAt: "desc" },
42
+ });
43
+ const [currentId, setCurrentId] = useState<string | null>(null);
44
+ const { data: messages } = db.useQuery<MessageRow>("Message", {
45
+ where: { conversationId: currentId ?? "__none__" },
46
+ orderBy: { createdAt: "asc" },
47
+ });
48
+
49
+ const [streaming, setStreaming] = useState<string | null>(null);
50
+ const [sending, setSending] = useState(false);
51
+ const [notice, setNotice] = useState<string | null>(null);
52
+ const [input, setInput] = useState("");
53
+ const [model, setModel] = useState(chat.defaultModel);
54
+ const scrollRef = useRef<HTMLDivElement>(null);
55
+ const initialized = useRef(false);
56
+
57
+ // On first load, drop into the most recent conversation (if any).
58
+ useEffect(() => {
59
+ if (!initialized.current && conversations.length > 0) {
60
+ initialized.current = true;
61
+ setCurrentId(conversations[0].id);
62
+ }
63
+ }, [conversations]);
64
+
65
+ // Keep the latest turn in view as content streams in.
66
+ useEffect(() => {
67
+ scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });
68
+ }, [messages.length, streaming]);
69
+
70
+ function selectConversation(id: string) {
71
+ setCurrentId(id);
72
+ setStreaming(null);
73
+ setNotice(null);
74
+ }
75
+ function newChat() {
76
+ setCurrentId(null);
77
+ setStreaming(null);
78
+ setNotice(null);
79
+ }
80
+
81
+ async function send(text: string) {
82
+ const trimmed = text.trim();
83
+ if (!trimmed || sending) return;
84
+ setSending(true);
85
+ setNotice(null);
86
+ setInput("");
87
+
88
+ // Snapshot the history BEFORE the async work (messages is for the current
89
+ // conversation; empty for a brand-new chat).
90
+ const history = messages.map((m) => ({ role: m.role, content: m.content }));
91
+
92
+ // Make sure we have a conversation to attach to.
93
+ let convId = currentId;
94
+ if (!convId) {
95
+ convId = await db.insert("Conversation", { title: trimmed.slice(0, 48) });
96
+ setCurrentId(convId);
97
+ }
98
+
99
+ // Persist the user's turn (optimistic — paints immediately).
100
+ await db.insert("Message", { conversationId: convId, role: "user", content: trimmed });
101
+
102
+ const payload = [
103
+ { role: "system", content: chat.systemPrompt },
104
+ ...history,
105
+ { role: "user", content: trimmed },
106
+ ];
107
+
108
+ let acc = "";
109
+ try {
110
+ await streamCompletion(payload, model, (delta) => {
111
+ acc += delta;
112
+ setStreaming(acc);
113
+ });
114
+ if (acc.trim()) {
115
+ await db.insert("Message", { conversationId: convId, role: "assistant", content: acc });
116
+ }
117
+ } catch (err) {
118
+ const code = (err as { code?: string })?.code;
119
+ if (code === "AI_NOT_CONFIGURED") {
120
+ setNotice(
121
+ "AI isn't configured yet. Set PYLON_AI_PROVIDER and PYLON_AI_API_KEY in .env, then restart — see the README.",
122
+ );
123
+ } else if (code === "MODEL_OVERRIDE_FORBIDDEN" || code === "MODEL_NOT_ALLOWED") {
124
+ setNotice(
125
+ "That model isn't enabled. Add it to PYLON_AI_MODELS_ALLOWED in .env (comma-separated), then restart.",
126
+ );
127
+ } else if (code === "RATE_LIMITED") {
128
+ setNotice("You've hit the AI rate limit — try again in a little while.");
129
+ } else {
130
+ setNotice("Something went wrong reaching the model. Try again.");
131
+ }
132
+ } finally {
133
+ setStreaming(null);
134
+ setSending(false);
135
+ }
136
+ }
137
+
138
+ const empty = messages.length === 0 && streaming === null;
139
+
140
+ return (
141
+ <div className="flex h-[calc(100vh-3.5rem)]">
142
+ <Sidebar
143
+ conversations={conversations}
144
+ currentId={currentId}
145
+ onSelect={selectConversation}
146
+ onNew={newChat}
147
+ />
148
+ <div className="flex flex-1 flex-col bg-white">
149
+ <div ref={scrollRef} className="flex-1 overflow-y-auto">
150
+ <div className="mx-auto max-w-3xl px-4 py-6">
151
+ {empty ? (
152
+ <EmptyState onPick={send} />
153
+ ) : (
154
+ <div className="space-y-5">
155
+ {messages.map((m) => (
156
+ <Bubble key={m.id} role={m.role} content={m.content} />
157
+ ))}
158
+ {streaming !== null ? <Bubble role="assistant" content={streaming || "…"} streaming /> : null}
159
+ </div>
160
+ )}
161
+ {notice ? (
162
+ <div className="mt-5 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-[13px] leading-relaxed text-amber-800">
163
+ {notice}
164
+ </div>
165
+ ) : null}
166
+ </div>
167
+ </div>
168
+
169
+ <Composer
170
+ value={input}
171
+ onChange={setInput}
172
+ onSend={() => send(input)}
173
+ disabled={sending}
174
+ placeholder={chat.inputPlaceholder}
175
+ model={model}
176
+ onModelChange={setModel}
177
+ />
178
+ </div>
179
+ </div>
180
+ );
181
+ }
182
+
183
+ function Sidebar({
184
+ conversations,
185
+ currentId,
186
+ onSelect,
187
+ onNew,
188
+ }: {
189
+ conversations: ConversationRow[];
190
+ currentId: string | null;
191
+ onSelect: (id: string) => void;
192
+ onNew: () => void;
193
+ }) {
194
+ return (
195
+ <aside className="hidden w-64 shrink-0 flex-col border-r border-zinc-200 bg-paper sm:flex">
196
+ <div className="p-3">
197
+ <button
198
+ type="button"
199
+ onClick={onNew}
200
+ className="flex w-full items-center justify-center gap-2 rounded-lg bg-brand px-3 py-2 text-[13.5px] font-medium text-white transition-opacity hover:opacity-90"
201
+ >
202
+ <PlusIcon /> New chat
203
+ </button>
204
+ </div>
205
+ <nav className="flex-1 overflow-y-auto px-2 pb-3">
206
+ {conversations.length === 0 ? (
207
+ <p className="px-2 py-4 text-[12.5px] text-zinc-400">No conversations yet.</p>
208
+ ) : (
209
+ <ul className="space-y-0.5">
210
+ {conversations.map((c) => (
211
+ <li key={c.id}>
212
+ <button
213
+ type="button"
214
+ onClick={() => onSelect(c.id)}
215
+ className={
216
+ "w-full truncate rounded-lg px-2.5 py-2 text-left text-[13.5px] transition-colors " +
217
+ (c.id === currentId ? "bg-brand-soft font-medium text-brand" : "text-zinc-600 hover:bg-zinc-100")
218
+ }
219
+ title={c.title}
220
+ >
221
+ {c.title || "New chat"}
222
+ </button>
223
+ </li>
224
+ ))}
225
+ </ul>
226
+ )}
227
+ </nav>
228
+ </aside>
229
+ );
230
+ }
231
+
232
+ function EmptyState({ onPick }: { onPick: (text: string) => void }) {
233
+ const { chat, brand } = siteConfig;
234
+ return (
235
+ <div className="flex flex-col items-center pt-[12vh] text-center">
236
+ <span className="flex size-12 items-center justify-center rounded-2xl bg-brand text-xl font-bold text-white">
237
+ {brand.letter}
238
+ </span>
239
+ <h1 className="mt-5 text-2xl font-semibold tracking-tight text-zinc-900">{chat.emptyHeadline}</h1>
240
+ <p className="mt-2 max-w-md text-[15px] leading-relaxed text-zinc-500">{chat.emptySubcopy}</p>
241
+ <div className="mt-7 grid w-full max-w-xl gap-2 sm:grid-cols-2">
242
+ {chat.suggestions.map((s) => (
243
+ <button
244
+ key={s}
245
+ type="button"
246
+ onClick={() => onPick(s)}
247
+ className="rounded-xl border border-zinc-200 bg-white px-4 py-3 text-left text-[13.5px] text-zinc-600 transition-colors hover:border-brand hover:text-zinc-900"
248
+ >
249
+ {s}
250
+ </button>
251
+ ))}
252
+ </div>
253
+ </div>
254
+ );
255
+ }
256
+
257
+ function Bubble({ role, content, streaming }: { role: string; content: string; streaming?: boolean }) {
258
+ const { brand } = siteConfig;
259
+ const isUser = role === "user";
260
+ return (
261
+ <div className={"flex gap-3 " + (isUser ? "flex-row-reverse" : "")}>
262
+ <span
263
+ className={
264
+ "mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full text-[11px] font-semibold " +
265
+ (isUser ? "bg-zinc-200 text-zinc-600" : "bg-brand text-white")
266
+ }
267
+ >
268
+ {isUser ? "You" : brand.letter}
269
+ </span>
270
+ <div
271
+ className={
272
+ "max-w-[80%] whitespace-pre-wrap rounded-2xl px-4 py-2.5 text-[14.5px] leading-relaxed " +
273
+ (isUser ? "bg-zinc-900 text-white" : "bg-zinc-100 text-zinc-800")
274
+ }
275
+ >
276
+ {content}
277
+ {streaming ? <span className="ml-0.5 inline-block h-4 w-1.5 animate-pulse bg-zinc-400 align-middle" /> : null}
278
+ </div>
279
+ </div>
280
+ );
281
+ }
282
+
283
+ function Composer({
284
+ value,
285
+ onChange,
286
+ onSend,
287
+ disabled,
288
+ placeholder,
289
+ model,
290
+ onModelChange,
291
+ }: {
292
+ value: string;
293
+ onChange: (v: string) => void;
294
+ onSend: () => void;
295
+ disabled: boolean;
296
+ placeholder: string;
297
+ model: string;
298
+ onModelChange: (m: string) => void;
299
+ }) {
300
+ const { models } = siteConfig.chat;
301
+ function onKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
302
+ if (e.key === "Enter" && !e.shiftKey) {
303
+ e.preventDefault();
304
+ onSend();
305
+ }
306
+ }
307
+ return (
308
+ <div className="border-t border-zinc-200 bg-white px-4 py-3">
309
+ <div className="mx-auto flex max-w-3xl items-end gap-2">
310
+ <textarea
311
+ value={value}
312
+ onChange={(e) => onChange(e.target.value)}
313
+ onKeyDown={onKeyDown}
314
+ rows={1}
315
+ placeholder={placeholder}
316
+ aria-label="Message"
317
+ className="max-h-40 flex-1 resize-none rounded-2xl border border-zinc-300 bg-white px-4 py-2.5 text-[14.5px] text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
318
+ />
319
+ <button
320
+ type="button"
321
+ onClick={onSend}
322
+ disabled={disabled || !value.trim()}
323
+ aria-label="Send"
324
+ className="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-brand text-white transition-opacity hover:opacity-90 disabled:opacity-40"
325
+ >
326
+ <SendIcon />
327
+ </button>
328
+ </div>
329
+ <div className="mx-auto mt-1.5 flex max-w-3xl items-center justify-between gap-3">
330
+ <label className="flex items-center gap-1.5 text-[11px] text-zinc-400">
331
+ <span className="hidden sm:inline">Model</span>
332
+ <select
333
+ value={model}
334
+ onChange={(e) => onModelChange(e.target.value)}
335
+ aria-label="Model"
336
+ className="rounded-md border border-zinc-200 bg-white px-1.5 py-1 text-[11.5px] text-zinc-600 outline-none focus:border-brand"
337
+ >
338
+ {models.map((m) => (
339
+ <option key={m.id} value={m.id}>
340
+ {m.label} · {m.provider}
341
+ </option>
342
+ ))}
343
+ </select>
344
+ </label>
345
+ <span className="text-[11px] text-zinc-400">Enter to send · Shift+Enter for a new line</span>
346
+ </div>
347
+ </div>
348
+ );
349
+ }
350
+
351
+ // Parse the OpenAI-style SSE stream from POST /api/ai/stream:
352
+ // data: {"choices":[{"delta":{"content":"…"}}]} … data: [DONE]
353
+ // Throws { code } on the 503 (AI not configured) / 429 (rate limited) shims.
354
+ async function streamCompletion(
355
+ messages: { role: string; content: string }[],
356
+ model: string,
357
+ onDelta: (delta: string) => void,
358
+ ): Promise<void> {
359
+ const res = await fetch("/api/ai/stream", {
360
+ method: "POST",
361
+ headers: { "content-type": "application/json" },
362
+ body: JSON.stringify({ messages, model }),
363
+ });
364
+ if (!res.ok || !res.body) {
365
+ let code = `HTTP_${res.status}`;
366
+ try {
367
+ code = (await res.json())?.error?.code ?? code;
368
+ } catch {
369
+ /* ignore */
370
+ }
371
+ throw { code };
372
+ }
373
+ const reader = res.body.getReader();
374
+ const decoder = new TextDecoder();
375
+ let buf = "";
376
+ for (;;) {
377
+ const { done, value } = await reader.read();
378
+ if (done) break;
379
+ buf += decoder.decode(value, { stream: true });
380
+ const lines = buf.split("\n");
381
+ buf = lines.pop() ?? "";
382
+ for (const raw of lines) {
383
+ const line = raw.trim();
384
+ if (!line.startsWith("data:")) continue;
385
+ const data = line.slice(5).trim();
386
+ if (data === "[DONE]") return;
387
+ try {
388
+ const j = JSON.parse(data);
389
+ if (j.error) throw { code: j.error.code ?? "STREAM_ERROR" };
390
+ const delta = j.choices?.[0]?.delta?.content;
391
+ if (delta) onDelta(delta);
392
+ } catch (e) {
393
+ if ((e as { code?: string })?.code) throw e;
394
+ /* ignore keep-alive / partial lines */
395
+ }
396
+ }
397
+ }
398
+ }
399
+
400
+ function PlusIcon() {
401
+ return (
402
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" aria-hidden>
403
+ <path d="M12 5v14M5 12h14" />
404
+ </svg>
405
+ );
406
+ }
407
+ function SendIcon() {
408
+ return (
409
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
410
+ <path d="M22 2 11 13M22 2l-7 20-4-9-9-4 20-7z" />
411
+ </svg>
412
+ );
413
+ }
414
+
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { type ErrorBoundaryProps } from "@pylonsync/react";
3
+
4
+ // `app/error.tsx` → the error boundary for this segment. Hydrated + interactive:
5
+ // `reset()` re-attempts the route. The thrown error reaches the client as
6
+ // `{ message, digest }` only — the stack stays in the dev overlay / server logs.
7
+ export default function Error({ error, reset }: ErrorBoundaryProps) {
8
+ return (
9
+ <div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
10
+ <h1 className="text-2xl font-semibold tracking-tight">Something went wrong</h1>
11
+ <p className="mt-2 text-zinc-500">{error.message}</p>
12
+ {error.digest ? (
13
+ <p className="mt-1 text-xs text-zinc-400">
14
+ Reference: <code>{error.digest}</code>
15
+ </p>
16
+ ) : null}
17
+ <button
18
+ type="button"
19
+ onClick={reset}
20
+ className="mt-6 inline-flex h-10 items-center rounded-full bg-zinc-900 px-5 text-sm font-medium text-white transition-colors hover:bg-zinc-700"
21
+ >
22
+ Try again
23
+ </button>
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,148 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ /* Tailwind v4 scans these globs for class names. Add more @source lines if you
5
+ put markup elsewhere. The @pylonsync/client line lets its components
6
+ (EnsureGuest, auth helpers) keep any classes they ship. */
7
+ @source "../app/**/*.{tsx,ts,jsx,js}";
8
+ @source "../components/**/*.{tsx,ts,jsx,js}";
9
+ @source "../lib/**/*.{tsx,ts,jsx,js}";
10
+ @source "../node_modules/@pylonsync/client/**/*.{tsx,ts,jsx,js}";
11
+
12
+ @custom-variant dark (&:where(.dark, .dark *));
13
+
14
+ /* shadcn/ui design tokens (new-york / zinc) + the marketing brand accent. The
15
+ three brand vars are defaults — app/layout.tsx overrides them from
16
+ lib/site.config.ts on <html>, so re-theming the whole page is one edit there. */
17
+ :root {
18
+ --radius: 0.625rem;
19
+ --brand: #4f46e5;
20
+ --brand-soft: #eef2ff;
21
+ --paper: #fafafa;
22
+ --background: oklch(1 0 0);
23
+ --foreground: oklch(0.141 0.005 285.823);
24
+ --card: oklch(1 0 0);
25
+ --card-foreground: oklch(0.141 0.005 285.823);
26
+ --popover: oklch(1 0 0);
27
+ --popover-foreground: oklch(0.141 0.005 285.823);
28
+ --primary: oklch(0.21 0.006 285.885);
29
+ --primary-foreground: oklch(0.985 0 0);
30
+ --secondary: oklch(0.967 0.001 286.375);
31
+ --secondary-foreground: oklch(0.21 0.006 285.885);
32
+ --muted: oklch(0.967 0.001 286.375);
33
+ --muted-foreground: oklch(0.552 0.016 285.938);
34
+ --accent: oklch(0.967 0.001 286.375);
35
+ --accent-foreground: oklch(0.21 0.006 285.885);
36
+ --destructive: oklch(0.577 0.245 27.325);
37
+ --border: oklch(0.92 0.004 286.32);
38
+ --input: oklch(0.92 0.004 286.32);
39
+ --ring: oklch(0.705 0.015 286.067);
40
+ --chart-1: oklch(0.646 0.222 41.116);
41
+ --chart-2: oklch(0.6 0.118 184.704);
42
+ --chart-3: oklch(0.398 0.07 227.392);
43
+ --chart-4: oklch(0.828 0.189 84.429);
44
+ --chart-5: oklch(0.769 0.188 70.08);
45
+ --sidebar: oklch(0.985 0 0);
46
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
47
+ --sidebar-primary: oklch(0.21 0.006 285.885);
48
+ --sidebar-primary-foreground: oklch(0.985 0 0);
49
+ --sidebar-accent: oklch(0.967 0.001 286.375);
50
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
51
+ --sidebar-border: oklch(0.92 0.004 286.32);
52
+ --sidebar-ring: oklch(0.705 0.015 286.067);
53
+ }
54
+
55
+ .dark {
56
+ --background: oklch(0.141 0.005 285.823);
57
+ --foreground: oklch(0.985 0 0);
58
+ --card: oklch(0.21 0.006 285.885);
59
+ --card-foreground: oklch(0.985 0 0);
60
+ --popover: oklch(0.21 0.006 285.885);
61
+ --popover-foreground: oklch(0.985 0 0);
62
+ --primary: oklch(0.92 0.004 286.32);
63
+ --primary-foreground: oklch(0.21 0.006 285.885);
64
+ --secondary: oklch(0.274 0.006 286.033);
65
+ --secondary-foreground: oklch(0.985 0 0);
66
+ --muted: oklch(0.274 0.006 286.033);
67
+ --muted-foreground: oklch(0.705 0.015 286.067);
68
+ --accent: oklch(0.274 0.006 286.033);
69
+ --accent-foreground: oklch(0.985 0 0);
70
+ --destructive: oklch(0.704 0.191 22.216);
71
+ --border: oklch(1 0 0 / 10%);
72
+ --input: oklch(1 0 0 / 15%);
73
+ --ring: oklch(0.552 0.016 285.938);
74
+ --chart-1: oklch(0.488 0.243 264.376);
75
+ --chart-2: oklch(0.696 0.17 162.48);
76
+ --chart-3: oklch(0.769 0.188 70.08);
77
+ --chart-4: oklch(0.627 0.265 303.9);
78
+ --chart-5: oklch(0.645 0.246 16.439);
79
+ --sidebar: oklch(0.21 0.006 285.885);
80
+ --sidebar-foreground: oklch(0.985 0 0);
81
+ --sidebar-primary: oklch(0.488 0.243 264.376);
82
+ --sidebar-primary-foreground: oklch(0.985 0 0);
83
+ --sidebar-accent: oklch(0.274 0.006 286.033);
84
+ --sidebar-accent-foreground: oklch(0.985 0 0);
85
+ --sidebar-border: oklch(1 0 0 / 10%);
86
+ --sidebar-ring: oklch(0.552 0.016 285.938);
87
+ }
88
+
89
+ @theme inline {
90
+ --radius-sm: calc(var(--radius) - 4px);
91
+ --radius-md: calc(var(--radius) - 2px);
92
+ --radius-lg: var(--radius);
93
+ --radius-xl: calc(var(--radius) + 4px);
94
+ --color-background: var(--background);
95
+ --color-foreground: var(--foreground);
96
+ --color-card: var(--card);
97
+ --color-card-foreground: var(--card-foreground);
98
+ --color-popover: var(--popover);
99
+ --color-popover-foreground: var(--popover-foreground);
100
+ --color-primary: var(--primary);
101
+ --color-primary-foreground: var(--primary-foreground);
102
+ --color-secondary: var(--secondary);
103
+ --color-secondary-foreground: var(--secondary-foreground);
104
+ --color-muted: var(--muted);
105
+ --color-muted-foreground: var(--muted-foreground);
106
+ --color-accent: var(--accent);
107
+ --color-accent-foreground: var(--accent-foreground);
108
+ --color-destructive: var(--destructive);
109
+ --color-border: var(--border);
110
+ --color-input: var(--input);
111
+ --color-ring: var(--ring);
112
+ --color-brand: var(--brand);
113
+ --color-brand-soft: var(--brand-soft);
114
+ --color-paper: var(--paper);
115
+ --color-chart-1: var(--chart-1);
116
+ --color-chart-2: var(--chart-2);
117
+ --color-chart-3: var(--chart-3);
118
+ --color-chart-4: var(--chart-4);
119
+ --color-chart-5: var(--chart-5);
120
+ --color-sidebar: var(--sidebar);
121
+ --color-sidebar-foreground: var(--sidebar-foreground);
122
+ --color-sidebar-primary: var(--sidebar-primary);
123
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
124
+ --color-sidebar-accent: var(--sidebar-accent);
125
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
126
+ --color-sidebar-border: var(--sidebar-border);
127
+ --color-sidebar-ring: var(--sidebar-ring);
128
+ }
129
+
130
+ @layer base {
131
+ *,
132
+ ::after,
133
+ ::before,
134
+ ::backdrop,
135
+ ::file-selector-button {
136
+ border-color: var(--color-border, currentColor);
137
+ outline-color: var(--color-ring);
138
+ }
139
+ body {
140
+ background-color: var(--color-background);
141
+ color: var(--color-foreground);
142
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
143
+ -webkit-font-smoothing: antialiased;
144
+ }
145
+ button {
146
+ cursor: pointer;
147
+ }
148
+ }
@@ -0,0 +1,75 @@
1
+ import React from "react";
2
+ import { Link, type PageAuth } from "@pylonsync/react";
3
+ import { siteConfig } from "@/lib/site.config";
4
+
5
+ // App shell: a slim top bar over a full-height chat. `auth.user_id` is resolved
6
+ // server-side from the session cookie before any HTML is sent, so the bar shows
7
+ // the account / "Sign in" with no flash. The chat page fills the rest of the
8
+ // viewport (h-[calc(100vh-3.5rem)] in chat-client.tsx — keep the header at h-14).
9
+ interface LayoutProps {
10
+ children: React.ReactNode;
11
+ url: string;
12
+ auth: PageAuth;
13
+ }
14
+
15
+ export default function RootLayout({ children, url, auth }: LayoutProps) {
16
+ // Resolved server-side from the session cookie. The app requires sign-in, so
17
+ // this is set on every in-app page; the header reflects it with no flash.
18
+ const signedIn = Boolean(auth?.user_id);
19
+ const { brand, colors } = siteConfig;
20
+
21
+ // The auth screen brings its own chrome → render it bare.
22
+ const path = (url ?? "").split("?")[0];
23
+ const isBare = path === "/login" || path.startsWith("/login/");
24
+
25
+ return (
26
+ <html
27
+ lang="en"
28
+ style={
29
+ {
30
+ "--brand": colors.brand,
31
+ "--brand-soft": colors.brandSoft,
32
+ "--paper": colors.paper,
33
+ } as React.CSSProperties
34
+ }
35
+ >
36
+ <head>
37
+ <meta charSet="utf-8" />
38
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
39
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
40
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
41
+ <link
42
+ rel="stylesheet"
43
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
44
+ />
45
+ </head>
46
+ <body className="bg-background text-foreground antialiased">
47
+ {isBare ? (
48
+ children
49
+ ) : (
50
+ <>
51
+ <header className="flex h-14 items-center justify-between border-b border-zinc-200 bg-white px-4">
52
+ <Link href="/" className="flex items-center gap-2">
53
+ <span className="flex size-6 items-center justify-center rounded-[7px] bg-brand text-[13px] font-bold text-white">
54
+ {brand.letter}
55
+ </span>
56
+ <span className="text-[15px] font-semibold tracking-tight text-zinc-900">{brand.name}</span>
57
+ </Link>
58
+ {signedIn ? (
59
+ <span className="text-[13px] text-zinc-400">Signed in</span>
60
+ ) : (
61
+ <Link
62
+ href="/login"
63
+ className="rounded-full border border-zinc-300 px-3.5 py-1.5 text-[13px] font-medium text-zinc-700 transition-colors hover:bg-zinc-50"
64
+ >
65
+ Sign in
66
+ </Link>
67
+ )}
68
+ </header>
69
+ {children}
70
+ </>
71
+ )}
72
+ </body>
73
+ </html>
74
+ );
75
+ }