@btst/stack 2.3.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/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/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 +7 -1
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +7 -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 +6 -1
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +6 -1
- 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/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/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.d.cts +2 -2
- package/dist/plugins/blog/api/index.d.mts +2 -2
- package/dist/plugins/blog/api/index.d.ts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- 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.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- package/dist/plugins/cms/api/index.cjs +2 -0
- package/dist/plugins/cms/api/index.d.cts +1 -1
- package/dist/plugins/cms/api/index.d.mts +1 -1
- package/dist/plugins/cms/api/index.d.ts +1 -1
- package/dist/plugins/cms/api/index.mjs +1 -0
- package/dist/plugins/cms/query-keys.d.cts +1 -1
- package/dist/plugins/cms/query-keys.d.mts +1 -1
- package/dist/plugins/cms/query-keys.d.ts +1 -1
- package/dist/plugins/form-builder/api/index.d.cts +1 -1
- package/dist/plugins/form-builder/api/index.d.mts +1 -1
- package/dist/plugins/form-builder/api/index.d.ts +1 -1
- package/dist/plugins/form-builder/query-keys.d.cts +1 -1
- package/dist/plugins/form-builder/query-keys.d.mts +1 -1
- package/dist/plugins/form-builder/query-keys.d.ts +1 -1
- package/dist/plugins/kanban/api/index.cjs +4 -0
- package/dist/plugins/kanban/api/index.d.cts +1 -1
- package/dist/plugins/kanban/api/index.d.mts +1 -1
- package/dist/plugins/kanban/api/index.d.ts +1 -1
- package/dist/plugins/kanban/api/index.mjs +1 -0
- package/dist/plugins/kanban/query-keys.d.cts +1 -1
- package/dist/plugins/kanban/query-keys.d.mts +1 -1
- package/dist/plugins/kanban/query-keys.d.ts +1 -1
- package/dist/shared/{stack.BeSm90va.d.ts → stack.BEn34wW6.d.ts} +60 -2
- package/dist/shared/{stack.IdtKDRka.d.cts → stack.BUkC2EsZ.d.cts} +32 -2
- 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.rTy7-wQU.d.mts → stack.BepFXT3w.d.mts} +70 -15
- package/dist/shared/{stack.BKfolAyK.d.ts → stack.CL8ts1Mu.d.ts} +3 -3
- package/dist/shared/{stack.CP68pFEH.d.mts → stack.CczspVn2.d.mts} +32 -2
- package/dist/shared/{stack.TIBF2AOx.d.ts → stack.CgWzG5jH.d.ts} +70 -15
- package/dist/shared/{stack.BpolpQpf.d.cts → stack.D3GB6wKv.d.cts} +70 -15
- package/dist/shared/{stack.B1EeBt1b.d.ts → stack.DASmUVjX.d.ts} +32 -2
- 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.snB1EDP7.d.cts → stack.Dk5r4W1F.d.mts} +3 -3
- package/dist/shared/{stack.BIXEI6v_.d.mts → stack.heOA9gzA.d.cts} +3 -3
- package/package.json +14 -1
- 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/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/cms/api/index.ts +4 -0
- package/src/plugins/cms/api/mutations.ts +84 -0
- package/src/plugins/cms/api/plugin.ts +9 -0
- package/src/plugins/kanban/api/index.ts +6 -0
- package/src/plugins/kanban/api/mutations.ts +169 -0
- package/src/plugins/kanban/api/plugin.ts +12 -0
- package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +4 -0
- package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +132 -0
- package/dist/shared/{stack.C5dtIncc.d.mts → stack.B7ONvlD_.d.mts} +1 -1
- package/dist/shared/{stack.CBON0dWL.d.cts → stack.BQmuNl5p.d.cts} +2 -2
- package/dist/shared/{stack.CBON0dWL.d.mts → stack.BQmuNl5p.d.mts} +2 -2
- package/dist/shared/{stack.CBON0dWL.d.ts → stack.BQmuNl5p.d.ts} +2 -2
- package/dist/shared/{stack.CIP6QS9l.d.ts → stack.Kq2-QzOC.d.ts} +1 -1
- package/dist/shared/{stack.Dw0Ly2TM.d.cts → stack.kcdnD4gA.d.cts} +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Adapter } from "@btst/db";
|
|
2
|
+
import type { Board, Column, Task, Priority } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Input for creating a new Kanban task.
|
|
6
|
+
*/
|
|
7
|
+
export interface CreateKanbanTaskInput {
|
|
8
|
+
title: string;
|
|
9
|
+
columnId: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
priority?: Priority;
|
|
12
|
+
assigneeId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a new task in a Kanban column.
|
|
17
|
+
* Computes the next order value from existing tasks in the column.
|
|
18
|
+
*
|
|
19
|
+
* @remarks **Security:** No authorization hooks (onBeforeCreateTask) are called.
|
|
20
|
+
* The caller is responsible for any access-control checks before invoking this
|
|
21
|
+
* function.
|
|
22
|
+
*
|
|
23
|
+
* @param adapter - The database adapter
|
|
24
|
+
* @param input - Task creation input
|
|
25
|
+
*/
|
|
26
|
+
export async function createKanbanTask(
|
|
27
|
+
adapter: Adapter,
|
|
28
|
+
input: CreateKanbanTaskInput,
|
|
29
|
+
): Promise<Task> {
|
|
30
|
+
const existingTasks = await adapter.findMany<Task>({
|
|
31
|
+
model: "kanbanTask",
|
|
32
|
+
where: [
|
|
33
|
+
{
|
|
34
|
+
field: "columnId",
|
|
35
|
+
value: input.columnId,
|
|
36
|
+
operator: "eq" as const,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const nextOrder =
|
|
42
|
+
existingTasks.length > 0
|
|
43
|
+
? Math.max(...existingTasks.map((t) => t.order)) + 1
|
|
44
|
+
: 0;
|
|
45
|
+
|
|
46
|
+
return adapter.create<Task>({
|
|
47
|
+
model: "kanbanTask",
|
|
48
|
+
data: {
|
|
49
|
+
title: input.title,
|
|
50
|
+
columnId: input.columnId,
|
|
51
|
+
description: input.description,
|
|
52
|
+
priority: input.priority ?? "MEDIUM",
|
|
53
|
+
order: nextOrder,
|
|
54
|
+
assigneeId: input.assigneeId,
|
|
55
|
+
isArchived: false,
|
|
56
|
+
createdAt: new Date(),
|
|
57
|
+
updatedAt: new Date(),
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Coalesces concurrent `findOrCreateKanbanBoard` calls within the same process.
|
|
64
|
+
* Keyed by slug; entries are removed once the creation promise settles.
|
|
65
|
+
*/
|
|
66
|
+
const _pendingBoardCreations = new Map<string, Promise<Board>>();
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Find a board by slug, or create it with the given name and custom column titles.
|
|
70
|
+
*
|
|
71
|
+
* Concurrency-safe at two levels:
|
|
72
|
+
* - **Same process**: concurrent calls with the same slug share a single in-flight
|
|
73
|
+
* Promise (via `_pendingBoardCreations`), so only one DB write is attempted.
|
|
74
|
+
* - **Cross-instance**: the DB `unique` constraint on `slug` causes the losing
|
|
75
|
+
* write to throw; the catch block re-fetches and returns the winner's board.
|
|
76
|
+
*
|
|
77
|
+
* @remarks **Security:** No authorization hooks are called. The caller is
|
|
78
|
+
* responsible for any access-control checks before invoking this function.
|
|
79
|
+
*
|
|
80
|
+
* @param adapter - The database adapter
|
|
81
|
+
* @param slug - Unique URL-safe slug for the board
|
|
82
|
+
* @param name - Display name for the board (used only on creation)
|
|
83
|
+
* @param columnTitles - Ordered list of column names to create (used only on creation)
|
|
84
|
+
*/
|
|
85
|
+
export async function findOrCreateKanbanBoard(
|
|
86
|
+
adapter: Adapter,
|
|
87
|
+
slug: string,
|
|
88
|
+
name: string,
|
|
89
|
+
columnTitles: string[],
|
|
90
|
+
): Promise<Board> {
|
|
91
|
+
const existing = await adapter.findOne<Board>({
|
|
92
|
+
model: "kanbanBoard",
|
|
93
|
+
where: [{ field: "slug", value: slug, operator: "eq" as const }],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (existing) return existing;
|
|
97
|
+
|
|
98
|
+
// Coalesce same-process concurrent calls for this slug
|
|
99
|
+
const inflight = _pendingBoardCreations.get(slug);
|
|
100
|
+
if (inflight) return inflight;
|
|
101
|
+
|
|
102
|
+
const creation = (async () => {
|
|
103
|
+
try {
|
|
104
|
+
const board = await adapter.create<Board>({
|
|
105
|
+
model: "kanbanBoard",
|
|
106
|
+
data: {
|
|
107
|
+
name,
|
|
108
|
+
slug,
|
|
109
|
+
createdAt: new Date(),
|
|
110
|
+
updatedAt: new Date(),
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await Promise.all(
|
|
115
|
+
columnTitles.map((title, index) =>
|
|
116
|
+
adapter.create<Column>({
|
|
117
|
+
model: "kanbanColumn",
|
|
118
|
+
data: {
|
|
119
|
+
title,
|
|
120
|
+
boardId: board.id,
|
|
121
|
+
order: index,
|
|
122
|
+
createdAt: new Date(),
|
|
123
|
+
updatedAt: new Date(),
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return board;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
// Cross-instance race: another process won the unique-constraint race.
|
|
132
|
+
// Re-fetch so all callers return the same board.
|
|
133
|
+
const winner = await adapter.findOne<Board>({
|
|
134
|
+
model: "kanbanBoard",
|
|
135
|
+
where: [{ field: "slug", value: slug, operator: "eq" as const }],
|
|
136
|
+
});
|
|
137
|
+
if (winner) return winner;
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
})();
|
|
141
|
+
|
|
142
|
+
_pendingBoardCreations.set(slug, creation);
|
|
143
|
+
try {
|
|
144
|
+
return await creation;
|
|
145
|
+
} finally {
|
|
146
|
+
_pendingBoardCreations.delete(slug);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Retrieve all columns for a given board, sorted by order.
|
|
152
|
+
* Co-located with mutations because it is primarily used alongside
|
|
153
|
+
* {@link createKanbanTask} to resolve column IDs before task creation.
|
|
154
|
+
*
|
|
155
|
+
* @remarks **Security:** No authorization hooks are called.
|
|
156
|
+
*
|
|
157
|
+
* @param adapter - The database adapter
|
|
158
|
+
* @param boardId - The board ID
|
|
159
|
+
*/
|
|
160
|
+
export async function getKanbanColumnsByBoardId(
|
|
161
|
+
adapter: Adapter,
|
|
162
|
+
boardId: string,
|
|
163
|
+
): Promise<Column[]> {
|
|
164
|
+
return adapter.findMany<Column>({
|
|
165
|
+
model: "kanbanColumn",
|
|
166
|
+
where: [{ field: "boardId", value: boardId, operator: "eq" as const }],
|
|
167
|
+
sortBy: { field: "order", direction: "asc" },
|
|
168
|
+
});
|
|
169
|
+
}
|
|
@@ -24,6 +24,11 @@ import {
|
|
|
24
24
|
updateTaskSchema,
|
|
25
25
|
} from "../schemas";
|
|
26
26
|
import { getAllBoards, getBoardById } from "./getters";
|
|
27
|
+
import {
|
|
28
|
+
createKanbanTask,
|
|
29
|
+
findOrCreateKanbanBoard,
|
|
30
|
+
getKanbanColumnsByBoardId,
|
|
31
|
+
} from "./mutations";
|
|
27
32
|
import { KANBAN_QUERY_KEYS } from "./query-key-defs";
|
|
28
33
|
import { serializeBoard } from "./serializers";
|
|
29
34
|
import type { QueryClient } from "@tanstack/react-query";
|
|
@@ -317,6 +322,13 @@ export const kanbanBackendPlugin = (hooks?: KanbanBackendHooks) =>
|
|
|
317
322
|
getAllBoards(adapter, params),
|
|
318
323
|
getBoardById: (id: string) => getBoardById(adapter, id),
|
|
319
324
|
prefetchForRoute: createKanbanPrefetchForRoute(adapter),
|
|
325
|
+
// Mutations
|
|
326
|
+
createTask: (input: Parameters<typeof createKanbanTask>[1]) =>
|
|
327
|
+
createKanbanTask(adapter, input),
|
|
328
|
+
findOrCreateBoard: (slug: string, name: string, columnTitles: string[]) =>
|
|
329
|
+
findOrCreateKanbanBoard(adapter, slug, name, columnTitles),
|
|
330
|
+
getColumnsByBoardId: (boardId: string) =>
|
|
331
|
+
getKanbanColumnsByBoardId(adapter, boardId),
|
|
320
332
|
}),
|
|
321
333
|
|
|
322
334
|
routes: (adapter: Adapter) => {
|
|
@@ -85,6 +85,7 @@ export function useBoards(params?: {
|
|
|
85
85
|
return useQuery({
|
|
86
86
|
...queries.boards.list(params),
|
|
87
87
|
staleTime: 30_000,
|
|
88
|
+
refetchOnWindowFocus: true,
|
|
88
89
|
});
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -102,6 +103,7 @@ export function useSuspenseBoards(params?: {
|
|
|
102
103
|
const result = useSuspenseQuery({
|
|
103
104
|
...queries.boards.list(params),
|
|
104
105
|
staleTime: 30_000,
|
|
106
|
+
refetchOnWindowFocus: true,
|
|
105
107
|
});
|
|
106
108
|
|
|
107
109
|
if (result.error && !result.isFetching) {
|
|
@@ -121,6 +123,7 @@ export function useBoard(boardId: string) {
|
|
|
121
123
|
return useQuery({
|
|
122
124
|
...queries.boards.detail(boardId),
|
|
123
125
|
staleTime: 30_000,
|
|
126
|
+
refetchOnWindowFocus: true,
|
|
124
127
|
enabled: !!boardId,
|
|
125
128
|
});
|
|
126
129
|
}
|
|
@@ -135,6 +138,7 @@ export function useSuspenseBoard(boardId: string) {
|
|
|
135
138
|
const result = useSuspenseQuery({
|
|
136
139
|
...queries.boards.detail(boardId),
|
|
137
140
|
staleTime: 30_000,
|
|
141
|
+
refetchOnWindowFocus: true,
|
|
138
142
|
});
|
|
139
143
|
|
|
140
144
|
if (result.error && !result.isFetching) {
|
|
@@ -22,9 +22,12 @@ import { toast } from "sonner";
|
|
|
22
22
|
import UIBuilder from "@workspace/ui/components/ui-builder";
|
|
23
23
|
import type {
|
|
24
24
|
ComponentLayer,
|
|
25
|
+
ComponentRegistry,
|
|
25
26
|
Variable,
|
|
26
27
|
} from "@workspace/ui/components/ui-builder/types";
|
|
27
28
|
|
|
29
|
+
import { useLayerStore } from "@workspace/ui/lib/ui-builder/store/layer-store";
|
|
30
|
+
import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context";
|
|
28
31
|
import {
|
|
29
32
|
useSuspenseUIBuilderPage,
|
|
30
33
|
useCreateUIBuilderPage,
|
|
@@ -39,6 +42,104 @@ export interface PageBuilderPageProps {
|
|
|
39
42
|
id?: string;
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Generate a concise AI-readable description of the available components
|
|
47
|
+
* in the component registry, including their prop names.
|
|
48
|
+
*/
|
|
49
|
+
function buildRegistryDescription(registry: ComponentRegistry): string {
|
|
50
|
+
const lines: string[] = [];
|
|
51
|
+
for (const [name, entry] of Object.entries(registry) as [
|
|
52
|
+
string,
|
|
53
|
+
{ schema?: unknown },
|
|
54
|
+
][]) {
|
|
55
|
+
let propsLine = "";
|
|
56
|
+
try {
|
|
57
|
+
const shape = (entry.schema as any)?.shape as
|
|
58
|
+
| Record<string, unknown>
|
|
59
|
+
| undefined;
|
|
60
|
+
if (shape) {
|
|
61
|
+
const fields = Object.keys(shape).join(", ");
|
|
62
|
+
propsLine = ` — props: ${fields}`;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// ignore schema introspection errors
|
|
66
|
+
}
|
|
67
|
+
lines.push(`- ${name}${propsLine}`);
|
|
68
|
+
}
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build the full page description string for the AI context.
|
|
74
|
+
* Stays within the 8,000-character pageContext limit.
|
|
75
|
+
*/
|
|
76
|
+
function buildPageDescription(
|
|
77
|
+
id: string | undefined,
|
|
78
|
+
slug: string,
|
|
79
|
+
layers: ComponentLayer[],
|
|
80
|
+
registry: ComponentRegistry,
|
|
81
|
+
): string {
|
|
82
|
+
const header = id
|
|
83
|
+
? `UI Builder — editing page (slug: "${slug}")`
|
|
84
|
+
: "UI Builder — creating new page";
|
|
85
|
+
|
|
86
|
+
const layersJson = JSON.stringify(layers, null, 2);
|
|
87
|
+
|
|
88
|
+
const registryDesc = buildRegistryDescription(registry);
|
|
89
|
+
|
|
90
|
+
const layerFormat = `Each layer: { id: string, type: string, name: string, props: Record<string,any>, children?: ComponentLayer[] | string }`;
|
|
91
|
+
|
|
92
|
+
const full = [
|
|
93
|
+
header,
|
|
94
|
+
"",
|
|
95
|
+
`## Current Layers (${layers.length})`,
|
|
96
|
+
layersJson,
|
|
97
|
+
"",
|
|
98
|
+
`## Available Component Types`,
|
|
99
|
+
registryDesc,
|
|
100
|
+
"",
|
|
101
|
+
`## ComponentLayer format`,
|
|
102
|
+
layerFormat,
|
|
103
|
+
].join("\n");
|
|
104
|
+
|
|
105
|
+
// Trim to fit the 16,000-char server-side limit, cutting the layers JSON if needed
|
|
106
|
+
if (full.length <= 16000) return full;
|
|
107
|
+
|
|
108
|
+
// Re-build with truncated layers JSON
|
|
109
|
+
const overhead =
|
|
110
|
+
[
|
|
111
|
+
header,
|
|
112
|
+
"",
|
|
113
|
+
`## Current Layers (${layers.length})`,
|
|
114
|
+
"",
|
|
115
|
+
"",
|
|
116
|
+
`## Available Component Types`,
|
|
117
|
+
registryDesc,
|
|
118
|
+
"",
|
|
119
|
+
`## ComponentLayer format`,
|
|
120
|
+
layerFormat,
|
|
121
|
+
].join("\n").length + 30; // 30-char buffer for "...(truncated)"
|
|
122
|
+
|
|
123
|
+
const budget = Math.max(0, 16000 - overhead);
|
|
124
|
+
const truncatedLayers =
|
|
125
|
+
layersJson.length > budget
|
|
126
|
+
? layersJson.slice(0, budget) + "\n...(truncated)"
|
|
127
|
+
: layersJson;
|
|
128
|
+
|
|
129
|
+
return [
|
|
130
|
+
header,
|
|
131
|
+
"",
|
|
132
|
+
`## Current Layers (${layers.length})`,
|
|
133
|
+
truncatedLayers,
|
|
134
|
+
"",
|
|
135
|
+
`## Available Component Types`,
|
|
136
|
+
registryDesc,
|
|
137
|
+
"",
|
|
138
|
+
`## ComponentLayer format`,
|
|
139
|
+
layerFormat,
|
|
140
|
+
].join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
42
143
|
/**
|
|
43
144
|
* Slugify a string for URL-friendly slugs
|
|
44
145
|
*/
|
|
@@ -139,6 +240,37 @@ function PageBuilderPageContent({
|
|
|
139
240
|
// Auto-generate slug from first page name
|
|
140
241
|
const [autoSlug, setAutoSlug] = useState(!id);
|
|
141
242
|
|
|
243
|
+
// Register AI context so the chat can update the page layout
|
|
244
|
+
useRegisterPageAIContext({
|
|
245
|
+
routeName: id ? "ui-builder-edit-page" : "ui-builder-new-page",
|
|
246
|
+
pageDescription: buildPageDescription(id, slug, layers, componentRegistry),
|
|
247
|
+
suggestions: [
|
|
248
|
+
"Add a hero section",
|
|
249
|
+
"Add a 3-column feature grid",
|
|
250
|
+
"Make the layout full-width",
|
|
251
|
+
"Add a card with a title, description, and button",
|
|
252
|
+
"Replace the layout with a centered single-column design",
|
|
253
|
+
],
|
|
254
|
+
clientTools: {
|
|
255
|
+
updatePageLayers: async ({ layers: newLayers }) => {
|
|
256
|
+
// Drive the UIBuilder's Zustand store directly so the editor
|
|
257
|
+
// and layers panel update immediately. The store's onChange
|
|
258
|
+
// callback will propagate back to the parent's `layers` state.
|
|
259
|
+
const store = useLayerStore.getState();
|
|
260
|
+
store.initialize(
|
|
261
|
+
newLayers,
|
|
262
|
+
store.selectedPageId || newLayers[0]?.id,
|
|
263
|
+
undefined,
|
|
264
|
+
store.variables,
|
|
265
|
+
);
|
|
266
|
+
return {
|
|
267
|
+
success: true,
|
|
268
|
+
message: `Applied ${newLayers.length} layer(s) to the page`,
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
142
274
|
// Handle layers change from UIBuilder
|
|
143
275
|
const handleLayersChange = useCallback(
|
|
144
276
|
(newLayers: ComponentLayer[]) => {
|
|
@@ -191,8 +191,8 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
|
|
|
191
191
|
};
|
|
192
192
|
submittedAt: string;
|
|
193
193
|
id: string;
|
|
194
|
-
formId: string;
|
|
195
194
|
data: string;
|
|
195
|
+
formId: string;
|
|
196
196
|
submittedBy?: string | undefined;
|
|
197
197
|
ipAddress?: string | undefined;
|
|
198
198
|
userAgent?: string | undefined;
|
|
@@ -42,11 +42,11 @@ declare const createPostSchema: z.ZodObject<{
|
|
|
42
42
|
name: z.ZodString;
|
|
43
43
|
slug: z.ZodString;
|
|
44
44
|
}, z.core.$strip>]>>>>;
|
|
45
|
+
title: z.ZodString;
|
|
45
46
|
slug: z.ZodOptional<z.ZodString>;
|
|
46
|
-
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
47
47
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
48
48
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
49
|
-
|
|
49
|
+
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
50
50
|
content: z.ZodString;
|
|
51
51
|
excerpt: z.ZodString;
|
|
52
52
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -42,11 +42,11 @@ declare const createPostSchema: z.ZodObject<{
|
|
|
42
42
|
name: z.ZodString;
|
|
43
43
|
slug: z.ZodString;
|
|
44
44
|
}, z.core.$strip>]>>>>;
|
|
45
|
+
title: z.ZodString;
|
|
45
46
|
slug: z.ZodOptional<z.ZodString>;
|
|
46
|
-
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
47
47
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
48
48
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
49
|
-
|
|
49
|
+
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
50
50
|
content: z.ZodString;
|
|
51
51
|
excerpt: z.ZodString;
|
|
52
52
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -42,11 +42,11 @@ declare const createPostSchema: z.ZodObject<{
|
|
|
42
42
|
name: z.ZodString;
|
|
43
43
|
slug: z.ZodString;
|
|
44
44
|
}, z.core.$strip>]>>>>;
|
|
45
|
+
title: z.ZodString;
|
|
45
46
|
slug: z.ZodOptional<z.ZodString>;
|
|
46
|
-
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
47
47
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
48
48
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
49
|
-
|
|
49
|
+
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
50
50
|
content: z.ZodString;
|
|
51
51
|
excerpt: z.ZodString;
|
|
52
52
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -191,8 +191,8 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
|
|
|
191
191
|
};
|
|
192
192
|
submittedAt: string;
|
|
193
193
|
id: string;
|
|
194
|
-
formId: string;
|
|
195
194
|
data: string;
|
|
195
|
+
formId: string;
|
|
196
196
|
submittedBy?: string | undefined;
|
|
197
197
|
ipAddress?: string | undefined;
|
|
198
198
|
userAgent?: string | undefined;
|
|
@@ -191,8 +191,8 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
|
|
|
191
191
|
};
|
|
192
192
|
submittedAt: string;
|
|
193
193
|
id: string;
|
|
194
|
-
formId: string;
|
|
195
194
|
data: string;
|
|
195
|
+
formId: string;
|
|
196
196
|
submittedBy?: string | undefined;
|
|
197
197
|
ipAddress?: string | undefined;
|
|
198
198
|
userAgent?: string | undefined;
|