@kubuild/ai 0.3.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/LICENSE +21 -0
- package/dist/chunk-27AY2KBI.js +500 -0
- package/dist/chunk-27AY2KBI.js.map +1 -0
- package/dist/chunk-JY4NHD3Q.js +227 -0
- package/dist/chunk-JY4NHD3Q.js.map +1 -0
- package/dist/chunk-UH24LHRY.js +443 -0
- package/dist/chunk-UH24LHRY.js.map +1 -0
- package/dist/client/ai-client.d.ts +59 -0
- package/dist/client/ai-client.d.ts.map +1 -0
- package/dist/client/index.cjs +254 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +9 -0
- package/dist/client/index.js.map +1 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/normalizer.d.ts +9 -0
- package/dist/core/normalizer.d.ts.map +1 -0
- package/dist/core/prompt-compiler.d.ts +10 -0
- package/dist/core/prompt-compiler.d.ts.map +1 -0
- package/dist/index.cjs +468 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +584 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +336 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/use-ai-chat.d.ts +63 -0
- package/dist/react/use-ai-chat.d.ts.map +1 -0
- package/dist/react/use-ai-generator.d.ts +19 -0
- package/dist/react/use-ai-generator.d.ts.map +1 -0
- package/dist/server/adapters/anthropic.d.ts +30 -0
- package/dist/server/adapters/anthropic.d.ts.map +1 -0
- package/dist/server/adapters/base.d.ts +2 -0
- package/dist/server/adapters/base.d.ts.map +1 -0
- package/dist/server/adapters/custom.d.ts +18 -0
- package/dist/server/adapters/custom.d.ts.map +1 -0
- package/dist/server/adapters/gemini.d.ts +22 -0
- package/dist/server/adapters/gemini.d.ts.map +1 -0
- package/dist/server/adapters/index.cjs +529 -0
- package/dist/server/adapters/index.cjs.map +1 -0
- package/dist/server/adapters/index.d.ts +6 -0
- package/dist/server/adapters/index.d.ts.map +1 -0
- package/dist/server/adapters/index.js +13 -0
- package/dist/server/adapters/index.js.map +1 -0
- package/dist/server/adapters/openai.d.ts +26 -0
- package/dist/server/adapters/openai.d.ts.map +1 -0
- package/dist/server/adapters/stream-utils.d.ts +12 -0
- package/dist/server/adapters/stream-utils.d.ts.map +1 -0
- package/dist/server/engine.d.ts +59 -0
- package/dist/server/engine.d.ts.map +1 -0
- package/dist/server/handler.d.ts +66 -0
- package/dist/server/handler.d.ts.map +1 -0
- package/dist/server/index.cjs +1824 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +890 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types.d.ts +184 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildJsonSchemaForMode,
|
|
3
|
+
buildSystemPrompt,
|
|
4
|
+
compileComponentCatalog,
|
|
5
|
+
extractJsonFromResponse,
|
|
6
|
+
normalizeAndValidatePageDocument,
|
|
7
|
+
normalizeAndValidateRefactoredNode,
|
|
8
|
+
normalizeAndValidateSectionNode
|
|
9
|
+
} from "../chunk-UH24LHRY.js";
|
|
10
|
+
import {
|
|
11
|
+
AnthropicAdapter,
|
|
12
|
+
CustomHttpAdapter,
|
|
13
|
+
GeminiAdapter,
|
|
14
|
+
OpenAiAdapter
|
|
15
|
+
} from "../chunk-27AY2KBI.js";
|
|
16
|
+
|
|
17
|
+
// src/server/engine.ts
|
|
18
|
+
import { DocumentMetadataSchema } from "@kubuild/schema";
|
|
19
|
+
var KubuildAiEngine = class {
|
|
20
|
+
options;
|
|
21
|
+
catalog;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
if (!options.adapter) {
|
|
24
|
+
throw new Error("KubuildAiEngine requires an adapter instance");
|
|
25
|
+
}
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.catalog = compileComponentCatalog(options.registry);
|
|
28
|
+
}
|
|
29
|
+
get adapterName() {
|
|
30
|
+
return this.options.adapter.name;
|
|
31
|
+
}
|
|
32
|
+
refreshCatalog() {
|
|
33
|
+
this.catalog = compileComponentCatalog(this.options.registry);
|
|
34
|
+
}
|
|
35
|
+
log(level, message, meta) {
|
|
36
|
+
if (this.options.logger) {
|
|
37
|
+
this.options.logger(level, message, meta);
|
|
38
|
+
} else if (this.options.debug) {
|
|
39
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
40
|
+
console.log(`[KUBUILD-AI ${level.toUpperCase()}] [${ts}] ${message}`, meta !== void 0 ? meta : "");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async generatePage(request, context) {
|
|
44
|
+
let rawText = "";
|
|
45
|
+
try {
|
|
46
|
+
this.log("info", `Generating full page for prompt: "${request.prompt}"`);
|
|
47
|
+
const systemPrompt = buildSystemPrompt({
|
|
48
|
+
catalog: this.catalog,
|
|
49
|
+
mode: "full-page",
|
|
50
|
+
prefix: this.options.systemPromptPrefix,
|
|
51
|
+
stylePreference: request.stylePreference
|
|
52
|
+
});
|
|
53
|
+
let userPrompt = `User Request: ${request.prompt}`;
|
|
54
|
+
if (request.tone) userPrompt += `
|
|
55
|
+
Tone: ${request.tone}`;
|
|
56
|
+
if (request.locale) userPrompt += `
|
|
57
|
+
Language/Locale: ${request.locale}`;
|
|
58
|
+
const jsonSchema = buildJsonSchemaForMode("full-page");
|
|
59
|
+
const result = await this.options.adapter.generate({
|
|
60
|
+
systemPrompt,
|
|
61
|
+
userPrompt,
|
|
62
|
+
jsonSchema,
|
|
63
|
+
signal: context?.signal
|
|
64
|
+
});
|
|
65
|
+
this.log("debug", "Raw model response (generatePage)", result.text);
|
|
66
|
+
rawText = result.text;
|
|
67
|
+
const rawJson = extractJsonFromResponse(rawText);
|
|
68
|
+
const document = normalizeAndValidatePageDocument(
|
|
69
|
+
rawJson,
|
|
70
|
+
this.options.securityLimits
|
|
71
|
+
);
|
|
72
|
+
if (request.metadata) {
|
|
73
|
+
document.metadata = DocumentMetadataSchema.parse({
|
|
74
|
+
...document.metadata,
|
|
75
|
+
...request.metadata
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
this.log(
|
|
79
|
+
"info",
|
|
80
|
+
`Full page successfully generated: "${document.metadata?.title || "Untitled"}" (${document.document.children?.length || 0} sections)`
|
|
81
|
+
);
|
|
82
|
+
return {
|
|
83
|
+
success: true,
|
|
84
|
+
data: document,
|
|
85
|
+
usage: result.usage,
|
|
86
|
+
rawModelResponse: rawText
|
|
87
|
+
};
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
this.log("error", `Generation failed: ${message}`);
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
error: {
|
|
94
|
+
code: "GENERATION_ERROR",
|
|
95
|
+
message
|
|
96
|
+
},
|
|
97
|
+
rawModelResponse: rawText || void 0
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async generateSection(request, context) {
|
|
102
|
+
let rawText = "";
|
|
103
|
+
try {
|
|
104
|
+
const systemPrompt = buildSystemPrompt({
|
|
105
|
+
catalog: this.catalog,
|
|
106
|
+
mode: "section",
|
|
107
|
+
prefix: this.options.systemPromptPrefix,
|
|
108
|
+
stylePreference: request.stylePreference
|
|
109
|
+
});
|
|
110
|
+
let userPrompt = `Generate a single section node for: ${request.prompt}`;
|
|
111
|
+
if (request.targetSectionType) {
|
|
112
|
+
userPrompt += `
|
|
113
|
+
Target Section Purpose: ${request.targetSectionType}`;
|
|
114
|
+
}
|
|
115
|
+
if (request.parentContext) {
|
|
116
|
+
userPrompt += `
|
|
117
|
+
Surrounding Page Context: ${request.parentContext}`;
|
|
118
|
+
}
|
|
119
|
+
const jsonSchema = buildJsonSchemaForMode("section");
|
|
120
|
+
const result = await this.options.adapter.generate({
|
|
121
|
+
systemPrompt,
|
|
122
|
+
userPrompt,
|
|
123
|
+
jsonSchema,
|
|
124
|
+
signal: context?.signal
|
|
125
|
+
});
|
|
126
|
+
this.log("debug", "Raw model response (generateSection)", result.text);
|
|
127
|
+
rawText = result.text;
|
|
128
|
+
const rawJson = extractJsonFromResponse(rawText);
|
|
129
|
+
const sectionNode = normalizeAndValidateSectionNode(
|
|
130
|
+
rawJson,
|
|
131
|
+
this.options.securityLimits
|
|
132
|
+
);
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
data: sectionNode,
|
|
136
|
+
usage: result.usage,
|
|
137
|
+
rawModelResponse: rawText
|
|
138
|
+
};
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
error: {
|
|
144
|
+
code: "SECTION_GENERATION_ERROR",
|
|
145
|
+
message
|
|
146
|
+
},
|
|
147
|
+
rawModelResponse: rawText || void 0
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async refactorNode(request, context) {
|
|
152
|
+
let rawText = "";
|
|
153
|
+
try {
|
|
154
|
+
const systemPrompt = buildSystemPrompt({
|
|
155
|
+
catalog: this.catalog,
|
|
156
|
+
mode: "refactor",
|
|
157
|
+
prefix: this.options.systemPromptPrefix,
|
|
158
|
+
stylePreference: request.stylePreference
|
|
159
|
+
});
|
|
160
|
+
const userPrompt = `Instruction: ${request.instruction}
|
|
161
|
+
|
|
162
|
+
Current Node:
|
|
163
|
+
${JSON.stringify(request.node, null, 2)}`;
|
|
164
|
+
const jsonSchema = buildJsonSchemaForMode("refactor");
|
|
165
|
+
const result = await this.options.adapter.generate({
|
|
166
|
+
systemPrompt,
|
|
167
|
+
userPrompt,
|
|
168
|
+
jsonSchema,
|
|
169
|
+
signal: context?.signal
|
|
170
|
+
});
|
|
171
|
+
rawText = result.text;
|
|
172
|
+
const rawJson = extractJsonFromResponse(rawText);
|
|
173
|
+
const updatedNode = normalizeAndValidateRefactoredNode(
|
|
174
|
+
rawJson,
|
|
175
|
+
request.node,
|
|
176
|
+
this.options.securityLimits
|
|
177
|
+
);
|
|
178
|
+
return {
|
|
179
|
+
success: true,
|
|
180
|
+
data: updatedNode,
|
|
181
|
+
usage: result.usage,
|
|
182
|
+
rawModelResponse: rawText
|
|
183
|
+
};
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
186
|
+
return {
|
|
187
|
+
success: false,
|
|
188
|
+
error: {
|
|
189
|
+
code: "REFACTOR_ERROR",
|
|
190
|
+
message
|
|
191
|
+
},
|
|
192
|
+
rawModelResponse: rawText || void 0
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Progressive Section Streaming Generator.
|
|
198
|
+
* Emits structured SSE events: status -> metadata -> section (one by one) -> complete.
|
|
199
|
+
*/
|
|
200
|
+
async *streamPage(request, context) {
|
|
201
|
+
try {
|
|
202
|
+
this.log("info", `[SSE] Starting streamPage for prompt: "${request.prompt}"`);
|
|
203
|
+
yield {
|
|
204
|
+
type: "status",
|
|
205
|
+
message: "Analyzing requirements and planning page sections..."
|
|
206
|
+
};
|
|
207
|
+
const planSystemPrompt = `
|
|
208
|
+
You are a web architect for the KUBUILD page builder.
|
|
209
|
+
Given the user's prompt, plan the website structure. Output pure JSON (no markdown fences, no explanatory text):
|
|
210
|
+
{
|
|
211
|
+
"title": "<Page Title>",
|
|
212
|
+
"description": "<Page Description>",
|
|
213
|
+
"pageStyles": {
|
|
214
|
+
"base": {
|
|
215
|
+
"backgroundColor": "#ffffff",
|
|
216
|
+
"fontFamily": "Inter, system-ui, sans-serif",
|
|
217
|
+
"color": "#111827",
|
|
218
|
+
"minHeight": "100vh"
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
"sections": [
|
|
222
|
+
{
|
|
223
|
+
"type": "<hero|features|pricing|testimonials|cta|contact|footer>",
|
|
224
|
+
"title": "<Brief title>",
|
|
225
|
+
"prompt": "<Detailed prompt for generating this specific section with components, layout, and content>"
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
Plan 3 cohesive, essential sections (e.g., hero, features, and cta) that fulfill the request.
|
|
230
|
+
`;
|
|
231
|
+
let planUserPrompt = `User Request: ${request.prompt}`;
|
|
232
|
+
if (request.stylePreference) {
|
|
233
|
+
planUserPrompt += `
|
|
234
|
+
Style Preference: ${request.stylePreference}`;
|
|
235
|
+
}
|
|
236
|
+
if (request.tone) planUserPrompt += `
|
|
237
|
+
Tone: ${request.tone}`;
|
|
238
|
+
if (request.locale) planUserPrompt += `
|
|
239
|
+
Locale: ${request.locale}`;
|
|
240
|
+
this.log("info", "[SSE] Generating website layout plan...");
|
|
241
|
+
const planResult = await this.options.adapter.generate({
|
|
242
|
+
systemPrompt: planSystemPrompt,
|
|
243
|
+
userPrompt: planUserPrompt,
|
|
244
|
+
signal: context?.signal
|
|
245
|
+
});
|
|
246
|
+
this.log("debug", "[SSE] Raw plan response from model", planResult.text);
|
|
247
|
+
let plan;
|
|
248
|
+
try {
|
|
249
|
+
plan = extractJsonFromResponse(planResult.text);
|
|
250
|
+
this.log("debug", "[SSE] Parsed plan successfully", plan);
|
|
251
|
+
} catch (parseErr) {
|
|
252
|
+
this.log("warn", "[SSE] Failed to parse plan JSON, using fallback plan", parseErr);
|
|
253
|
+
plan = {
|
|
254
|
+
title: "AI Generated Page",
|
|
255
|
+
description: "Generated by KUBUILD AI",
|
|
256
|
+
sections: [
|
|
257
|
+
{ type: "hero", title: "Hero Banner", prompt: `Hero section for: ${request.prompt}` },
|
|
258
|
+
{ type: "features", title: "Key Features", prompt: `Features grid for: ${request.prompt}` },
|
|
259
|
+
{ type: "cta", title: "Call To Action", prompt: `Call to action section for: ${request.prompt}` }
|
|
260
|
+
]
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const sectionsToGenerate = Array.isArray(plan.sections) && plan.sections.length > 0 ? plan.sections : [
|
|
264
|
+
{ type: "hero", title: "Hero Section", prompt: `Hero banner for: ${request.prompt}` },
|
|
265
|
+
{ type: "features", title: "Features", prompt: `Features grid for: ${request.prompt}` },
|
|
266
|
+
{ type: "cta", title: "Call To Action", prompt: `CTA section for: ${request.prompt}` }
|
|
267
|
+
];
|
|
268
|
+
const metadata = DocumentMetadataSchema.parse({
|
|
269
|
+
title: plan.title || "AI Generated Page",
|
|
270
|
+
description: plan.description || "Generated by KUBUILD AI",
|
|
271
|
+
author: "KUBUILD AI",
|
|
272
|
+
tags: ["ai-generated", "progressive-stream"],
|
|
273
|
+
category: "landing",
|
|
274
|
+
version: "1.0.0",
|
|
275
|
+
...request.metadata || {}
|
|
276
|
+
});
|
|
277
|
+
const rootPageNode = {
|
|
278
|
+
id: "root-page",
|
|
279
|
+
type: "page",
|
|
280
|
+
styles: plan.pageStyles || {
|
|
281
|
+
base: {
|
|
282
|
+
backgroundColor: "#ffffff",
|
|
283
|
+
fontFamily: "Inter, system-ui, sans-serif",
|
|
284
|
+
color: "#111827",
|
|
285
|
+
minHeight: "100vh"
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
children: []
|
|
289
|
+
};
|
|
290
|
+
this.log("info", `[SSE] Emitting metadata: "${metadata.title}"`);
|
|
291
|
+
yield {
|
|
292
|
+
type: "metadata",
|
|
293
|
+
metadata,
|
|
294
|
+
rootPageNode
|
|
295
|
+
};
|
|
296
|
+
this.log(
|
|
297
|
+
"info",
|
|
298
|
+
`[SSE] Plan ready with ${sectionsToGenerate.length} sections: ${sectionsToGenerate.map((s) => s.title).join(", ")}`
|
|
299
|
+
);
|
|
300
|
+
yield {
|
|
301
|
+
type: "status",
|
|
302
|
+
message: `Plan ready with ${sectionsToGenerate.length} sections: ${sectionsToGenerate.map((s) => s.title).join(", ")}`
|
|
303
|
+
};
|
|
304
|
+
const completedSections = [];
|
|
305
|
+
const total = sectionsToGenerate.length;
|
|
306
|
+
for (let i = 0; i < total; i++) {
|
|
307
|
+
if (context?.signal?.aborted) {
|
|
308
|
+
this.log("warn", "[SSE] Streaming aborted by client signal");
|
|
309
|
+
throw new Error("Streaming aborted by client");
|
|
310
|
+
}
|
|
311
|
+
const plannedSec = sectionsToGenerate[i];
|
|
312
|
+
this.log(
|
|
313
|
+
"info",
|
|
314
|
+
`[SSE] Generating section ${i + 1}/${total} [${plannedSec.type}]: "${plannedSec.title}"`
|
|
315
|
+
);
|
|
316
|
+
yield {
|
|
317
|
+
type: "status",
|
|
318
|
+
message: `Generating section ${i + 1}/${total} (${plannedSec.title})...`
|
|
319
|
+
};
|
|
320
|
+
const secRes = await this.generateSection(
|
|
321
|
+
{
|
|
322
|
+
prompt: plannedSec.prompt,
|
|
323
|
+
stylePreference: request.stylePreference,
|
|
324
|
+
targetSectionType: plannedSec.type,
|
|
325
|
+
parentContext: `Website: ${plan.title}. Previous sections: ${sectionsToGenerate.slice(0, i).map((s) => s.title).join(", ")}`
|
|
326
|
+
},
|
|
327
|
+
{ signal: context?.signal }
|
|
328
|
+
);
|
|
329
|
+
if (secRes.success && secRes.data) {
|
|
330
|
+
this.log(
|
|
331
|
+
"info",
|
|
332
|
+
`[SSE] Section ${i + 1}/${total} generated successfully: [${secRes.data.id || secRes.data.type}]`
|
|
333
|
+
);
|
|
334
|
+
completedSections.push(secRes.data);
|
|
335
|
+
yield {
|
|
336
|
+
type: "section",
|
|
337
|
+
index: i,
|
|
338
|
+
total,
|
|
339
|
+
section: secRes.data
|
|
340
|
+
};
|
|
341
|
+
} else {
|
|
342
|
+
this.log("error", `[SSE] Section ${i + 1}/${total} failed`, secRes.error?.message);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (completedSections.length === 0) {
|
|
346
|
+
this.log("error", "[SSE] All sections failed to generate.");
|
|
347
|
+
yield {
|
|
348
|
+
type: "error",
|
|
349
|
+
error: {
|
|
350
|
+
code: "STREAM_GENERATION_FAILED",
|
|
351
|
+
message: "All sections failed to generate. Please check server logs or retry."
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const finalDocument = {
|
|
357
|
+
schema: "stora.page",
|
|
358
|
+
version: "1.0.0",
|
|
359
|
+
metadata,
|
|
360
|
+
document: {
|
|
361
|
+
...rootPageNode,
|
|
362
|
+
children: completedSections
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
this.log(
|
|
366
|
+
"info",
|
|
367
|
+
`[SSE] All ${completedSections.length} sections generated. Emitting complete event.`
|
|
368
|
+
);
|
|
369
|
+
yield {
|
|
370
|
+
type: "complete",
|
|
371
|
+
document: finalDocument
|
|
372
|
+
};
|
|
373
|
+
} catch (err) {
|
|
374
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
375
|
+
this.log("error", `[SSE] Streaming failed: ${message}`);
|
|
376
|
+
yield {
|
|
377
|
+
type: "error",
|
|
378
|
+
error: {
|
|
379
|
+
code: "STREAM_ERROR",
|
|
380
|
+
message
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Builds the system/user prompt pair shared by `chat()` and `chatStream()` — the exact
|
|
387
|
+
* same document/selection grounding is used regardless of whether the response is
|
|
388
|
+
* streamed token-by-token or returned as a single request/response (STORA-515).
|
|
389
|
+
*/
|
|
390
|
+
buildChatPrompt(request) {
|
|
391
|
+
let systemPrompt = `You are the KUBUILD AI Assistant \u2014 an expert web designer, developer, and page architecture consultant.
|
|
392
|
+
Your role is to assist users with creating, designing, refining, and understanding web pages built with the KUBUILD builder.
|
|
393
|
+
Answer questions directly, concisely, and helpfully using friendly professional tone and markdown formatting.
|
|
394
|
+
|
|
395
|
+
### Available KUBUILD Components:
|
|
396
|
+
${this.catalog.map((c) => `- **${c.type}** (${c.category}): ${c.label}${c.description ? ` \u2014 ${c.description}` : ""}`).join("\n")}
|
|
397
|
+
`;
|
|
398
|
+
if (this.options.systemPromptPrefix) {
|
|
399
|
+
systemPrompt = `${this.options.systemPromptPrefix}
|
|
400
|
+
|
|
401
|
+
${systemPrompt}`;
|
|
402
|
+
}
|
|
403
|
+
if (request.systemPrompt) {
|
|
404
|
+
systemPrompt += `
|
|
405
|
+
Additional Context:
|
|
406
|
+
${request.systemPrompt}`;
|
|
407
|
+
}
|
|
408
|
+
if (request.currentDocument) {
|
|
409
|
+
const doc = request.currentDocument;
|
|
410
|
+
const sections = doc.document?.children || [];
|
|
411
|
+
const sectionSummary = sections.map((s, idx) => `Section ${idx + 1}: id="${s.id}", type="${s.type}", title="${s.props?.title || "untitled"}", childrenCount=${s.children?.length || 0}`).join("\n");
|
|
412
|
+
systemPrompt += `
|
|
413
|
+
### Current Page in Builder Canvas:
|
|
414
|
+
- Title: ${doc.metadata?.title || "Untitled Page"}
|
|
415
|
+
- Description: ${doc.metadata?.description || "N/A"}
|
|
416
|
+
- Total Sections: ${sections.length}
|
|
417
|
+
- Sections Summary:
|
|
418
|
+
${sectionSummary || "(Canvas is currently empty)"}
|
|
419
|
+
`;
|
|
420
|
+
}
|
|
421
|
+
if (request.selectedNodeId) {
|
|
422
|
+
systemPrompt += `
|
|
423
|
+
Currently Selected Component Node ID: "${request.selectedNodeId}"`;
|
|
424
|
+
}
|
|
425
|
+
const lastUserMsg = [...request.messages].reverse().find((m) => m.role === "user");
|
|
426
|
+
const userPrompt = lastUserMsg?.content || "Hello";
|
|
427
|
+
return { systemPrompt, userPrompt };
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Conversational Assistant / Q&A.
|
|
431
|
+
* Can discuss web design, explain KUBUILD components, review current page,
|
|
432
|
+
* suggest improvements, or recommend copywriting.
|
|
433
|
+
*
|
|
434
|
+
* Single request/response — unchanged since before STORA-515. See `chatStream()` for
|
|
435
|
+
* the token-level streaming variant.
|
|
436
|
+
*/
|
|
437
|
+
async chat(request, context) {
|
|
438
|
+
let rawText = "";
|
|
439
|
+
try {
|
|
440
|
+
if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
|
|
441
|
+
return {
|
|
442
|
+
success: false,
|
|
443
|
+
error: {
|
|
444
|
+
code: "INVALID_CHAT_REQUEST",
|
|
445
|
+
message: '"messages" array is required and must not be empty'
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
this.log("info", `Processing chat turn with ${request.messages.length} messages`);
|
|
450
|
+
const { systemPrompt, userPrompt } = this.buildChatPrompt(request);
|
|
451
|
+
const result = await this.options.adapter.generate({
|
|
452
|
+
systemPrompt,
|
|
453
|
+
userPrompt,
|
|
454
|
+
messages: request.messages,
|
|
455
|
+
signal: context?.signal
|
|
456
|
+
});
|
|
457
|
+
rawText = result.text.trim();
|
|
458
|
+
const assistantMessage = {
|
|
459
|
+
role: "assistant",
|
|
460
|
+
content: rawText,
|
|
461
|
+
timestamp: Date.now()
|
|
462
|
+
};
|
|
463
|
+
return {
|
|
464
|
+
success: true,
|
|
465
|
+
data: {
|
|
466
|
+
message: assistantMessage
|
|
467
|
+
},
|
|
468
|
+
usage: result.usage,
|
|
469
|
+
rawModelResponse: rawText
|
|
470
|
+
};
|
|
471
|
+
} catch (err) {
|
|
472
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
473
|
+
this.log("error", `Chat failed: ${message}`);
|
|
474
|
+
return {
|
|
475
|
+
success: false,
|
|
476
|
+
error: {
|
|
477
|
+
code: "CHAT_ERROR",
|
|
478
|
+
message
|
|
479
|
+
},
|
|
480
|
+
rawModelResponse: rawText || void 0
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Token-level streaming variant of `chat()` (STORA-515). Emits `chat-chunk` events as
|
|
486
|
+
* partial text arrives, then a terminal `chat-complete` event with the fully assembled
|
|
487
|
+
* assistant message — the same `AsyncIterable<AiStreamEvent>` mechanism `streamPage`
|
|
488
|
+
* already uses, so it can be piped over SSE by `createAiHandler`/consumed by
|
|
489
|
+
* `KubuildAiClient` with no separate transport.
|
|
490
|
+
*
|
|
491
|
+
* When the configured adapter does not implement `generateStream` (e.g. the generic
|
|
492
|
+
* custom/HTTP adapter), this falls back to a single non-streaming `generate()` call and
|
|
493
|
+
* emits the entire response as one `chat-chunk`, so callers never see an error just
|
|
494
|
+
* because the underlying provider can't stream — only a less granular update cadence.
|
|
495
|
+
*/
|
|
496
|
+
async *chatStream(request, context) {
|
|
497
|
+
try {
|
|
498
|
+
if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
|
|
499
|
+
yield {
|
|
500
|
+
type: "error",
|
|
501
|
+
error: {
|
|
502
|
+
code: "INVALID_CHAT_REQUEST",
|
|
503
|
+
message: '"messages" array is required and must not be empty'
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
this.log(
|
|
509
|
+
"info",
|
|
510
|
+
`[SSE] Starting chatStream with ${request.messages.length} messages (adapter: ${this.options.adapter.name})`
|
|
511
|
+
);
|
|
512
|
+
const { systemPrompt, userPrompt } = this.buildChatPrompt(request);
|
|
513
|
+
let accumulated = "";
|
|
514
|
+
let usage;
|
|
515
|
+
if (typeof this.options.adapter.generateStream === "function") {
|
|
516
|
+
const generator = this.options.adapter.generateStream({
|
|
517
|
+
systemPrompt,
|
|
518
|
+
userPrompt,
|
|
519
|
+
messages: request.messages,
|
|
520
|
+
signal: context?.signal
|
|
521
|
+
});
|
|
522
|
+
while (true) {
|
|
523
|
+
const { value, done } = await generator.next();
|
|
524
|
+
if (done) {
|
|
525
|
+
usage = value?.usage;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
accumulated += value;
|
|
529
|
+
yield { type: "chat-chunk", delta: value, content: accumulated };
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
this.log(
|
|
533
|
+
"debug",
|
|
534
|
+
`Adapter "${this.options.adapter.name}" has no generateStream; falling back to non-streaming chat`
|
|
535
|
+
);
|
|
536
|
+
const result = await this.options.adapter.generate({
|
|
537
|
+
systemPrompt,
|
|
538
|
+
userPrompt,
|
|
539
|
+
messages: request.messages,
|
|
540
|
+
signal: context?.signal
|
|
541
|
+
});
|
|
542
|
+
accumulated = result.text;
|
|
543
|
+
usage = result.usage;
|
|
544
|
+
yield { type: "chat-chunk", delta: accumulated, content: accumulated };
|
|
545
|
+
}
|
|
546
|
+
const assistantMessage = {
|
|
547
|
+
role: "assistant",
|
|
548
|
+
content: accumulated.trim(),
|
|
549
|
+
timestamp: Date.now()
|
|
550
|
+
};
|
|
551
|
+
this.log("info", "[SSE] Chat stream completed", usage);
|
|
552
|
+
yield { type: "chat-complete", message: assistantMessage };
|
|
553
|
+
} catch (err) {
|
|
554
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
555
|
+
this.log("error", `[SSE] Chat streaming failed: ${message}`);
|
|
556
|
+
yield {
|
|
557
|
+
type: "error",
|
|
558
|
+
error: {
|
|
559
|
+
code: "CHAT_STREAM_ERROR",
|
|
560
|
+
message
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
// src/server/handler.ts
|
|
568
|
+
async function processAiRequest(engine, body, signal) {
|
|
569
|
+
if (!body || typeof body !== "object") {
|
|
570
|
+
return {
|
|
571
|
+
status: 400,
|
|
572
|
+
response: {
|
|
573
|
+
success: false,
|
|
574
|
+
error: {
|
|
575
|
+
code: "BAD_REQUEST",
|
|
576
|
+
message: "Request body must be a valid JSON object"
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
const payload = body;
|
|
582
|
+
const mode = payload.mode || "full-page";
|
|
583
|
+
if (mode === "full-page") {
|
|
584
|
+
if (!payload.prompt || typeof payload.prompt !== "string") {
|
|
585
|
+
return {
|
|
586
|
+
status: 400,
|
|
587
|
+
response: {
|
|
588
|
+
success: false,
|
|
589
|
+
error: {
|
|
590
|
+
code: "INVALID_PROMPT",
|
|
591
|
+
message: '"prompt" is required for full-page generation'
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
const result = await engine.generatePage(
|
|
597
|
+
{
|
|
598
|
+
prompt: payload.prompt,
|
|
599
|
+
stylePreference: payload.stylePreference,
|
|
600
|
+
tone: payload.tone,
|
|
601
|
+
locale: payload.locale,
|
|
602
|
+
metadata: payload.metadata
|
|
603
|
+
},
|
|
604
|
+
{ signal }
|
|
605
|
+
);
|
|
606
|
+
return {
|
|
607
|
+
status: result.success ? 200 : 500,
|
|
608
|
+
response: result
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
if (mode === "section") {
|
|
612
|
+
if (!payload.prompt || typeof payload.prompt !== "string") {
|
|
613
|
+
return {
|
|
614
|
+
status: 400,
|
|
615
|
+
response: {
|
|
616
|
+
success: false,
|
|
617
|
+
error: {
|
|
618
|
+
code: "INVALID_PROMPT",
|
|
619
|
+
message: '"prompt" is required for section generation'
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const result = await engine.generateSection(
|
|
625
|
+
{
|
|
626
|
+
prompt: payload.prompt,
|
|
627
|
+
stylePreference: payload.stylePreference,
|
|
628
|
+
targetSectionType: payload.targetSectionType,
|
|
629
|
+
parentContext: payload.parentContext
|
|
630
|
+
},
|
|
631
|
+
{ signal }
|
|
632
|
+
);
|
|
633
|
+
return {
|
|
634
|
+
status: result.success ? 200 : 500,
|
|
635
|
+
response: result
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
if (mode === "refactor") {
|
|
639
|
+
if (!payload.node || !payload.instruction) {
|
|
640
|
+
return {
|
|
641
|
+
status: 400,
|
|
642
|
+
response: {
|
|
643
|
+
success: false,
|
|
644
|
+
error: {
|
|
645
|
+
code: "INVALID_REFACTOR_PARAMS",
|
|
646
|
+
message: '"node" and "instruction" are required for refactoring'
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const result = await engine.refactorNode(
|
|
652
|
+
{
|
|
653
|
+
node: payload.node,
|
|
654
|
+
instruction: payload.instruction,
|
|
655
|
+
stylePreference: payload.stylePreference
|
|
656
|
+
},
|
|
657
|
+
{ signal }
|
|
658
|
+
);
|
|
659
|
+
return {
|
|
660
|
+
status: result.success ? 200 : 500,
|
|
661
|
+
response: result
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
if (mode === "chat") {
|
|
665
|
+
if (!payload.messages || !Array.isArray(payload.messages) || payload.messages.length === 0) {
|
|
666
|
+
return {
|
|
667
|
+
status: 400,
|
|
668
|
+
response: {
|
|
669
|
+
success: false,
|
|
670
|
+
error: {
|
|
671
|
+
code: "INVALID_CHAT_PARAMS",
|
|
672
|
+
message: '"messages" array is required and must not be empty for chat mode'
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
const result = await engine.chat(
|
|
678
|
+
{
|
|
679
|
+
messages: payload.messages,
|
|
680
|
+
currentDocument: payload.currentDocument,
|
|
681
|
+
selectedNodeId: payload.selectedNodeId
|
|
682
|
+
},
|
|
683
|
+
{ signal }
|
|
684
|
+
);
|
|
685
|
+
return {
|
|
686
|
+
status: result.success ? 200 : 500,
|
|
687
|
+
response: result
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
status: 400,
|
|
692
|
+
response: {
|
|
693
|
+
success: false,
|
|
694
|
+
error: {
|
|
695
|
+
code: "UNKNOWN_MODE",
|
|
696
|
+
message: `Unsupported mode: ${String(mode)}. Supported modes: 'full-page', 'section', 'refactor', 'chat'`
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
function createAiHandler(engine, options) {
|
|
702
|
+
return async (request) => {
|
|
703
|
+
try {
|
|
704
|
+
if (options?.beforeRequest) {
|
|
705
|
+
const hookResult = await options.beforeRequest(request);
|
|
706
|
+
if (hookResult instanceof Response) {
|
|
707
|
+
return hookResult;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (request.method !== "POST") {
|
|
711
|
+
return new Response(
|
|
712
|
+
JSON.stringify({
|
|
713
|
+
success: false,
|
|
714
|
+
error: {
|
|
715
|
+
code: "METHOD_NOT_ALLOWED",
|
|
716
|
+
message: "Only POST requests are accepted"
|
|
717
|
+
}
|
|
718
|
+
}),
|
|
719
|
+
{
|
|
720
|
+
status: 405,
|
|
721
|
+
headers: {
|
|
722
|
+
"Content-Type": "application/json",
|
|
723
|
+
Allow: "POST"
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
const body = await request.json().catch(() => null);
|
|
729
|
+
if (body && typeof body === "object" && body.stream === true) {
|
|
730
|
+
const mode = body.mode || "full-page";
|
|
731
|
+
if (mode === "chat") {
|
|
732
|
+
if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) {
|
|
733
|
+
return new Response(
|
|
734
|
+
JSON.stringify({
|
|
735
|
+
success: false,
|
|
736
|
+
error: {
|
|
737
|
+
code: "INVALID_CHAT_PARAMS",
|
|
738
|
+
message: '"messages" array is required and must not be empty for chat mode'
|
|
739
|
+
}
|
|
740
|
+
}),
|
|
741
|
+
{
|
|
742
|
+
status: 400,
|
|
743
|
+
headers: { "Content-Type": "application/json" }
|
|
744
|
+
}
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
const encoder2 = new TextEncoder();
|
|
748
|
+
const readable2 = new ReadableStream({
|
|
749
|
+
async start(controller) {
|
|
750
|
+
try {
|
|
751
|
+
for await (const event of engine.chatStream(
|
|
752
|
+
{
|
|
753
|
+
messages: body.messages,
|
|
754
|
+
currentDocument: body.currentDocument,
|
|
755
|
+
selectedNodeId: body.selectedNodeId
|
|
756
|
+
},
|
|
757
|
+
{ signal: request.signal }
|
|
758
|
+
)) {
|
|
759
|
+
controller.enqueue(
|
|
760
|
+
encoder2.encode(`event: ${event.type}
|
|
761
|
+
data: ${JSON.stringify(event)}
|
|
762
|
+
|
|
763
|
+
`)
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
} catch (err) {
|
|
767
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
768
|
+
controller.enqueue(
|
|
769
|
+
encoder2.encode(
|
|
770
|
+
`event: error
|
|
771
|
+
data: ${JSON.stringify({ type: "error", error: { code: "CHAT_STREAM_ERROR", message } })}
|
|
772
|
+
|
|
773
|
+
`
|
|
774
|
+
)
|
|
775
|
+
);
|
|
776
|
+
} finally {
|
|
777
|
+
controller.close();
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
return new Response(readable2, {
|
|
782
|
+
status: 200,
|
|
783
|
+
headers: {
|
|
784
|
+
"Content-Type": "text/event-stream",
|
|
785
|
+
"Cache-Control": "no-cache, no-transform",
|
|
786
|
+
Connection: "keep-alive"
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
if (!body.prompt || typeof body.prompt !== "string") {
|
|
791
|
+
return new Response(
|
|
792
|
+
JSON.stringify({
|
|
793
|
+
success: false,
|
|
794
|
+
error: {
|
|
795
|
+
code: "INVALID_PROMPT",
|
|
796
|
+
message: '"prompt" is required for streaming generation'
|
|
797
|
+
}
|
|
798
|
+
}),
|
|
799
|
+
{
|
|
800
|
+
status: 400,
|
|
801
|
+
headers: { "Content-Type": "application/json" }
|
|
802
|
+
}
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
const encoder = new TextEncoder();
|
|
806
|
+
const readable = new ReadableStream({
|
|
807
|
+
async start(controller) {
|
|
808
|
+
try {
|
|
809
|
+
for await (const event of engine.streamPage(
|
|
810
|
+
{
|
|
811
|
+
prompt: body.prompt,
|
|
812
|
+
stylePreference: body.stylePreference,
|
|
813
|
+
tone: body.tone,
|
|
814
|
+
locale: body.locale,
|
|
815
|
+
metadata: body.metadata
|
|
816
|
+
},
|
|
817
|
+
{ signal: request.signal }
|
|
818
|
+
)) {
|
|
819
|
+
controller.enqueue(
|
|
820
|
+
encoder.encode(`event: ${event.type}
|
|
821
|
+
data: ${JSON.stringify(event)}
|
|
822
|
+
|
|
823
|
+
`)
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
} catch (err) {
|
|
827
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
828
|
+
controller.enqueue(
|
|
829
|
+
encoder.encode(
|
|
830
|
+
`event: error
|
|
831
|
+
data: ${JSON.stringify({ type: "error", error: { code: "STREAM_ERROR", message } })}
|
|
832
|
+
|
|
833
|
+
`
|
|
834
|
+
)
|
|
835
|
+
);
|
|
836
|
+
} finally {
|
|
837
|
+
controller.close();
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
return new Response(readable, {
|
|
842
|
+
status: 200,
|
|
843
|
+
headers: {
|
|
844
|
+
"Content-Type": "text/event-stream",
|
|
845
|
+
"Cache-Control": "no-cache, no-transform",
|
|
846
|
+
Connection: "keep-alive"
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
const { status, response } = await processAiRequest(
|
|
851
|
+
engine,
|
|
852
|
+
body,
|
|
853
|
+
request.signal
|
|
854
|
+
);
|
|
855
|
+
return new Response(JSON.stringify(response), {
|
|
856
|
+
status,
|
|
857
|
+
headers: {
|
|
858
|
+
"Content-Type": "application/json"
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
} catch (err) {
|
|
862
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
863
|
+
return new Response(
|
|
864
|
+
JSON.stringify({
|
|
865
|
+
success: false,
|
|
866
|
+
error: {
|
|
867
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
868
|
+
message
|
|
869
|
+
}
|
|
870
|
+
}),
|
|
871
|
+
{
|
|
872
|
+
status: 500,
|
|
873
|
+
headers: {
|
|
874
|
+
"Content-Type": "application/json"
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
export {
|
|
882
|
+
AnthropicAdapter,
|
|
883
|
+
CustomHttpAdapter,
|
|
884
|
+
GeminiAdapter,
|
|
885
|
+
KubuildAiEngine,
|
|
886
|
+
OpenAiAdapter,
|
|
887
|
+
createAiHandler,
|
|
888
|
+
processAiRequest
|
|
889
|
+
};
|
|
890
|
+
//# sourceMappingURL=index.js.map
|