@drawnagency/primitives 0.1.67 → 0.1.69

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 (44) hide show
  1. package/dist/chunk-4YEOTFVU.js +14 -0
  2. package/dist/{chunk-VU3XR362.js → chunk-6UDYJQL6.js} +1 -1
  3. package/dist/{chunk-7IMQVDDJ.js → chunk-EWKCEPMW.js} +1 -1
  4. package/dist/{chunk-XNGG5AGI.js → chunk-KWBM27UR.js} +1 -1
  5. package/dist/{chunk-TEYTLQVH.js → chunk-QKOIEWZ4.js} +33 -1
  6. package/dist/{chunk-4VMAZZXT.js → chunk-TYYCKVAW.js} +4 -4
  7. package/dist/{chunk-KM6HOW4H.js → chunk-VVK6ZRCN.js} +2 -2
  8. package/dist/{chunk-AFK45HX2.js → chunk-WNPMBFD6.js} +1 -1
  9. package/dist/{chunk-WJ7RZ6CC.js → chunk-WXVKX6PY.js} +1 -1
  10. package/dist/components/chat/ChatPanel.d.ts +33 -0
  11. package/dist/components/chat/ChatPanel.d.ts.map +1 -0
  12. package/dist/components/chat/ChatWidget.d.ts +14 -0
  13. package/dist/components/chat/ChatWidget.d.ts.map +1 -0
  14. package/dist/components/primitives/index.js +5 -5
  15. package/dist/components/sections/brand-guide.js +5 -5
  16. package/dist/components/sections/register-schemas.js +7 -7
  17. package/dist/components/sections/register.js +7 -7
  18. package/dist/components/shell/ChatbotConfigModal.d.ts +13 -0
  19. package/dist/components/shell/ChatbotConfigModal.d.ts.map +1 -0
  20. package/dist/components/shell/EditorShell.d.ts +2 -1
  21. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  22. package/dist/index.js +12 -5
  23. package/dist/lib/index.d.ts +1 -0
  24. package/dist/lib/index.d.ts.map +1 -1
  25. package/dist/lib/index.js +6 -3
  26. package/dist/lib/platform-broker.d.ts +7 -0
  27. package/dist/lib/platform-broker.d.ts.map +1 -1
  28. package/dist/lib/platform-broker.js +27 -1
  29. package/dist/media/index.js +3 -3
  30. package/dist/schemas/index.js +6 -2
  31. package/dist/schemas/site-config.d.ts +30 -0
  32. package/dist/schemas/site-config.d.ts.map +1 -1
  33. package/dist/types/database.d.ts +48 -0
  34. package/dist/types/database.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/components/chat/ChatPanel.tsx +158 -0
  37. package/src/components/chat/ChatWidget.tsx +61 -0
  38. package/src/components/shell/ChatbotConfigModal.tsx +152 -0
  39. package/src/components/shell/EditorShell.tsx +43 -1
  40. package/src/lib/index.ts +1 -0
  41. package/src/lib/platform-broker.ts +36 -0
  42. package/src/schemas/site-config.ts +36 -0
  43. package/src/types/database.ts +48 -0
  44. package/dist/chunk-ZGFAYZWB.js +0 -8
@@ -19,6 +19,8 @@ import type { BlockMove } from "../../lib/block-move";
19
19
  import { EditorModalProvider, useEditorModal } from "./EditorModalContext";
20
20
  import { EditorModal } from "./EditorModal";
21
21
  import { SiteSettingsModal } from "./SiteSettingsModal";
22
+ import { ChatbotConfigModal } from "./ChatbotConfigModal";
23
+ import ChatWidget from "../chat/ChatWidget";
22
24
  import { MediaLibraryModal } from "./MediaLibraryModal";
23
25
  import { MediaLibraryContext } from "./MediaLibraryContext";
24
26
  import { ProcessingIndicator } from "./ProcessingIndicator";
@@ -72,7 +74,7 @@ import { IconButton } from "../shared/IconButton";
72
74
  import { SegmentedControl } from "../shared/SegmentedControl";
73
75
  import { computeDisplaySections } from "./view-branch";
74
76
  import { SettingsIcon } from "../shared/icons";
75
- import { ImageIcon, ListOrderedIcon, FilesIcon, X } from "lucide-react";
77
+ import { ImageIcon, ListOrderedIcon, FilesIcon, MessageCircleIcon, X } from "lucide-react";
76
78
  import { ErrorBoundary } from "../shared/ErrorBoundary";
