@btst/stack 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/stack/src/client/components/compose.cjs +1 -2
- package/dist/packages/stack/src/client/components/compose.mjs +1 -2
- package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.cjs +71 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.mjs +68 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +54 -7
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +54 -7
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.cjs +2 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.mjs +2 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +89 -22
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +90 -23
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +110 -33
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +112 -35
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.cjs +1 -1
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.mjs +1 -1
- package/dist/packages/stack/src/plugins/ai-chat/schemas.cjs +17 -1
- package/dist/packages/stack/src/plugins/ai-chat/schemas.mjs +17 -1
- package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +52 -1
- package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +52 -1
- package/dist/packages/stack/src/plugins/blog/api/query-key-defs.cjs +18 -0
- package/dist/packages/stack/src/plugins/blog/api/query-key-defs.mjs +15 -0
- package/dist/packages/stack/src/plugins/blog/api/serializers.cjs +21 -0
- package/dist/packages/stack/src/plugins/blog/api/serializers.mjs +18 -0
- package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.cjs +15 -2
- package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.mjs +16 -3
- package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.cjs +24 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.mjs +24 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.cjs +26 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.mjs +24 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.cjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.mjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -0
- package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +15 -0
- package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +16 -1
- package/dist/packages/stack/src/plugins/cms/api/getters.cjs +10 -0
- package/dist/packages/stack/src/plugins/cms/api/getters.mjs +10 -1
- package/dist/packages/stack/src/plugins/cms/api/mutations.cjs +48 -0
- package/dist/packages/stack/src/plugins/cms/api/mutations.mjs +46 -0
- package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +75 -0
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +76 -1
- package/dist/packages/stack/src/plugins/cms/api/query-key-defs.cjs +29 -0
- package/dist/packages/stack/src/plugins/cms/api/query-key-defs.mjs +26 -0
- package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +15 -0
- package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +16 -1
- package/dist/packages/stack/src/plugins/form-builder/api/getters.cjs +9 -0
- package/dist/packages/stack/src/plugins/form-builder/api/getters.mjs +9 -1
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +62 -1
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +63 -2
- package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.cjs +37 -0
- package/dist/packages/stack/src/plugins/form-builder/api/query-key-defs.mjs +33 -0
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +15 -0
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +16 -1
- package/dist/packages/stack/src/plugins/kanban/api/mutations.cjs +91 -0
- package/dist/packages/stack/src/plugins/kanban/api/mutations.mjs +87 -0
- package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +34 -1
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +34 -1
- package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.cjs +26 -0
- package/dist/packages/stack/src/plugins/kanban/api/query-key-defs.mjs +23 -0
- package/dist/packages/stack/src/plugins/kanban/api/serializers.cjs +30 -0
- package/dist/packages/stack/src/plugins/kanban/api/serializers.mjs +26 -0
- package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.cjs +7 -3
- package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.mjs +7 -3
- package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +10 -0
- package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +11 -1
- package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.cjs +89 -0
- package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.mjs +89 -0
- package/dist/packages/stack/src/plugins/utils.cjs +6 -0
- package/dist/packages/stack/src/plugins/utils.mjs +6 -1
- package/dist/plugins/ai-chat/api/index.d.cts +1 -1
- package/dist/plugins/ai-chat/api/index.d.mts +1 -1
- package/dist/plugins/ai-chat/api/index.d.ts +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
- package/dist/plugins/ai-chat/client/context/page-ai-context.cjs +92 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.cts +84 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.mts +84 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.ts +84 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.mjs +88 -0
- package/dist/plugins/ai-chat/client/hooks/index.d.cts +1 -1
- package/dist/plugins/ai-chat/client/hooks/index.d.mts +1 -1
- package/dist/plugins/ai-chat/client/hooks/index.d.ts +1 -1
- package/dist/plugins/ai-chat/client/index.d.cts +2 -2
- package/dist/plugins/ai-chat/client/index.d.mts +2 -2
- package/dist/plugins/ai-chat/client/index.d.ts +2 -2
- package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
- package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
- package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
- package/dist/plugins/blog/api/index.cjs +5 -0
- package/dist/plugins/blog/api/index.d.cts +19 -4
- package/dist/plugins/blog/api/index.d.mts +19 -4
- package/dist/plugins/blog/api/index.d.ts +19 -4
- package/dist/plugins/blog/api/index.mjs +2 -0
- package/dist/plugins/blog/client/hooks/index.d.cts +3 -3
- package/dist/plugins/blog/client/hooks/index.d.mts +3 -3
- package/dist/plugins/blog/client/hooks/index.d.ts +3 -3
- package/dist/plugins/blog/client/index.d.cts +1 -1
- package/dist/plugins/blog/client/index.d.mts +1 -1
- package/dist/plugins/blog/client/index.d.ts +1 -1
- package/dist/plugins/blog/query-keys.cjs +6 -5
- package/dist/plugins/blog/query-keys.d.cts +8 -387
- package/dist/plugins/blog/query-keys.d.mts +8 -387
- package/dist/plugins/blog/query-keys.d.ts +8 -387
- package/dist/plugins/blog/query-keys.mjs +6 -5
- package/dist/plugins/client/index.cjs +1 -0
- package/dist/plugins/client/index.d.cts +8 -1
- package/dist/plugins/client/index.d.mts +8 -1
- package/dist/plugins/client/index.d.ts +8 -1
- package/dist/plugins/client/index.mjs +1 -1
- package/dist/plugins/cms/api/index.cjs +8 -0
- package/dist/plugins/cms/api/index.d.cts +7 -219
- package/dist/plugins/cms/api/index.d.mts +7 -219
- package/dist/plugins/cms/api/index.d.ts +7 -219
- package/dist/plugins/cms/api/index.mjs +3 -1
- package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
- package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
- package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
- package/dist/plugins/cms/query-keys.cjs +2 -1
- package/dist/plugins/cms/query-keys.d.cts +5 -9
- package/dist/plugins/cms/query-keys.d.mts +5 -9
- package/dist/plugins/cms/query-keys.d.ts +5 -9
- package/dist/plugins/cms/query-keys.mjs +2 -1
- package/dist/plugins/form-builder/api/index.cjs +6 -0
- package/dist/plugins/form-builder/api/index.d.cts +7 -211
- package/dist/plugins/form-builder/api/index.d.mts +7 -211
- package/dist/plugins/form-builder/api/index.d.ts +7 -211
- package/dist/plugins/form-builder/api/index.mjs +2 -1
- package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
- package/dist/plugins/form-builder/query-keys.cjs +3 -2
- package/dist/plugins/form-builder/query-keys.d.cts +6 -6
- package/dist/plugins/form-builder/query-keys.d.mts +6 -6
- package/dist/plugins/form-builder/query-keys.d.ts +6 -6
- package/dist/plugins/form-builder/query-keys.mjs +3 -2
- package/dist/plugins/kanban/api/index.cjs +10 -0
- package/dist/plugins/kanban/api/index.d.cts +17 -392
- package/dist/plugins/kanban/api/index.d.mts +17 -392
- package/dist/plugins/kanban/api/index.d.ts +17 -392
- package/dist/plugins/kanban/api/index.mjs +3 -0
- package/dist/plugins/kanban/client/components/index.d.cts +1 -1
- package/dist/plugins/kanban/client/components/index.d.mts +1 -1
- package/dist/plugins/kanban/client/components/index.d.ts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
- package/dist/plugins/kanban/client/index.d.cts +1 -1
- package/dist/plugins/kanban/client/index.d.mts +1 -1
- package/dist/plugins/kanban/client/index.d.ts +1 -1
- package/dist/plugins/kanban/query-keys.cjs +2 -9
- package/dist/plugins/kanban/query-keys.d.cts +4 -16
- package/dist/plugins/kanban/query-keys.d.mts +4 -16
- package/dist/plugins/kanban/query-keys.d.ts +4 -16
- package/dist/plugins/kanban/query-keys.mjs +2 -9
- package/dist/plugins/ui-builder/index.d.cts +1 -1
- package/dist/plugins/ui-builder/index.d.mts +1 -1
- package/dist/plugins/ui-builder/index.d.ts +1 -1
- package/dist/shared/stack.B7ONvlD_.d.mts +293 -0
- package/dist/shared/{stack.BeSm90va.d.ts → stack.BEn34wW6.d.ts} +60 -2
- package/dist/shared/stack.BUkC2EsZ.d.cts +327 -0
- package/dist/shared/{stack.DaOcgmrM.d.ts → stack.BV9hnvu4.d.cts} +31 -7
- package/dist/shared/{stack.DaOcgmrM.d.cts → stack.BV9hnvu4.d.mts} +31 -7
- package/dist/shared/{stack.DaOcgmrM.d.mts → stack.BV9hnvu4.d.ts} +31 -7
- package/dist/shared/stack.BepFXT3w.d.mts +500 -0
- package/dist/shared/stack.CL8ts1Mu.d.ts +419 -0
- package/dist/shared/{stack.CXjzTMsb.d.cts → stack.CVDTkMoO.d.cts} +7 -1
- package/dist/shared/{stack.CXjzTMsb.d.mts → stack.CVDTkMoO.d.mts} +7 -1
- package/dist/shared/{stack.CXjzTMsb.d.ts → stack.CVDTkMoO.d.ts} +7 -1
- package/dist/shared/stack.CczspVn2.d.mts +327 -0
- package/dist/shared/stack.CgWzG5jH.d.ts +500 -0
- package/dist/shared/stack.D3GB6wKv.d.cts +500 -0
- package/dist/shared/stack.DASmUVjX.d.ts +327 -0
- package/dist/shared/{stack.QD1y_7NY.d.cts → stack.DJaKVY7v.d.cts} +1 -1
- package/dist/shared/{stack.QD1y_7NY.d.mts → stack.DJaKVY7v.d.mts} +1 -1
- package/dist/shared/{stack.QD1y_7NY.d.ts → stack.DJaKVY7v.d.ts} +1 -1
- package/dist/shared/{stack.Dg09R0oB.d.mts → stack.DTDxgFj8.d.mts} +60 -2
- package/dist/shared/{stack.CMh_EdxW.d.cts → stack.DWoCZff7.d.cts} +60 -2
- package/dist/shared/{stack.CIrIsc-A.d.cts → stack.DdI5W6MB.d.cts} +7 -1
- package/dist/shared/{stack.CIrIsc-A.d.mts → stack.DdI5W6MB.d.mts} +7 -1
- package/dist/shared/{stack.CIrIsc-A.d.ts → stack.DdI5W6MB.d.ts} +7 -1
- package/dist/shared/stack.Dk5r4W1F.d.mts +419 -0
- package/dist/shared/stack.Kq2-QzOC.d.ts +293 -0
- package/dist/shared/stack.heOA9gzA.d.cts +419 -0
- package/dist/shared/stack.kcdnD4gA.d.cts +293 -0
- package/package.json +16 -3
- package/src/client/components/compose.tsx +7 -4
- package/src/plugins/ai-chat/api/page-tools.ts +111 -0
- package/src/plugins/ai-chat/api/plugin.ts +180 -9
- package/src/plugins/ai-chat/client/components/chat-input.tsx +2 -2
- package/src/plugins/ai-chat/client/components/chat-interface.tsx +154 -58
- package/src/plugins/ai-chat/client/components/chat-layout.tsx +166 -32
- package/src/plugins/ai-chat/client/components/chat-sidebar.tsx +1 -1
- package/src/plugins/ai-chat/client/context/page-ai-context.tsx +240 -0
- package/src/plugins/ai-chat/schemas.ts +16 -0
- package/src/plugins/blog/api/index.ts +2 -0
- package/src/plugins/blog/api/plugin.ts +85 -0
- package/src/plugins/blog/api/query-key-defs.ts +46 -0
- package/src/plugins/blog/api/serializers.ts +27 -0
- package/src/plugins/blog/client/components/forms/post-forms.tsx +29 -2
- package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +28 -0
- package/src/plugins/blog/client/components/pages/fill-blog-form-handler.ts +38 -0
- package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +33 -1
- package/src/plugins/blog/client/components/pages/post-page.internal.tsx +20 -0
- package/src/plugins/blog/client/plugin.tsx +19 -0
- package/src/plugins/blog/query-keys.ts +5 -7
- package/src/plugins/client/index.ts +1 -1
- package/src/plugins/cms/api/getters.ts +24 -0
- package/src/plugins/cms/api/index.ts +14 -1
- package/src/plugins/cms/api/mutations.ts +84 -0
- package/src/plugins/cms/api/plugin.ts +114 -0
- package/src/plugins/cms/api/query-key-defs.ts +53 -0
- package/src/plugins/cms/api/serializers.ts +12 -0
- package/src/plugins/cms/client/plugin.tsx +19 -0
- package/src/plugins/cms/query-keys.ts +2 -1
- package/src/plugins/form-builder/api/getters.ts +23 -0
- package/src/plugins/form-builder/api/index.ts +15 -2
- package/src/plugins/form-builder/api/plugin.ts +91 -0
- package/src/plugins/form-builder/api/query-key-defs.ts +79 -0
- package/src/plugins/form-builder/api/serializers.ts +12 -0
- package/src/plugins/form-builder/client/plugin.tsx +19 -0
- package/src/plugins/form-builder/query-keys.ts +6 -2
- package/src/plugins/kanban/api/index.ts +9 -0
- package/src/plugins/kanban/api/mutations.ts +169 -0
- package/src/plugins/kanban/api/plugin.ts +61 -0
- package/src/plugins/kanban/api/query-key-defs.ts +54 -0
- package/src/plugins/kanban/api/serializers.ts +49 -0
- package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +4 -0
- package/src/plugins/kanban/client/plugin.tsx +13 -0
- package/src/plugins/kanban/query-keys.ts +2 -9
- package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +132 -0
- package/src/plugins/utils.ts +19 -0
- package/dist/shared/{stack.BkYlUT_8.d.cts → stack.BQmuNl5p.d.cts} +6 -6
- package/dist/shared/{stack.BkYlUT_8.d.mts → stack.BQmuNl5p.d.mts} +6 -6
- package/dist/shared/{stack.BkYlUT_8.d.ts → stack.BQmuNl5p.d.ts} +6 -6
|
@@ -2,57 +2,116 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback } from "react";
|
|
4
4
|
import { Button } from "@workspace/ui/components/button";
|
|
5
|
+
import { Badge } from "@workspace/ui/components/badge";
|
|
5
6
|
import {
|
|
6
7
|
Sheet,
|
|
7
8
|
SheetContent,
|
|
8
9
|
SheetTrigger,
|
|
9
10
|
} from "@workspace/ui/components/sheet";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Menu,
|
|
13
|
+
PanelLeftClose,
|
|
14
|
+
PanelLeft,
|
|
15
|
+
Sparkles,
|
|
16
|
+
Trash2,
|
|
17
|
+
X,
|
|
18
|
+
} from "lucide-react";
|
|
11
19
|
import { cn } from "@workspace/ui/lib/utils";
|
|
12
20
|
import { ChatSidebar } from "./chat-sidebar";
|
|
13
21
|
import { ChatInterface } from "./chat-interface";
|
|
14
22
|
import type { UIMessage } from "ai";
|
|
23
|
+
import { usePageAIContext } from "../context/page-ai-context";
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
interface ChatLayoutBaseProps {
|
|
17
26
|
/** API base URL */
|
|
18
27
|
apiBaseURL: string;
|
|
19
28
|
/** API base path */
|
|
20
29
|
apiBasePath: string;
|
|
21
30
|
/** Current conversation ID (if viewing existing conversation) */
|
|
22
31
|
conversationId?: string;
|
|
23
|
-
/** Layout mode: 'full' for full page with sidebar, 'widget' for embeddable widget */
|
|
24
|
-
layout?: "full" | "widget";
|
|
25
32
|
/** Additional class name for the container */
|
|
26
33
|
className?: string;
|
|
27
|
-
/** Whether to show the sidebar
|
|
34
|
+
/** Whether to show the sidebar */
|
|
28
35
|
showSidebar?: boolean;
|
|
29
|
-
/** Height of the widget (only applies to widget layout) */
|
|
30
|
-
widgetHeight?: string | number;
|
|
31
36
|
/** Initial messages to populate the chat (useful for localStorage persistence in public mode) */
|
|
32
37
|
initialMessages?: UIMessage[];
|
|
33
38
|
/** Called whenever messages change (for persistence). Only fires in public mode. */
|
|
34
39
|
onMessagesChange?: (messages: UIMessage[]) => void;
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
interface ChatLayoutWidgetProps extends ChatLayoutBaseProps {
|
|
43
|
+
/** Widget mode: compact embeddable panel with a floating trigger button */
|
|
44
|
+
layout: "widget";
|
|
45
|
+
/** Height of the widget panel. Default: `"600px"` */
|
|
46
|
+
widgetHeight?: string | number;
|
|
47
|
+
/** Width of the widget panel. Default: `"380px"` */
|
|
48
|
+
widgetWidth?: string | number;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the widget panel starts open. Default: `false`.
|
|
51
|
+
* Set to `true` when embedding inside an already-open container such as a
|
|
52
|
+
* Next.js intercepting-route modal — the panel will be immediately visible
|
|
53
|
+
* without the user needing to click the trigger button.
|
|
54
|
+
*/
|
|
55
|
+
defaultOpen?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Whether to render the built-in floating trigger button. Default: `true`.
|
|
58
|
+
* Set to `false` when you control open/close externally (e.g. a Next.js
|
|
59
|
+
* parallel-route slot, a custom button, or a `router.back()` dismiss action)
|
|
60
|
+
* so that the built-in button does not appear alongside your own UI.
|
|
61
|
+
*/
|
|
62
|
+
showTrigger?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ChatLayoutFullProps extends ChatLayoutBaseProps {
|
|
66
|
+
/** Full-page mode with sidebar navigation (default) */
|
|
67
|
+
layout?: "full";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Props for the ChatLayout component */
|
|
71
|
+
export type ChatLayoutProps = ChatLayoutWidgetProps | ChatLayoutFullProps;
|
|
72
|
+
|
|
37
73
|
/**
|
|
38
74
|
* ChatLayout component that provides a full-page chat experience with sidebar
|
|
39
75
|
* or a compact widget mode for embedding.
|
|
40
76
|
*/
|
|
41
|
-
export function ChatLayout({
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
77
|
+
export function ChatLayout(props: ChatLayoutProps) {
|
|
78
|
+
const {
|
|
79
|
+
apiBaseURL,
|
|
80
|
+
apiBasePath,
|
|
81
|
+
conversationId,
|
|
82
|
+
layout = "full",
|
|
83
|
+
className,
|
|
84
|
+
showSidebar = true,
|
|
85
|
+
initialMessages,
|
|
86
|
+
onMessagesChange,
|
|
87
|
+
} = props;
|
|
88
|
+
|
|
89
|
+
// Widget-specific props — TypeScript narrows props to ChatLayoutWidgetProps here
|
|
90
|
+
const widgetHeight =
|
|
91
|
+
props.layout === "widget" ? (props.widgetHeight ?? "600px") : "600px";
|
|
92
|
+
const widgetWidth =
|
|
93
|
+
props.layout === "widget" ? (props.widgetWidth ?? "380px") : "380px";
|
|
94
|
+
const defaultOpen =
|
|
95
|
+
props.layout === "widget" ? (props.defaultOpen ?? false) : false;
|
|
96
|
+
const showTrigger =
|
|
97
|
+
props.layout === "widget" ? (props.showTrigger ?? true) : true;
|
|
98
|
+
|
|
52
99
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
53
100
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
54
101
|
// Key to force ChatInterface remount when starting a new chat
|
|
55
102
|
const [chatResetKey, setChatResetKey] = useState(0);
|
|
103
|
+
// Widget open/closed state — starts with defaultOpen value
|
|
104
|
+
const [widgetOpen, setWidgetOpen] = useState(defaultOpen);
|
|
105
|
+
// Key to force widget ChatInterface remount on clear
|
|
106
|
+
const [widgetResetKey, setWidgetResetKey] = useState(0);
|
|
107
|
+
// Only mount the widget ChatInterface after the widget has been opened at least once.
|
|
108
|
+
// This ensures pageAIContext is already registered before ChatInterface first renders,
|
|
109
|
+
// so suggestion chips and tool hints appear immediately on first open.
|
|
110
|
+
// When defaultOpen is true the widget is pre-opened, so we mark it as ever-opened immediately.
|
|
111
|
+
const [widgetEverOpened, setWidgetEverOpened] = useState(defaultOpen);
|
|
112
|
+
|
|
113
|
+
// Read page AI context to show badge in header
|
|
114
|
+
const pageAIContext = usePageAIContext();
|
|
56
115
|
|
|
57
116
|
const apiPath = `${apiBaseURL}${apiBasePath}/chat`;
|
|
58
117
|
|
|
@@ -67,20 +126,83 @@ export function ChatLayout({
|
|
|
67
126
|
|
|
68
127
|
if (layout === "widget") {
|
|
69
128
|
return (
|
|
70
|
-
<div
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
className
|
|
129
|
+
<div className={cn("flex flex-col items-end gap-3", className)}>
|
|
130
|
+
{/* Chat panel — always mounted to preserve conversation state, hidden when closed */}
|
|
131
|
+
<div
|
|
132
|
+
className={cn(
|
|
133
|
+
"flex flex-col border rounded-xl overflow-hidden bg-background shadow-xl",
|
|
134
|
+
widgetOpen ? "flex" : "hidden",
|
|
135
|
+
)}
|
|
136
|
+
style={{ height: widgetHeight, width: widgetWidth }}
|
|
137
|
+
>
|
|
138
|
+
{/* Widget header with page context badge and action buttons */}
|
|
139
|
+
<div className="flex items-center gap-1.5 px-3 py-1.5 border-b bg-muted/40">
|
|
140
|
+
<Sparkles className="h-3 w-3 text-muted-foreground" />
|
|
141
|
+
{pageAIContext ? (
|
|
142
|
+
<Badge
|
|
143
|
+
variant="secondary"
|
|
144
|
+
className="text-xs"
|
|
145
|
+
data-testid="page-context-badge"
|
|
146
|
+
>
|
|
147
|
+
{pageAIContext.routeName}
|
|
148
|
+
</Badge>
|
|
149
|
+
) : (
|
|
150
|
+
<span className="text-xs text-muted-foreground font-medium">
|
|
151
|
+
AI Chat
|
|
152
|
+
</span>
|
|
153
|
+
)}
|
|
154
|
+
<div className="flex-1" />
|
|
155
|
+
<Button
|
|
156
|
+
variant="ghost"
|
|
157
|
+
size="icon"
|
|
158
|
+
className="h-5 w-5"
|
|
159
|
+
onClick={() => setWidgetResetKey((prev) => prev + 1)}
|
|
160
|
+
aria-label="Clear chat"
|
|
161
|
+
title="Clear chat"
|
|
162
|
+
>
|
|
163
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
164
|
+
</Button>
|
|
165
|
+
<Button
|
|
166
|
+
variant="ghost"
|
|
167
|
+
size="icon"
|
|
168
|
+
className="h-5 w-5"
|
|
169
|
+
onClick={() => setWidgetOpen(false)}
|
|
170
|
+
aria-label="Close chat"
|
|
171
|
+
>
|
|
172
|
+
<X className="h-3.5 w-3.5" />
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
{widgetEverOpened && (
|
|
176
|
+
<ChatInterface
|
|
177
|
+
key={`widget-${conversationId ?? "new"}-${widgetResetKey}`}
|
|
178
|
+
apiPath={apiPath}
|
|
179
|
+
id={conversationId}
|
|
180
|
+
variant="widget"
|
|
181
|
+
initialMessages={initialMessages}
|
|
182
|
+
onMessagesChange={onMessagesChange}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* Trigger button — rendered only when showTrigger is true */}
|
|
188
|
+
{showTrigger && (
|
|
189
|
+
<Button
|
|
190
|
+
size="icon"
|
|
191
|
+
className="h-12 w-12 rounded-full shadow-lg"
|
|
192
|
+
onClick={() => {
|
|
193
|
+
setWidgetOpen((prev) => !prev);
|
|
194
|
+
setWidgetEverOpened(true);
|
|
195
|
+
}}
|
|
196
|
+
aria-label={widgetOpen ? "Close chat" : "Open chat"}
|
|
197
|
+
data-testid="widget-trigger"
|
|
198
|
+
>
|
|
199
|
+
{widgetOpen ? (
|
|
200
|
+
<X className="h-5 w-5" />
|
|
201
|
+
) : (
|
|
202
|
+
<Sparkles className="h-5 w-5" />
|
|
203
|
+
)}
|
|
204
|
+
</Button>
|
|
74
205
|
)}
|
|
75
|
-
style={{ height: widgetHeight }}
|
|
76
|
-
>
|
|
77
|
-
<ChatInterface
|
|
78
|
-
apiPath={apiPath}
|
|
79
|
-
id={conversationId}
|
|
80
|
-
variant="widget"
|
|
81
|
-
initialMessages={initialMessages}
|
|
82
|
-
onMessagesChange={onMessagesChange}
|
|
83
|
-
/>
|
|
84
206
|
</div>
|
|
85
207
|
);
|
|
86
208
|
}
|
|
@@ -115,7 +237,7 @@ export function ChatLayout({
|
|
|
115
237
|
{/* Main Chat Area */}
|
|
116
238
|
<div className="flex-1 flex flex-col min-w-0">
|
|
117
239
|
{/* Header */}
|
|
118
|
-
<div className="flex items-center gap-2 p-2 border-b bg-background/95 backdrop-blur supports-
|
|
240
|
+
<div className="flex items-center gap-2 p-2 border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
|
|
119
241
|
{/* Mobile menu button */}
|
|
120
242
|
{showSidebar && (
|
|
121
243
|
<Sheet open={mobileSidebarOpen} onOpenChange={setMobileSidebarOpen}>
|
|
@@ -159,6 +281,18 @@ export function ChatLayout({
|
|
|
159
281
|
)}
|
|
160
282
|
|
|
161
283
|
<div className="flex-1" />
|
|
284
|
+
|
|
285
|
+
{/* Page context badge — shown when a page has registered AI context */}
|
|
286
|
+
{pageAIContext && (
|
|
287
|
+
<Badge
|
|
288
|
+
variant="secondary"
|
|
289
|
+
className="text-xs gap-1 mr-2"
|
|
290
|
+
data-testid="page-context-badge"
|
|
291
|
+
>
|
|
292
|
+
<Sparkles className="h-3 w-3" />
|
|
293
|
+
{pageAIContext.routeName}
|
|
294
|
+
</Badge>
|
|
295
|
+
)}
|
|
162
296
|
</div>
|
|
163
297
|
|
|
164
298
|
<ChatInterface
|
|
@@ -163,7 +163,7 @@ export function ChatSidebar({
|
|
|
163
163
|
</div>
|
|
164
164
|
|
|
165
165
|
{/* Conversations List */}
|
|
166
|
-
<ScrollArea className="flex-1">
|
|
166
|
+
<ScrollArea className="flex-1 [&_[data-slot=scroll-area-viewport]>div]:!block">
|
|
167
167
|
<div className="p-2">
|
|
168
168
|
{isLoading ? (
|
|
169
169
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useId,
|
|
8
|
+
useEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from "react";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A client-side tool handler — receives the AI's tool call args and returns a result.
|
|
16
|
+
* The result is sent back to the model so it can continue the conversation.
|
|
17
|
+
*/
|
|
18
|
+
export type PageAIClientTool = (
|
|
19
|
+
args: any,
|
|
20
|
+
) => Promise<{ success: boolean; message?: string }>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configuration registered by a page to provide AI context and capabilities.
|
|
24
|
+
* Any component in the tree can call useRegisterPageAIContext with this config.
|
|
25
|
+
*/
|
|
26
|
+
export interface PageAIContextConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Identifier for the current route/page (e.g. "blog-post", "ui-builder-edit-page").
|
|
29
|
+
* Shown as a badge in the chat header.
|
|
30
|
+
*/
|
|
31
|
+
routeName: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Human-readable description of the current page and its content.
|
|
35
|
+
* Injected into the AI system prompt so it understands what the user is looking at.
|
|
36
|
+
* Capped at 8,000 characters server-side.
|
|
37
|
+
*/
|
|
38
|
+
pageDescription: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Optional suggested prompts shown as quick-action chips in the chat empty state.
|
|
42
|
+
* These augment (not replace) any static suggestions configured in plugin overrides.
|
|
43
|
+
*/
|
|
44
|
+
suggestions?: string[];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Client-side tool handlers keyed by tool name.
|
|
48
|
+
* When the AI calls a tool by this name, the handler is invoked with the tool args.
|
|
49
|
+
* The result is sent back to the model via addToolResult.
|
|
50
|
+
*
|
|
51
|
+
* Tool schemas must be registered server-side via enablePageTools + clientToolSchemas
|
|
52
|
+
* in aiChatBackendPlugin (built-in tools like fillBlogForm are pre-registered).
|
|
53
|
+
*/
|
|
54
|
+
clientTools?: Record<string, PageAIClientTool>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface PageAIAPIContextValue {
|
|
58
|
+
register: (id: string, config: PageAIContextConfig) => void;
|
|
59
|
+
unregister: (id: string) => void;
|
|
60
|
+
getActive: () => PageAIContextConfig | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Stable API context — holds register/unregister/getActive.
|
|
65
|
+
* Never changes reference, so useRegisterPageAIContext effects don't re-run
|
|
66
|
+
* simply because the provider re-rendered after a bumpVersion call.
|
|
67
|
+
*/
|
|
68
|
+
const PageAIAPIContext = createContext<PageAIAPIContextValue | null>(null);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reactive version context — incremented on every register/unregister.
|
|
72
|
+
* Consumers of usePageAIContext subscribe here so they re-render when
|
|
73
|
+
* registrations change and re-call getActive() to pick up the latest config.
|
|
74
|
+
*/
|
|
75
|
+
const PageAIVersionContext = createContext<number>(0);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Provider that enables route-aware AI context across the app.
|
|
79
|
+
*
|
|
80
|
+
* Place this at the root layout — above all StackProviders — so it spans
|
|
81
|
+
* both your main app tree and any chat modals rendered as parallel/intercept routes.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // app/layout.tsx
|
|
85
|
+
* import { PageAIContextProvider } from "@btst/stack/plugins/ai-chat/client/context"
|
|
86
|
+
*
|
|
87
|
+
* export default function RootLayout({ children }) {
|
|
88
|
+
* return <PageAIContextProvider>{children}</PageAIContextProvider>
|
|
89
|
+
* }
|
|
90
|
+
*/
|
|
91
|
+
export function PageAIContextProvider({
|
|
92
|
+
children,
|
|
93
|
+
}: {
|
|
94
|
+
children: React.ReactNode;
|
|
95
|
+
}) {
|
|
96
|
+
// Map from stable registration id → config
|
|
97
|
+
// Using useRef so mutations don't trigger re-renders of the provider itself
|
|
98
|
+
const registrationsRef = useRef<Map<string, PageAIContextConfig>>(new Map());
|
|
99
|
+
// Track insertion order so the last-registered (most specific) wins
|
|
100
|
+
const insertionOrderRef = useRef<string[]>([]);
|
|
101
|
+
|
|
102
|
+
// Version counter — bumped on every register/unregister so consumers re-read
|
|
103
|
+
const [version, setVersion] = useState(0);
|
|
104
|
+
const bumpVersion = useCallback(() => setVersion((v) => v + 1), []);
|
|
105
|
+
|
|
106
|
+
const register = useCallback(
|
|
107
|
+
(id: string, config: PageAIContextConfig) => {
|
|
108
|
+
registrationsRef.current.set(id, config);
|
|
109
|
+
// Move to end to mark as most recent
|
|
110
|
+
insertionOrderRef.current = insertionOrderRef.current.filter(
|
|
111
|
+
(k) => k !== id,
|
|
112
|
+
);
|
|
113
|
+
insertionOrderRef.current.push(id);
|
|
114
|
+
bumpVersion();
|
|
115
|
+
},
|
|
116
|
+
[bumpVersion],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const unregister = useCallback(
|
|
120
|
+
(id: string) => {
|
|
121
|
+
registrationsRef.current.delete(id);
|
|
122
|
+
insertionOrderRef.current = insertionOrderRef.current.filter(
|
|
123
|
+
(k) => k !== id,
|
|
124
|
+
);
|
|
125
|
+
bumpVersion();
|
|
126
|
+
},
|
|
127
|
+
[bumpVersion],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const getActive = useCallback((): PageAIContextConfig | null => {
|
|
131
|
+
const order = insertionOrderRef.current;
|
|
132
|
+
if (order.length === 0) return null;
|
|
133
|
+
// Last registered wins (most deeply nested / most recently mounted)
|
|
134
|
+
const lastId = order[order.length - 1];
|
|
135
|
+
if (!lastId) return null;
|
|
136
|
+
return registrationsRef.current.get(lastId) ?? null;
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
// Memoize the API object so its reference never changes — this is what
|
|
140
|
+
// breaks the infinite loop: useRegisterPageAIContext has `ctx` (the API)
|
|
141
|
+
// in its effect deps, and a stable reference means the effect won't re-run
|
|
142
|
+
// just because the provider re-rendered after bumpVersion().
|
|
143
|
+
const api = useMemo(
|
|
144
|
+
() => ({ register, unregister, getActive }),
|
|
145
|
+
[register, unregister, getActive],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<PageAIAPIContext.Provider value={api}>
|
|
150
|
+
<PageAIVersionContext.Provider value={version}>
|
|
151
|
+
{children}
|
|
152
|
+
</PageAIVersionContext.Provider>
|
|
153
|
+
</PageAIAPIContext.Provider>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Register page AI context from any component.
|
|
159
|
+
* The registration is cleaned up automatically when the component unmounts.
|
|
160
|
+
*
|
|
161
|
+
* Pass `null` to conditionally disable context (e.g. while data is loading).
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* // Blog post page
|
|
165
|
+
* useRegisterPageAIContext(post ? {
|
|
166
|
+
* routeName: "blog-post",
|
|
167
|
+
* pageDescription: `Blog post: "${post.title}"\n\n${post.content?.slice(0, 16000)}`,
|
|
168
|
+
* suggestions: ["Summarize this post", "What are the key takeaways?"],
|
|
169
|
+
* } : null)
|
|
170
|
+
*/
|
|
171
|
+
export function useRegisterPageAIContext(
|
|
172
|
+
config: PageAIContextConfig | null,
|
|
173
|
+
): void {
|
|
174
|
+
// Use the stable API context — its reference never changes, so adding it
|
|
175
|
+
// to the dependency array below does NOT cause the effect to re-run after
|
|
176
|
+
// bumpVersion() fires. This breaks the register → bumpVersion → re-render
|
|
177
|
+
// → effect re-run → register loop that caused "Maximum update depth exceeded".
|
|
178
|
+
const ctx = useContext(PageAIAPIContext);
|
|
179
|
+
const id = useId();
|
|
180
|
+
|
|
181
|
+
// Always keep the ref current so clientTools handlers are never stale.
|
|
182
|
+
// Updating a ref during render is safe — the value is visible to any effect
|
|
183
|
+
// that runs in the same commit.
|
|
184
|
+
const configRef = useRef<PageAIContextConfig | null>(config);
|
|
185
|
+
configRef.current = config;
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!ctx || !configRef.current) return;
|
|
189
|
+
// Register a live proxy that always reads from configRef. This ensures
|
|
190
|
+
// clientTools handlers are fresh even when the effect doesn't re-run —
|
|
191
|
+
// for example when a handler's closure captures new state but the
|
|
192
|
+
// serializable fields (routeName, pageDescription, suggestions) are unchanged.
|
|
193
|
+
// JSON.stringify silently strips function values, so clientTools would be
|
|
194
|
+
// invisible to a plain JSON.stringify(config) dependency check.
|
|
195
|
+
ctx.register(id, {
|
|
196
|
+
get routeName() {
|
|
197
|
+
return configRef.current?.routeName ?? "";
|
|
198
|
+
},
|
|
199
|
+
get pageDescription() {
|
|
200
|
+
return configRef.current?.pageDescription ?? "";
|
|
201
|
+
},
|
|
202
|
+
get suggestions() {
|
|
203
|
+
return configRef.current?.suggestions;
|
|
204
|
+
},
|
|
205
|
+
get clientTools() {
|
|
206
|
+
return configRef.current?.clientTools;
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
return () => {
|
|
210
|
+
ctx.unregister(id);
|
|
211
|
+
};
|
|
212
|
+
// Track serializable fields individually. JSON.stringify on the whole config
|
|
213
|
+
// would silently strip clientTools (functions), making handler changes invisible.
|
|
214
|
+
// Handler freshness is provided by the ref-proxy above instead.
|
|
215
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
216
|
+
}, [
|
|
217
|
+
ctx,
|
|
218
|
+
id,
|
|
219
|
+
config === null,
|
|
220
|
+
config?.routeName,
|
|
221
|
+
config?.pageDescription,
|
|
222
|
+
JSON.stringify(config?.suggestions),
|
|
223
|
+
]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Read the currently active page AI context.
|
|
228
|
+
* Returns null when no page has registered context, or when PageAIContextProvider
|
|
229
|
+
* is not in the tree.
|
|
230
|
+
*
|
|
231
|
+
* Used internally by ChatInterface to inject context into requests.
|
|
232
|
+
*/
|
|
233
|
+
export function usePageAIContext(): PageAIContextConfig | null {
|
|
234
|
+
// Subscribe to the version counter so this hook re-runs whenever a page
|
|
235
|
+
// registers or unregisters context, then read the latest active config.
|
|
236
|
+
useContext(PageAIVersionContext);
|
|
237
|
+
const ctx = useContext(PageAIAPIContext);
|
|
238
|
+
if (!ctx) return null;
|
|
239
|
+
return ctx.getActive();
|
|
240
|
+
}
|
|
@@ -37,4 +37,20 @@ export const chatRequestSchema = z.object({
|
|
|
37
37
|
),
|
|
38
38
|
conversationId: z.string().optional(),
|
|
39
39
|
model: z.string().optional(),
|
|
40
|
+
/**
|
|
41
|
+
* Description of the current page context, injected into the AI system prompt.
|
|
42
|
+
* Sent by ChatInterface when a page has registered context via useRegisterPageAIContext.
|
|
43
|
+
*/
|
|
44
|
+
pageContext: z.string().max(16000).optional(),
|
|
45
|
+
/**
|
|
46
|
+
* Names of client-side tools currently available on the page.
|
|
47
|
+
* The server includes matching tool schemas in the streamText call.
|
|
48
|
+
*/
|
|
49
|
+
availableTools: z.array(z.string()).optional(),
|
|
50
|
+
/**
|
|
51
|
+
* The routeName registered by the page via useRegisterPageAIContext.
|
|
52
|
+
* Cross-validated server-side against each built-in tool's route allowlist
|
|
53
|
+
* to prevent a page from claiming tools intended for a different route.
|
|
54
|
+
*/
|
|
55
|
+
routeName: z.string().optional(),
|
|
40
56
|
});
|
|
@@ -7,6 +7,90 @@ import type { Post, PostWithPostTag, Tag } from "../types";
|
|
|
7
7
|
import { slugify } from "../utils";
|
|
8
8
|
import { createPostSchema, updatePostSchema } from "../schemas";
|
|
9
9
|
import { getAllPosts, getPostBySlug, getAllTags } from "./getters";
|
|
10
|
+
import { BLOG_QUERY_KEYS } from "./query-key-defs";
|
|
11
|
+
import { serializePost, serializeTag } from "./serializers";
|
|
12
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Route keys for the blog plugin — matches the keys returned by
|
|
16
|
+
* `stackClient.router.getRoute(path).routeKey`.
|
|
17
|
+
*/
|
|
18
|
+
export type BlogRouteKey =
|
|
19
|
+
| "posts"
|
|
20
|
+
| "drafts"
|
|
21
|
+
| "post"
|
|
22
|
+
| "tag"
|
|
23
|
+
| "newPost"
|
|
24
|
+
| "editPost";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Overloaded signature for `prefetchForRoute`.
|
|
28
|
+
* TypeScript enforces the correct params for each routeKey at call sites.
|
|
29
|
+
*/
|
|
30
|
+
interface BlogPrefetchForRoute {
|
|
31
|
+
(key: "posts" | "drafts" | "newPost", qc: QueryClient): Promise<void>;
|
|
32
|
+
(
|
|
33
|
+
key: "post" | "editPost",
|
|
34
|
+
qc: QueryClient,
|
|
35
|
+
params: { slug: string },
|
|
36
|
+
): Promise<void>;
|
|
37
|
+
(key: "tag", qc: QueryClient, params: { tagSlug: string }): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createBlogPrefetchForRoute(adapter: Adapter): BlogPrefetchForRoute {
|
|
41
|
+
return async function prefetchForRoute(
|
|
42
|
+
key: BlogRouteKey,
|
|
43
|
+
qc: QueryClient,
|
|
44
|
+
params?: Record<string, string>,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
switch (key) {
|
|
47
|
+
case "posts":
|
|
48
|
+
case "drafts": {
|
|
49
|
+
const published = key === "posts";
|
|
50
|
+
const [result, tags] = await Promise.all([
|
|
51
|
+
getAllPosts(adapter, { published, limit: 10 }),
|
|
52
|
+
getAllTags(adapter),
|
|
53
|
+
]);
|
|
54
|
+
qc.setQueryData(BLOG_QUERY_KEYS.postsList({ published, limit: 10 }), {
|
|
55
|
+
pages: [result.items.map(serializePost)],
|
|
56
|
+
pageParams: [0],
|
|
57
|
+
});
|
|
58
|
+
qc.setQueryData(BLOG_QUERY_KEYS.tagsList(), tags.map(serializeTag));
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case "post":
|
|
62
|
+
case "editPost": {
|
|
63
|
+
const slug = params?.slug ?? "";
|
|
64
|
+
if (slug) {
|
|
65
|
+
const post = await getPostBySlug(adapter, slug);
|
|
66
|
+
qc.setQueryData(
|
|
67
|
+
BLOG_QUERY_KEYS.postDetail(slug),
|
|
68
|
+
post ? serializePost(post) : null,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "tag": {
|
|
74
|
+
const tagSlug = params?.tagSlug ?? "";
|
|
75
|
+
const [result, tags] = await Promise.all([
|
|
76
|
+
getAllPosts(adapter, { published: true, limit: 10, tagSlug }),
|
|
77
|
+
getAllTags(adapter),
|
|
78
|
+
]);
|
|
79
|
+
qc.setQueryData(
|
|
80
|
+
BLOG_QUERY_KEYS.postsList({ published: true, limit: 10, tagSlug }),
|
|
81
|
+
{
|
|
82
|
+
pages: [result.items.map(serializePost)],
|
|
83
|
+
pageParams: [0],
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
qc.setQueryData(BLOG_QUERY_KEYS.tagsList(), tags.map(serializeTag));
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
default:
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
} as BlogPrefetchForRoute;
|
|
93
|
+
}
|
|
10
94
|
|
|
11
95
|
export const PostListQuerySchema = z.object({
|
|
12
96
|
slug: z.string().optional(),
|
|
@@ -174,6 +258,7 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
174
258
|
getAllPosts(adapter, params),
|
|
175
259
|
getPostBySlug: (slug: string) => getPostBySlug(adapter, slug),
|
|
176
260
|
getAllTags: () => getAllTags(adapter),
|
|
261
|
+
prefetchForRoute: createBlogPrefetchForRoute(adapter),
|
|
177
262
|
}),
|
|
178
263
|
|
|
179
264
|
routes: (adapter: Adapter) => {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal query key constants for the blog plugin.
|
|
3
|
+
* Shared between query-keys.ts (HTTP path) and prefetchForRoute (DB path)
|
|
4
|
+
* to prevent key drift between SSR loaders and SSG prefetching.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface PostsListDiscriminator {
|
|
8
|
+
query: string | undefined;
|
|
9
|
+
limit: number;
|
|
10
|
+
published: boolean;
|
|
11
|
+
tagSlug: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds the discriminator object used as the cache key for the posts list.
|
|
16
|
+
* Mirrors the inline object in createPostsQueries so both paths stay in sync.
|
|
17
|
+
*/
|
|
18
|
+
export function postsListDiscriminator(params: {
|
|
19
|
+
published: boolean;
|
|
20
|
+
limit?: number;
|
|
21
|
+
tagSlug?: string;
|
|
22
|
+
query?: string;
|
|
23
|
+
}): PostsListDiscriminator {
|
|
24
|
+
return {
|
|
25
|
+
query:
|
|
26
|
+
params.query !== undefined && params.query.trim() === ""
|
|
27
|
+
? undefined
|
|
28
|
+
: params.query,
|
|
29
|
+
limit: params.limit ?? 10,
|
|
30
|
+
published: params.published,
|
|
31
|
+
tagSlug: params.tagSlug,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Full query key builders — use these with queryClient.setQueryData() */
|
|
36
|
+
export const BLOG_QUERY_KEYS = {
|
|
37
|
+
postsList: (params: {
|
|
38
|
+
published: boolean;
|
|
39
|
+
limit?: number;
|
|
40
|
+
tagSlug?: string;
|
|
41
|
+
}) => ["posts", "list", postsListDiscriminator(params)] as const,
|
|
42
|
+
|
|
43
|
+
postDetail: (slug: string) => ["posts", "detail", slug] as const,
|
|
44
|
+
|
|
45
|
+
tagsList: () => ["tags", "list", "tags"] as const,
|
|
46
|
+
};
|