dreative 0.1.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/README.md +35 -0
- package/dist/cli/index.js +169 -0
- package/dist/server/agentQueue.js +70 -0
- package/dist/server/ai.js +177 -0
- package/dist/server/diff.js +97 -0
- package/dist/server/index.js +328 -0
- package/dist/server/jobs.js +31 -0
- package/dist/server/preview.js +110 -0
- package/dist/server/store.js +71 -0
- package/dist/shared/design.js +163 -0
- package/dist/shared/types.js +1 -0
- package/dist/ui/assets/index--vztc_MR.js +71 -0
- package/dist/ui/assets/index-y0gVjC7u.css +1 -0
- package/dist/ui/index.html +13 -0
- package/package.json +44 -0
- package/skill/dreative/DESIGN.md +358 -0
- package/skill/dreative/SKILL.md +108 -0
- package/skill/dreative/skills/3d.md +131 -0
- package/skill/dreative/skills/interaction.md +126 -0
- package/skill/dreative/skills/motion.md +125 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { Store, findBlock, replaceBlock, newId } from "./store.js";
|
|
6
|
+
import { requestAgent, respond, nextEvent, pushFinish } from "./agentQueue.js";
|
|
7
|
+
import { computeDiff } from "./diff.js";
|
|
8
|
+
import { buildPreview, previewHtml, replicaHtml } from "./preview.js";
|
|
9
|
+
import { buildDesignPlan } from "../shared/design.js";
|
|
10
|
+
import { startJob, getJob } from "./jobs.js";
|
|
11
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
function cloneWithNewIds(block) {
|
|
13
|
+
return {
|
|
14
|
+
...block,
|
|
15
|
+
id: newId("blk"),
|
|
16
|
+
children: block.children?.map(cloneWithNewIds),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function createServer(projectDir) {
|
|
20
|
+
const store = new Store(projectDir);
|
|
21
|
+
const app = express();
|
|
22
|
+
app.use(express.json({ limit: "30mb" }));
|
|
23
|
+
app.get("/api/project", (_req, res) => {
|
|
24
|
+
res.json(store.load());
|
|
25
|
+
});
|
|
26
|
+
app.put("/api/project", (req, res) => {
|
|
27
|
+
store.save(req.body);
|
|
28
|
+
res.json({ ok: true });
|
|
29
|
+
});
|
|
30
|
+
app.get("/api/jobs/:id", (req, res) => {
|
|
31
|
+
const job = getJob(req.params.id);
|
|
32
|
+
if (!job)
|
|
33
|
+
return res.status(404).json({ error: "job not found" });
|
|
34
|
+
res.json(job);
|
|
35
|
+
});
|
|
36
|
+
// ---- Host-agent bridge -------------------------------------------------
|
|
37
|
+
// The coding CLI (dreative skill) long-polls for work and posts results.
|
|
38
|
+
app.get("/api/agent/next", async (_req, res) => {
|
|
39
|
+
const ev = await nextEvent(25_000);
|
|
40
|
+
if (!ev)
|
|
41
|
+
return res.status(204).end();
|
|
42
|
+
res.json(ev);
|
|
43
|
+
});
|
|
44
|
+
app.post("/api/agent/respond", (req, res) => {
|
|
45
|
+
const { id, result, error } = req.body;
|
|
46
|
+
if (!respond(id, { result, error }))
|
|
47
|
+
return res.status(404).json({ error: "request not found" });
|
|
48
|
+
res.json({ ok: true });
|
|
49
|
+
});
|
|
50
|
+
// Snapshot current project as diff baseline (agent calls this after extraction)
|
|
51
|
+
app.post("/api/baseline", (_req, res) => {
|
|
52
|
+
store.saveBaseline();
|
|
53
|
+
res.json({ ok: true });
|
|
54
|
+
});
|
|
55
|
+
// Finish: compute layout diff vs baseline and hand it to the agent
|
|
56
|
+
app.post("/api/finish", (_req, res) => {
|
|
57
|
+
const baseline = store.loadBaseline() ?? { version: 1, pages: [] };
|
|
58
|
+
const diff = computeDiff(baseline, store.load());
|
|
59
|
+
fs.writeFileSync(path.join(store.root, "finish.json"), JSON.stringify(diff, null, 2));
|
|
60
|
+
pushFinish(diff);
|
|
61
|
+
res.json({ ok: true, diff });
|
|
62
|
+
});
|
|
63
|
+
// Stage 1: prompt -> AI proposes skeleton pages placed on the canvas
|
|
64
|
+
app.post("/api/skeletons", (req, res) => {
|
|
65
|
+
const { prompt } = req.body;
|
|
66
|
+
const job = startJob("Proposing layouts", async (update) => {
|
|
67
|
+
const proposals = await requestAgent("propose-skeletons", { prompt, brief: store.load().brief }, update);
|
|
68
|
+
return store.update((p) => {
|
|
69
|
+
const baseX = Math.max(0, ...p.pages.map((pg) => pg.canvasPos.x + 480));
|
|
70
|
+
proposals.forEach((prop, i) => {
|
|
71
|
+
p.pages.push({
|
|
72
|
+
id: newId("pg"),
|
|
73
|
+
name: prop.name,
|
|
74
|
+
canvasPos: { x: baseX + i * 480, y: 40 },
|
|
75
|
+
status: "skeleton",
|
|
76
|
+
layout: prop.layout,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
res.json({ jobId: job.id });
|
|
82
|
+
});
|
|
83
|
+
// AI variants of an existing page
|
|
84
|
+
app.post("/api/pages/:pageId/variants", (req, res) => {
|
|
85
|
+
const page = store.getPage(req.params.pageId);
|
|
86
|
+
if (!page)
|
|
87
|
+
return res.status(404).json({ error: "page not found" });
|
|
88
|
+
const job = startJob(`Variants of ${page.name}`, async (update) => {
|
|
89
|
+
const proposals = await requestAgent("propose-variants", { pageName: page.name, layout: page.layout, brief: store.load().brief }, update);
|
|
90
|
+
return store.update((p) => {
|
|
91
|
+
proposals.forEach((prop, i) => {
|
|
92
|
+
p.pages.push({
|
|
93
|
+
id: newId("pg"),
|
|
94
|
+
name: prop.name,
|
|
95
|
+
canvasPos: { x: page.canvasPos.x + (i + 1) * 480, y: page.canvasPos.y + 60 },
|
|
96
|
+
status: "skeleton",
|
|
97
|
+
layout: prop.layout,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
res.json({ jobId: job.id });
|
|
103
|
+
});
|
|
104
|
+
// Duplicate a page (fresh block ids, no generated design)
|
|
105
|
+
app.post("/api/pages/:pageId/duplicate", (req, res) => {
|
|
106
|
+
const page = store.getPage(req.params.pageId);
|
|
107
|
+
if (!page)
|
|
108
|
+
return res.status(404).json({ error: "page not found" });
|
|
109
|
+
const project = store.update((p) => {
|
|
110
|
+
p.pages.push({
|
|
111
|
+
id: newId("pg"),
|
|
112
|
+
name: `${page.name} copy`,
|
|
113
|
+
canvasPos: { x: page.canvasPos.x + 60, y: page.canvasPos.y + 60 },
|
|
114
|
+
status: "skeleton",
|
|
115
|
+
refImage: page.refImage,
|
|
116
|
+
designPrompt: page.designPrompt,
|
|
117
|
+
layout: cloneWithNewIds(page.layout),
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
res.json(project);
|
|
121
|
+
});
|
|
122
|
+
// Stage 2: AI edit of a selected block (structure + functionality intents)
|
|
123
|
+
app.post("/api/pages/:pageId/blocks/:blockId/edit", (req, res) => {
|
|
124
|
+
const { pageId, blockId } = req.params;
|
|
125
|
+
const { instruction } = req.body;
|
|
126
|
+
const page = store.getPage(pageId);
|
|
127
|
+
if (!page)
|
|
128
|
+
return res.status(404).json({ error: "page not found" });
|
|
129
|
+
const block = findBlock(page.layout, blockId);
|
|
130
|
+
if (!block)
|
|
131
|
+
return res.status(404).json({ error: "block not found" });
|
|
132
|
+
const job = startJob("Editing block", async (update) => {
|
|
133
|
+
const updated = await requestAgent("edit-block", { block, instruction }, update);
|
|
134
|
+
return store.update((p) => {
|
|
135
|
+
const pg = p.pages.find((x) => x.id === pageId);
|
|
136
|
+
pg.layout = replaceBlock(pg.layout, blockId, updated);
|
|
137
|
+
if (pg.status === "designed")
|
|
138
|
+
pg.status = "skeleton"; // design is stale
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
res.json({ jobId: job.id });
|
|
142
|
+
});
|
|
143
|
+
// Block-level (or preview-element) ref image upload. Element ids in designed
|
|
144
|
+
// pages are block ids, so this covers both. Returns the stored path.
|
|
145
|
+
app.post("/api/pages/:pageId/blocks/:blockId/ref", (req, res) => {
|
|
146
|
+
const { pageId, blockId } = req.params;
|
|
147
|
+
const { name, dataBase64 } = req.body;
|
|
148
|
+
const ext = path.extname(name) || ".png";
|
|
149
|
+
const fileName = `${pageId}_${blockId}${ext}`;
|
|
150
|
+
fs.writeFileSync(store.refPath(fileName), Buffer.from(dataBase64, "base64"));
|
|
151
|
+
const rel = `refs/${fileName}`;
|
|
152
|
+
const project = store.update((p) => {
|
|
153
|
+
const pg = p.pages.find((x) => x.id === pageId);
|
|
154
|
+
const block = pg && findBlock(pg.layout, blockId);
|
|
155
|
+
if (block)
|
|
156
|
+
block.refImage = rel;
|
|
157
|
+
});
|
|
158
|
+
res.json({ ...project, refPath: rel });
|
|
159
|
+
});
|
|
160
|
+
// Ref image upload (JSON base64 keeps the client simple)
|
|
161
|
+
app.post("/api/pages/:pageId/ref", (req, res) => {
|
|
162
|
+
const { pageId } = req.params;
|
|
163
|
+
const { name, dataBase64 } = req.body;
|
|
164
|
+
const ext = path.extname(name) || ".png";
|
|
165
|
+
const fileName = `${pageId}${ext}`;
|
|
166
|
+
fs.writeFileSync(store.refPath(fileName), Buffer.from(dataBase64, "base64"));
|
|
167
|
+
const project = store.update((p) => {
|
|
168
|
+
const pg = p.pages.find((x) => x.id === pageId);
|
|
169
|
+
if (pg)
|
|
170
|
+
pg.refImage = `refs/${fileName}`;
|
|
171
|
+
});
|
|
172
|
+
res.json(project);
|
|
173
|
+
});
|
|
174
|
+
// Design one page via the agent bridge; shared by single-page design and design-all.
|
|
175
|
+
// Paths only (relative to .dreative/) — the agent reads images/previous
|
|
176
|
+
// code with its own tools and writes the .tsx file itself.
|
|
177
|
+
async function designPage(pageId, designPrompt, update) {
|
|
178
|
+
const project = store.load();
|
|
179
|
+
const page = project.pages.find((p) => p.id === pageId);
|
|
180
|
+
if (!page)
|
|
181
|
+
throw new Error("page not found");
|
|
182
|
+
const siblingPages = project.pages.filter((p) => p.id !== pageId).map((p) => p.name);
|
|
183
|
+
const blockRefs = [];
|
|
184
|
+
const collect = (b) => {
|
|
185
|
+
if (b.refImage)
|
|
186
|
+
blockRefs.push({ id: b.id, label: b.label, refImage: b.refImage });
|
|
187
|
+
b.children?.forEach(collect);
|
|
188
|
+
};
|
|
189
|
+
collect(page.layout);
|
|
190
|
+
const outFile = `generated/${pageId}.tsx`;
|
|
191
|
+
await requestAgent("design-page", {
|
|
192
|
+
pageName: page.name,
|
|
193
|
+
layout: page.layout,
|
|
194
|
+
brief: project.brief,
|
|
195
|
+
// Dreative decides, the agent executes: resolved dials, per-section
|
|
196
|
+
// layout families, spacing/motion budgets, doctrine lints.
|
|
197
|
+
plan: buildDesignPlan(page, project.brief),
|
|
198
|
+
refImage: page.refImage,
|
|
199
|
+
blockRefs,
|
|
200
|
+
designPrompt: designPrompt ?? page.designPrompt,
|
|
201
|
+
previousFile: page.generatedFile,
|
|
202
|
+
siblingPages,
|
|
203
|
+
outFile,
|
|
204
|
+
}, update);
|
|
205
|
+
if (!fs.existsSync(path.join(store.root, outFile)))
|
|
206
|
+
throw new Error(`agent did not write ${outFile}`);
|
|
207
|
+
return store.update((p) => {
|
|
208
|
+
const pg = p.pages.find((x) => x.id === pageId);
|
|
209
|
+
pg.status = "designed";
|
|
210
|
+
pg.generatedFile = outFile;
|
|
211
|
+
pg.designPrompt = designPrompt ?? pg.designPrompt;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// Stage 3: design pass (preserves prior element-level edits when regenerating)
|
|
215
|
+
app.post("/api/pages/:pageId/design", (req, res) => {
|
|
216
|
+
const { pageId } = req.params;
|
|
217
|
+
const { designPrompt } = req.body;
|
|
218
|
+
const page = store.getPage(pageId);
|
|
219
|
+
if (!page)
|
|
220
|
+
return res.status(404).json({ error: "page not found" });
|
|
221
|
+
const job = startJob(`Designing ${page.name}`, (update) => designPage(pageId, designPrompt, update));
|
|
222
|
+
res.json({ jobId: job.id });
|
|
223
|
+
});
|
|
224
|
+
// Design every page in one job (skips none — regenerates designed pages too if asked)
|
|
225
|
+
app.post("/api/design-all", (req, res) => {
|
|
226
|
+
const { onlySkeletons } = req.body;
|
|
227
|
+
const pages = store.load().pages.filter((p) => !onlySkeletons || p.status === "skeleton");
|
|
228
|
+
if (pages.length === 0)
|
|
229
|
+
return res.status(400).json({ error: "no pages to design" });
|
|
230
|
+
const job = startJob(`Designing ${pages.length} page(s)`, async (update) => {
|
|
231
|
+
let project;
|
|
232
|
+
for (let i = 0; i < pages.length; i++) {
|
|
233
|
+
const scoped = (d) => update(`${pages[i].name} (${i + 1}/${pages.length}): ${d}`);
|
|
234
|
+
project = await designPage(pages[i].id, undefined, scoped);
|
|
235
|
+
}
|
|
236
|
+
return project;
|
|
237
|
+
});
|
|
238
|
+
res.json({ jobId: job.id });
|
|
239
|
+
});
|
|
240
|
+
// Preview-mode element edit
|
|
241
|
+
app.post("/api/pages/:pageId/element/:elementId/edit", (req, res) => {
|
|
242
|
+
const { pageId, elementId } = req.params;
|
|
243
|
+
const { instruction, refPath } = req.body;
|
|
244
|
+
const page = store.getPage(pageId);
|
|
245
|
+
if (!page?.generatedFile)
|
|
246
|
+
return res.status(404).json({ error: "page not designed yet" });
|
|
247
|
+
const job = startJob("Editing element", async (update) => {
|
|
248
|
+
await requestAgent("edit-element", { file: page.generatedFile, elementId, instruction, refImage: refPath }, update);
|
|
249
|
+
return store.load();
|
|
250
|
+
});
|
|
251
|
+
res.json({ jobId: job.id });
|
|
252
|
+
});
|
|
253
|
+
// Export generated pages into the user's project
|
|
254
|
+
app.post("/api/export", (req, res) => {
|
|
255
|
+
const { dir } = req.body;
|
|
256
|
+
const target = path.resolve(projectDir, dir || "src/pages");
|
|
257
|
+
fs.mkdirSync(target, { recursive: true });
|
|
258
|
+
const exported = [];
|
|
259
|
+
for (const page of store.load().pages) {
|
|
260
|
+
if (!page.generatedFile)
|
|
261
|
+
continue;
|
|
262
|
+
const safe = page.name.replace(/[^a-zA-Z0-9]/g, "") || page.id;
|
|
263
|
+
const dest = path.join(target, `${safe}.tsx`);
|
|
264
|
+
const code = fs.readFileSync(path.join(store.root, page.generatedFile), "utf-8");
|
|
265
|
+
fs.writeFileSync(dest, `// Generated by Dreative. Requires Tailwind CSS in the host project.\n${code}`);
|
|
266
|
+
exported.push(dest);
|
|
267
|
+
}
|
|
268
|
+
res.json({ exported });
|
|
269
|
+
});
|
|
270
|
+
// Preview serving
|
|
271
|
+
app.get("/preview/:pageId", (req, res) => {
|
|
272
|
+
res.type("html").send(previewHtml(`/preview/${req.params.pageId}/bundle.js?t=${Date.now()}`));
|
|
273
|
+
});
|
|
274
|
+
app.get("/preview/:pageId/bundle.js", async (req, res) => {
|
|
275
|
+
try {
|
|
276
|
+
const page = store.getPage(req.params.pageId);
|
|
277
|
+
if (!page?.generatedFile)
|
|
278
|
+
return res.status(404).send("// not designed yet");
|
|
279
|
+
const js = await buildPreview(path.join(store.root, page.generatedFile));
|
|
280
|
+
res.type("application/javascript").send(js);
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
const msg = JSON.stringify(`Preview build error:\n${String(err)}`);
|
|
284
|
+
res
|
|
285
|
+
.type("application/javascript")
|
|
286
|
+
.send(`const pre=document.createElement("pre");pre.style.cssText="color:red;padding:16px;white-space:pre-wrap";pre.textContent=${msg};document.body.appendChild(pre);`);
|
|
287
|
+
console.error("preview build failed:", err);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
// Replica serving: stripped 1:1 copy of the real page, hover = agent summaries
|
|
291
|
+
app.get("/replica/:pageId", (req, res) => {
|
|
292
|
+
const page = store.getPage(req.params.pageId);
|
|
293
|
+
if (!page?.replicaFile)
|
|
294
|
+
return res.status(404).send("no replica for this page");
|
|
295
|
+
const summaries = {};
|
|
296
|
+
const collect = (b) => {
|
|
297
|
+
if (b.summary)
|
|
298
|
+
summaries[b.id] = b.summary;
|
|
299
|
+
b.children?.forEach(collect);
|
|
300
|
+
};
|
|
301
|
+
collect(page.layout);
|
|
302
|
+
res.type("html").send(replicaHtml(`/replica/${req.params.pageId}/bundle.js?t=${Date.now()}`, summaries));
|
|
303
|
+
});
|
|
304
|
+
app.get("/replica/:pageId/bundle.js", async (req, res) => {
|
|
305
|
+
try {
|
|
306
|
+
const page = store.getPage(req.params.pageId);
|
|
307
|
+
if (!page?.replicaFile)
|
|
308
|
+
return res.status(404).send("// no replica");
|
|
309
|
+
const js = await buildPreview(path.join(store.root, page.replicaFile));
|
|
310
|
+
res.type("application/javascript").send(js);
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
const msg = JSON.stringify(`Replica build error:\n${String(err)}`);
|
|
314
|
+
res
|
|
315
|
+
.type("application/javascript")
|
|
316
|
+
.send(`const pre=document.createElement("pre");pre.style.cssText="color:red;padding:16px;white-space:pre-wrap";pre.textContent=${msg};document.body.appendChild(pre);`);
|
|
317
|
+
console.error("replica build failed:", err);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
app.use("/refs", express.static(path.join(store.root, "refs")));
|
|
321
|
+
// Built UI (production / npx usage)
|
|
322
|
+
const uiDist = path.join(here, "..", "ui");
|
|
323
|
+
if (fs.existsSync(uiDist)) {
|
|
324
|
+
app.use(express.static(uiDist));
|
|
325
|
+
app.get("*", (_req, res) => res.sendFile(path.join(uiDist, "index.html")));
|
|
326
|
+
}
|
|
327
|
+
return app;
|
|
328
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { newId } from "./store.js";
|
|
2
|
+
const jobs = new Map();
|
|
3
|
+
export function startJob(label, work) {
|
|
4
|
+
const job = {
|
|
5
|
+
id: newId("job"),
|
|
6
|
+
label,
|
|
7
|
+
detail: "Starting…",
|
|
8
|
+
status: "running",
|
|
9
|
+
startedAt: Date.now(),
|
|
10
|
+
};
|
|
11
|
+
jobs.set(job.id, job);
|
|
12
|
+
void work((detail) => {
|
|
13
|
+
job.detail = detail;
|
|
14
|
+
})
|
|
15
|
+
.then((result) => {
|
|
16
|
+
job.status = "done";
|
|
17
|
+
job.result = result;
|
|
18
|
+
job.finishedAt = Date.now();
|
|
19
|
+
})
|
|
20
|
+
.catch((err) => {
|
|
21
|
+
job.status = "error";
|
|
22
|
+
job.error = String(err);
|
|
23
|
+
job.finishedAt = Date.now();
|
|
24
|
+
});
|
|
25
|
+
// drop finished jobs after 10 minutes
|
|
26
|
+
setTimeout(() => jobs.delete(job.id), 10 * 60 * 1000).unref?.();
|
|
27
|
+
return job;
|
|
28
|
+
}
|
|
29
|
+
export function getJob(id) {
|
|
30
|
+
return jobs.get(id);
|
|
31
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import esbuild from "esbuild";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
// Resolve react from dreative's own install, not the user's project.
|
|
7
|
+
const packageRoot = path.resolve(here, "..", "..");
|
|
8
|
+
/** Bundle a generated page component + mount shim into one iframe-ready JS file. */
|
|
9
|
+
export async function buildPreview(generatedFile) {
|
|
10
|
+
const entry = `
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { createRoot } from "react-dom/client";
|
|
13
|
+
import Page from ${JSON.stringify(generatedFile.replace(/\\/g, "/"))};
|
|
14
|
+
createRoot(document.getElementById("root")).render(React.createElement(Page));
|
|
15
|
+
`;
|
|
16
|
+
const result = await esbuild.build({
|
|
17
|
+
stdin: {
|
|
18
|
+
contents: entry,
|
|
19
|
+
resolveDir: packageRoot,
|
|
20
|
+
loader: "tsx",
|
|
21
|
+
},
|
|
22
|
+
bundle: true,
|
|
23
|
+
write: false,
|
|
24
|
+
format: "iife",
|
|
25
|
+
jsx: "automatic",
|
|
26
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
27
|
+
loader: { ".tsx": "tsx", ".ts": "ts" },
|
|
28
|
+
nodePaths: [path.join(packageRoot, "node_modules")],
|
|
29
|
+
});
|
|
30
|
+
return result.outputFiles[0].text;
|
|
31
|
+
}
|
|
32
|
+
export function previewHtml(scriptUrl) {
|
|
33
|
+
return `<!doctype html>
|
|
34
|
+
<html>
|
|
35
|
+
<head>
|
|
36
|
+
<meta charset="utf-8" />
|
|
37
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
38
|
+
<style>body{margin:0}</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<div id="root"></div>
|
|
42
|
+
<script>
|
|
43
|
+
document.addEventListener("click", (e) => {
|
|
44
|
+
const el = e.target.closest("[data-dreative-id]");
|
|
45
|
+
if (!el) return;
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
parent.postMessage({ type: "dreative-select", id: el.getAttribute("data-dreative-id") }, "*");
|
|
48
|
+
}, true);
|
|
49
|
+
document.addEventListener("mouseover", (e) => {
|
|
50
|
+
document.querySelectorAll(".dreative-hover").forEach(n => n.classList.remove("dreative-hover"));
|
|
51
|
+
const el = e.target.closest("[data-dreative-id]");
|
|
52
|
+
if (el) el.classList.add("dreative-hover");
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
<style>.dreative-hover{outline:2px dashed #6366f1;outline-offset:-2px;cursor:pointer}</style>
|
|
56
|
+
<script src="${scriptUrl}"></script>
|
|
57
|
+
</body>
|
|
58
|
+
</html>`;
|
|
59
|
+
}
|
|
60
|
+
/** Replica view: 1:1 stripped page. Interactions are inert; hover shows the
|
|
61
|
+
* agent-written summary of what each element does; click selects the block. */
|
|
62
|
+
export function replicaHtml(scriptUrl, summaries) {
|
|
63
|
+
return `<!doctype html>
|
|
64
|
+
<html>
|
|
65
|
+
<head>
|
|
66
|
+
<meta charset="utf-8" />
|
|
67
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
68
|
+
<style>body{margin:0}
|
|
69
|
+
.dreative-hover{outline:2px dashed #6366f1;outline-offset:-2px;cursor:pointer}
|
|
70
|
+
#dreative-tip{position:fixed;z-index:99999;max-width:340px;background:#16181f;color:#e8e8ea;border:1px solid #2c2f3a;border-radius:8px;padding:8px 10px;font:12px/1.45 ui-sans-serif,system-ui;pointer-events:none;display:none;box-shadow:0 8px 24px rgba(0,0,0,.4)}
|
|
71
|
+
#dreative-tip b{color:#a5b4fc}
|
|
72
|
+
</style>
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
<div id="root"></div>
|
|
76
|
+
<div id="dreative-tip"></div>
|
|
77
|
+
<script>
|
|
78
|
+
const SUMMARIES = ${JSON.stringify(summaries)};
|
|
79
|
+
// replica is a mockup: block navigation/submits, keep selection working
|
|
80
|
+
document.addEventListener("click", (e) => {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
const el = e.target.closest("[data-dreative-id]");
|
|
83
|
+
if (el) parent.postMessage({ type: "dreative-select-block", id: el.getAttribute("data-dreative-id") }, "*");
|
|
84
|
+
}, true);
|
|
85
|
+
document.addEventListener("submit", (e) => e.preventDefault(), true);
|
|
86
|
+
const tip = document.getElementById("dreative-tip");
|
|
87
|
+
document.addEventListener("mousemove", (e) => {
|
|
88
|
+
if (tip.style.display === "block") {
|
|
89
|
+
tip.style.left = Math.min(e.clientX + 14, innerWidth - 360) + "px";
|
|
90
|
+
tip.style.top = Math.min(e.clientY + 14, innerHeight - 80) + "px";
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
document.addEventListener("mouseover", (e) => {
|
|
94
|
+
document.querySelectorAll(".dreative-hover").forEach(n => n.classList.remove("dreative-hover"));
|
|
95
|
+
const el = e.target.closest("[data-dreative-id]");
|
|
96
|
+
if (!el) { tip.style.display = "none"; return; }
|
|
97
|
+
el.classList.add("dreative-hover");
|
|
98
|
+
const id = el.getAttribute("data-dreative-id");
|
|
99
|
+
const s = SUMMARIES[id];
|
|
100
|
+
if (s) { tip.innerHTML = "<b>" + id + "</b><br>" + s.replace(/</g, "<"); tip.style.display = "block"; }
|
|
101
|
+
else tip.style.display = "none";
|
|
102
|
+
});
|
|
103
|
+
</script>
|
|
104
|
+
<script src="${scriptUrl}"></script>
|
|
105
|
+
</body>
|
|
106
|
+
</html>`;
|
|
107
|
+
}
|
|
108
|
+
export function ensureDir(p) {
|
|
109
|
+
fs.mkdirSync(p, { recursive: true });
|
|
110
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export class Store {
|
|
4
|
+
root;
|
|
5
|
+
constructor(projectDir) {
|
|
6
|
+
this.root = path.join(projectDir, ".dreative");
|
|
7
|
+
fs.mkdirSync(path.join(this.root, "refs"), { recursive: true });
|
|
8
|
+
fs.mkdirSync(path.join(this.root, "generated"), { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
get file() {
|
|
11
|
+
return path.join(this.root, "project.json");
|
|
12
|
+
}
|
|
13
|
+
load() {
|
|
14
|
+
if (!fs.existsSync(this.file))
|
|
15
|
+
return { version: 1, pages: [] };
|
|
16
|
+
return JSON.parse(fs.readFileSync(this.file, "utf-8"));
|
|
17
|
+
}
|
|
18
|
+
save(project) {
|
|
19
|
+
fs.writeFileSync(this.file, JSON.stringify(project, null, 2));
|
|
20
|
+
}
|
|
21
|
+
update(fn) {
|
|
22
|
+
const project = this.load();
|
|
23
|
+
fn(project);
|
|
24
|
+
this.save(project);
|
|
25
|
+
return project;
|
|
26
|
+
}
|
|
27
|
+
get baselineFile() {
|
|
28
|
+
return path.join(this.root, "baseline.json");
|
|
29
|
+
}
|
|
30
|
+
/** Snapshot the current project as the diff baseline (taken after extraction). */
|
|
31
|
+
saveBaseline() {
|
|
32
|
+
fs.writeFileSync(this.baselineFile, JSON.stringify(this.load(), null, 2));
|
|
33
|
+
}
|
|
34
|
+
loadBaseline() {
|
|
35
|
+
if (!fs.existsSync(this.baselineFile))
|
|
36
|
+
return undefined;
|
|
37
|
+
return JSON.parse(fs.readFileSync(this.baselineFile, "utf-8"));
|
|
38
|
+
}
|
|
39
|
+
getPage(pageId) {
|
|
40
|
+
return this.load().pages.find((p) => p.id === pageId);
|
|
41
|
+
}
|
|
42
|
+
refPath(fileName) {
|
|
43
|
+
return path.join(this.root, "refs", fileName);
|
|
44
|
+
}
|
|
45
|
+
generatedPath(fileName) {
|
|
46
|
+
return path.join(this.root, "generated", fileName);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function findBlock(root, id) {
|
|
50
|
+
if (root.id === id)
|
|
51
|
+
return root;
|
|
52
|
+
for (const child of root.children ?? []) {
|
|
53
|
+
const found = findBlock(child, id);
|
|
54
|
+
if (found)
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
export function replaceBlock(root, id, replacement) {
|
|
60
|
+
if (root.id === id)
|
|
61
|
+
return replacement;
|
|
62
|
+
return {
|
|
63
|
+
...root,
|
|
64
|
+
children: root.children?.map((c) => replaceBlock(c, id, replacement)),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
let counter = 0;
|
|
68
|
+
export function newId(prefix) {
|
|
69
|
+
counter += 1;
|
|
70
|
+
return `${prefix}_${Date.now().toString(36)}${counter}`;
|
|
71
|
+
}
|