77
79
  import { HistoryToolbar } from "./HistoryToolbar";
78
80
  import { RestoreModal } from "./RestoreModal";
@@ -104,6 +106,10 @@ interface Props {
104
106
  passwordToggle: boolean;
105
107
  };
106
108
  currentUser: { email: string; role: "owner" | "editor" } | null;
109
+ // Whether the platform (admin) has enabled the chatbot for this site.
110
+ // Controls the configurator's banner + test panel; the config UI itself
111
+ // always works so editors can prepare before activation.
112
+ chatbotPlatformEnabled?: boolean;
107
113
  }
108
114
 
109
115
  export default function EditorShell({
@@ -113,6 +119,7 @@ export default function EditorShell({
113
119
  audiences: initialAudiences,
114
120
  capabilities,
115
121
  currentUser,
122
+ chatbotPlatformEnabled = false,
116
123
  }: Props) {
117
124
  const [shellState, setShellState] = useState<ShellState>({ phase: "loading-content" });
118
125
  const [sections, setSections] = useState<LoadedSection[]>([]);
@@ -143,6 +150,7 @@ export default function EditorShell({
143
150
  const [showRestoreModal, setShowRestoreModal] = useState(false);
144
151
  const [showOrderingModal, setShowOrderingModal] = useState(false);
145
152
  const [showPagesModal, setShowPagesModal] = useState(false);
153
+ const [showChatbotConfig, setShowChatbotConfig] = useState(false);
146
154
  const [isRestoring, setIsRestoring] = useState(false);
147
155
  const [rejectedUploads, setRejectedUploads] = useState<{ name: string; reason: "size" | "type" }[]>([]);
148
156
  const [activePageId, setActivePageId] = useState<string | null>(null);
@@ -887,6 +895,7 @@ export default function EditorShell({
887
895
  onRestoreClick={() => setShowRestoreModal(true)}
888
896
  onOrderingClick={() => setShowOrderingModal(true)}
889
897
  onPagesClick={() => setShowPagesModal(true)}
898
+ onChatbotClick={() => setShowChatbotConfig(true)}
890
899
  />
891
900
 
892
901
  {/* Bug reports persist via Supabase. On the password-only (zero-Supabase)
@@ -894,6 +903,20 @@ export default function EditorShell({
894
903
  the button would always 401 — hide it there. */}
895
904
  {!capabilities.passwordOnly && <BugReportFAB />}
896
905
 
906
+ {/* Editor chat FAB: chat while editing, against the DRAFT chatbot
907
+ config (tasks + prompt reflect unsaved changes; grounding is
908
+ published content). Gated only on the platform switch — editors
909
+ may chat before the widget is enabled for viewers. */}
910
+ {chatbotPlatformEnabled && siteConfig && (
911
+ <ChatWidget
912
+ siteName={siteConfig.siteName}
913
+ tasks={siteConfig.chatbot.tasks.map((t) => ({ id: t.id, label: t.label }))}
914
+ isEditor
915
+ draftChatbot={siteConfig.chatbot}
916
+ variant="editor"
917
+ />
918
+ )}
919
+
897
920
  {rejectedUploads.length > 0 && (
898
921
  <div className="sticky top-2 z-30 mx-auto w-full max-w-3xl px-4">
899
922
  <UploadRejectionAlert
@@ -937,6 +960,16 @@ export default function EditorShell({
937
960
  capabilities={capabilities}
938
961
  currentUser={currentUser}
939
962
  />
963
+ {siteConfig && (
964
+ <ChatbotConfigModal
965
+ isOpen={showChatbotConfig}
966
+ onClose={() => setShowChatbotConfig(false)}
967
+ siteConfig={siteConfig}
968
+ onSiteConfigChange={handleSiteConfigChange}
969
+ audiences={audiences}
970
+ platformEnabled={chatbotPlatformEnabled}
971
+ />
972
+ )}
940
973
  <EditorModal
941
974
  isOpen={pendingDeleteSectionId !== null}
942
975
  onClose={() => setPendingDeleteSectionId(null)}
@@ -1522,6 +1555,7 @@ function EditorToolbar({
1522
1555
  onRestoreClick,
1523
1556
  onOrderingClick,
1524
1557
  onPagesClick,
1558
+ onChatbotClick,
1525
1559
  }: {
1526
1560
  buttonState: "synced" | "publish" | "saveAndPublish";
1527
1561
  localChangesExist: boolean;
@@ -1540,6 +1574,7 @@ function EditorToolbar({
1540
1574
  onRestoreClick: () => void;
1541
1575
  onOrderingClick: () => void;
1542
1576
  onPagesClick: () => void;
1577
+ onChatbotClick: () => void;
1543
1578
  }) {
1544
1579
  const { isEditMode, viewBranch, setViewBranch, toggleEditMode, historyState, setHistoryState } = useEditorContext();
1545
1580
 
@@ -1625,6 +1660,13 @@ function EditorToolbar({
1625
1660
  onClick={onPagesClick}
1626
1661
  className="border border-base-200 bg-base-accent"
1627
1662
  />
1663
+ <IconButton
1664
+ icon={<MessageCircleIcon size={16} />}
1665
+ label="Chatbot"
1666
+ size="md"
1667
+ onClick={onChatbotClick}
1668
+ className="border border-base-200 bg-base-accent"
1669
+ />
1628
1670
  <IconButton
1629
1671
  icon={<SettingsIcon size={16} />}
1630
1672
  label="Site settings"
package/src/lib/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { env } from "./env";
2
2
  export { cn } from "./cn";
3
+ export { audiencePasses } from "./pages";
3
4
  export { generateNavLinks, toSectionId, type NavItem } from "./nav";
4
5
  export { deriveContrast } from "./contrast";
5
6
  export { sanitizeHtml, ensureSanitizer } from "./sanitize";
@@ -70,3 +70,39 @@ export async function platformBroker<T = unknown>(
70
70
 
71
71
  throw lastError instanceof Error ? lastError : new Error("Platform broker unreachable");
72
72
  }
73
+
74
+ /**
75
+ * Streaming variant of platformBroker for SSE endpoints (the LLM broker).
76
+ * Same auth headers; retries only pre-stream connection failures — once bytes
77
+ * are flowing there is no safe retry. Non-2xx throws with the status ONLY
78
+ * (never the body: stream endpoints may proxy provider errors).
79
+ */
80
+ export async function platformBrokerStream(
81
+ path: string,
82
+ body: unknown,
83
+ ): Promise<ReadableStream<Uint8Array>> {
84
+ const apiUrl = env("PLATFORM_API_URL");
85
+ const apiKey = env("PLATFORM_API_KEY");
86
+ const siteId = env("PORTAL_SITE_ID");
87
+ const url = `${apiUrl}${path}`;
88
+
89
+ let lastError: unknown;
90
+ for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
91
+ let res: Response;
92
+ try {
93
+ res = await fetch(url, {
94
+ method: "POST",
95
+ headers: { "x-api-key": apiKey, "x-site-id": siteId, "Content-Type": "application/json" },
96
+ body: JSON.stringify(body),
97
+ });
98
+ } catch (err) {
99
+ lastError = err;
100
+ if (attempt === ATTEMPTS - 1) throw err;
101
+ await sleep(RETRY_BASE_MS * 2 ** attempt);
102
+ continue;
103
+ }
104
+ if (!res.ok || !res.body) throw new Error(`Platform broker error (${res.status})`);
105
+ return res.body;
106
+ }
107
+ throw lastError instanceof Error ? lastError : new Error("Platform broker unreachable");
108
+ }
@@ -132,6 +132,38 @@ export function normalizeSiteIndex(raw: unknown): SiteIndex {
132
132
  return IndexSchema.parse(raw);
133
133
  }
134
134
 
135
+ // Task ids share the audience-name slug rules (lowercase alnum/dash/underscore).
136
+ const ChatbotTaskIdSchema = z.string().min(1).max(32).regex(/^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/,
137
+ "Task id must be lowercase letters, numbers, dashes, or underscores");
138
+
139
+ export const ChatbotTaskSchema = z.object({
140
+ id: ChatbotTaskIdSchema,
141
+ label: z.string().min(1).max(60),
142
+ prompt: z.string().max(4_000),
143
+ // Audience names allowed to see this task; [] = all — same semantics as SectionMeta.access.
144
+ access: z.array(z.string()).default([]),
145
+ });
146
+ export type ChatbotTask = z.infer<typeof ChatbotTaskSchema>;
147
+
148
+ export const ChatbotConfigSchema = z
149
+ .object({
150
+ enabled: z.boolean().default(false),
151
+ systemPrompt: z.string().max(8_000).default(""),
152
+ // Audience names the widget is shown to; [] = all audiences.
153
+ audiences: z.array(z.string()).default([]),
154
+ tasks: z.array(ChatbotTaskSchema).max(20).default([]),
155
+ })
156
+ .superRefine((cfg, ctx) => {
157
+ const seen = new Set<string>();
158
+ for (const t of cfg.tasks) {
159
+ if (seen.has(t.id)) {
160
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Duplicate task id "${t.id}"` });
161
+ }
162
+ seen.add(t.id);
163
+ }
164
+ });
165
+ export type ChatbotConfig = z.infer<typeof ChatbotConfigSchema>;
166
+
135
167
  export const SiteConfigSchema = z.object({
136
168
  siteName: z.string().default("Brand Portal"),
137
169
  primaryColor: HexColorSchema.default("#009ca6"),
@@ -153,6 +185,10 @@ export const SiteConfigSchema = z.object({
153
185
  .nullable()
154
186
  .default(null),
155
187
  media: MediaConfigSchema.default(MediaConfigSchema.parse({})),
188
+ // Brand chatbot config (editor-owned; the platform kill switch lives in the
189
+ // sites table, not here). Every field defaults so pre-existing
190
+ // site-config.json files keep parsing.
191
+ chatbot: ChatbotConfigSchema.default({ enabled: false, systemPrompt: "", audiences: [], tasks: [] }),
156
192
  });
157
193
 
158
194
  export type SiteConfig = z.infer<typeof SiteConfigSchema>;
@@ -150,6 +150,41 @@ export type Database = {
150
150
  },
151
151
  ]
152
152
  }
153
+ chat_usage: {
154
+ Row: {
155
+ day: string
156
+ input_tokens: number
157
+ output_tokens: number
158
+ requests: number
159
+ site_id: string
160
+ updated_at: string
161
+ }
162
+ Insert: {
163
+ day: string
164
+ input_tokens?: number
165
+ output_tokens?: number
166
+ requests?: number
167
+ site_id: string
168
+ updated_at?: string
169
+ }
170
+ Update: {
171
+ day?: string
172
+ input_tokens?: number
173
+ output_tokens?: number
174
+ requests?: number
175
+ site_id?: string
176
+ updated_at?: string
177
+ }
178
+ Relationships: [
179
+ {
180
+ foreignKeyName: "chat_usage_site_id_fkey"
181
+ columns: ["site_id"]
182
+ isOneToOne: false
183
+ referencedRelation: "sites"
184
+ referencedColumns: ["id"]
185
+ },
186
+ ]
187
+ }
153
188
  deploy_status: {
154
189
  Row: {
155
190
  commit_sha: string | null
@@ -535,6 +570,7 @@ export type Database = {
535
570
  }
536
571
  sites: {
537
572
  Row: {
573
+ chatbot_enabled: boolean
538
574
  created_at: string | null
539
575
  custom_domain: string | null
540
576
  failed_step: string | null
@@ -550,6 +586,7 @@ export type Database = {
550
586
  subdomain: string | null
551
587
  }
552
588
  Insert: {
589
+ chatbot_enabled?: boolean
553
590
  created_at?: string | null
554
591
  custom_domain?: string | null
555
592
  failed_step?: string | null
@@ -565,6 +602,7 @@ export type Database = {
565
602
  subdomain?: string | null
566
603
  }
567
604
  Update: {
605
+ chatbot_enabled?: boolean
568
606
  created_at?: string | null
569
607
  custom_domain?: string | null
570
608
  failed_step?: string | null
@@ -642,6 +680,16 @@ export type Database = {
642
680
  [_ in never]: never
643
681
  }
644
682
  Functions: {
683
+ chat_usage_hit: {
684
+ Args: {
685
+ p_day: string
686
+ p_input_tokens: number
687
+ p_output_tokens: number
688
+ p_requests: number
689
+ p_site_id: string
690
+ }
691
+ Returns: undefined
692
+ }
645
693
  get_user_id_by_email: { Args: { lookup_email: string }; Returns: string }
646
694
  is_site_member: { Args: { p_site_id: string }; Returns: boolean }
647
695
  is_site_owner: { Args: { p_site_id: string }; Returns: boolean }
@@ -1,8 +0,0 @@
1
- // src/lib/pages.ts
2
- function slugifyPageSlug(input) {
3
- return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
4
- }
5
-
6
- export {
7
- slugifyPageSlug
8
- };