Convert the bundled backend/internal/plugins/smartblock package into a standalone reactor-mode wasm plugin: - package smartblock -> package main; add wasip1 main.go with wasmguest.Serve(Registration). - Add go.mod (module git.dev.alexdunmow.com/block/smartblock) pinning block/core v0.18.2; no replace directives. - Flesh out plugin.mod (scope=ninja, kind=plugin, categories, tags, display_name, description). - Point web/eslint.config.js at ../../../cms/web/eslint.config.js and switch @block-ninja/ui from workspace:* to the ^0.1.0 registry version; rebuild web/dist. - Add .gitignore (*.bnp, *.wasm, web/node_modules). Behavior unchanged: registers the "smart" block; assets served from web/dist. Builds to smartblock-1.0.0.bnp; check-safety passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
444 lines
16 KiB
JavaScript
444 lines
16 KiB
JavaScript
import { importShared } from "./__federation_fn_import-hlt2XzeI.js";
|
|
import { j as jsxRuntimeExports } from "./jsx-runtime-CvJTHeKY.js";
|
|
|
|
const { useState, useCallback, useMemo } = await importShared("react");
|
|
|
|
const {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
Tabs,
|
|
TabsContent,
|
|
TabsList,
|
|
TabsTrigger,
|
|
ScrollArea,
|
|
Button,
|
|
Input,
|
|
Textarea,
|
|
Label,
|
|
Badge,
|
|
Monaco,
|
|
Loader2,
|
|
Sparkles,
|
|
Check,
|
|
Code,
|
|
History,
|
|
Wand2,
|
|
Send,
|
|
Edit3,
|
|
useAIGenerate,
|
|
useAIAssist,
|
|
} = await importShared("@block-ninja/ui");
|
|
|
|
function SmartBlockEditor({ content, onChange }) {
|
|
const [activeTab, setActiveTab] = useState("generate");
|
|
const [prompt, setPrompt] = useState("");
|
|
const [assistPrompt, setAssistPrompt] = useState("");
|
|
const [lastExplanation, setLastExplanation] = useState("");
|
|
const { generate, isLoading: isGenerating } = useAIGenerate();
|
|
const { assist, isLoading: isAssisting } = useAIAssist();
|
|
const meta = useMemo(() => {
|
|
return content._meta ?? { currentGenerationId: "", generations: [] };
|
|
}, [content._meta]);
|
|
const currentGeneration = useMemo(() => meta.generations?.find((g) => g.id === meta.currentGenerationId), [meta]);
|
|
const currentTemplate = currentGeneration?.template || "";
|
|
const contentFields = useMemo(() => {
|
|
const fields = {};
|
|
for (const [key, value] of Object.entries(content)) {
|
|
if (!key.startsWith("_")) {
|
|
fields[key] = value;
|
|
}
|
|
}
|
|
return fields;
|
|
}, [content]);
|
|
const handleGenerate = useCallback(async () => {
|
|
if (!prompt.trim()) return;
|
|
try {
|
|
const result = await generate(prompt, currentTemplate);
|
|
const newGeneration = {
|
|
id: result.generationId,
|
|
prompt,
|
|
schema: result.schema,
|
|
template: result.template,
|
|
createdAt: /* @__PURE__ */ new Date().toISOString(),
|
|
};
|
|
const updatedMeta = {
|
|
currentGenerationId: result.generationId,
|
|
generations: [...(meta.generations || []), newGeneration],
|
|
};
|
|
const newContent = { _meta: updatedMeta };
|
|
if (result.schema?.properties) {
|
|
for (const key of Object.keys(result.schema.properties)) {
|
|
newContent[key] = "";
|
|
}
|
|
}
|
|
onChange(newContent);
|
|
setLastExplanation(result.explanation);
|
|
setPrompt("");
|
|
setActiveTab("content");
|
|
} catch (error) {
|
|
console.error("Generation failed:", error);
|
|
}
|
|
}, [prompt, currentTemplate, meta, generate, onChange]);
|
|
const handleAssist = useCallback(async () => {
|
|
if (!assistPrompt.trim() || !currentTemplate) return;
|
|
try {
|
|
const result = await assist(assistPrompt, currentTemplate, currentGeneration?.schema ? JSON.stringify(currentGeneration.schema) : void 0);
|
|
const updatedGenerations = (meta.generations || []).map((g) => (g.id === meta.currentGenerationId ? { ...g, template: result.template } : g));
|
|
onChange({
|
|
...content,
|
|
_meta: { ...meta, generations: updatedGenerations },
|
|
});
|
|
setLastExplanation(result.explanation);
|
|
setAssistPrompt("");
|
|
} catch (error) {
|
|
console.error("Assist failed:", error);
|
|
}
|
|
}, [assistPrompt, currentTemplate, currentGeneration, meta, content, assist, onChange]);
|
|
const handleTemplateChange = useCallback(
|
|
(newTemplate) => {
|
|
if (!newTemplate || !currentGeneration) return;
|
|
const updatedGenerations = (meta.generations || []).map((g) => (g.id === meta.currentGenerationId ? { ...g, template: newTemplate } : g));
|
|
onChange({
|
|
...content,
|
|
_meta: { ...meta, generations: updatedGenerations },
|
|
});
|
|
},
|
|
[currentGeneration, meta, content, onChange],
|
|
);
|
|
const handleSwitchGeneration = useCallback(
|
|
(generationId) => {
|
|
const generation = meta.generations?.find((g) => g.id === generationId);
|
|
if (!generation) return;
|
|
const newContent = {
|
|
_meta: { ...meta, currentGenerationId: generationId },
|
|
};
|
|
if (generation.schema?.properties) {
|
|
for (const key of Object.keys(generation.schema.properties)) {
|
|
newContent[key] = content[key] ?? "";
|
|
}
|
|
}
|
|
onChange(newContent);
|
|
setActiveTab("content");
|
|
},
|
|
[meta, content, onChange],
|
|
);
|
|
const handleFieldChange = useCallback(
|
|
(fieldName, value) => {
|
|
onChange({ ...content, [fieldName]: value });
|
|
},
|
|
[content, onChange],
|
|
);
|
|
const renderContentFields = () => {
|
|
const schema = currentGeneration?.schema;
|
|
if (!schema?.properties) {
|
|
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", {
|
|
className: "text-center py-8 text-muted-foreground",
|
|
children: /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Generate content structure first" }),
|
|
});
|
|
}
|
|
const properties = Object.entries(schema.properties);
|
|
const requiredFields = new Set(schema.required || []);
|
|
return /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, {
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(CardHeader, {
|
|
className: "pb-2",
|
|
children: /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-sm", children: "Content Fields" }),
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, {
|
|
className: "space-y-4",
|
|
children: properties.map(([name, prop]) => {
|
|
const propObj = prop;
|
|
const isRequired = requiredFields.has(name);
|
|
const editor = propObj["x-editor"] || "text";
|
|
let inputType = "text";
|
|
if (editor === "number") {
|
|
inputType = "number";
|
|
} else if (editor === "url") {
|
|
inputType = "url";
|
|
}
|
|
return /* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
"div",
|
|
{
|
|
className: "space-y-2",
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(Label, {
|
|
htmlFor: name,
|
|
children: [
|
|
propObj.title || propObj.description || name,
|
|
isRequired && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-destructive ml-1", children: "*" }),
|
|
],
|
|
}),
|
|
editor === "textarea" ?
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(Textarea, {
|
|
id: name,
|
|
value: contentFields[name] || "",
|
|
onChange: (e) => handleFieldChange(name, e.target.value),
|
|
placeholder: propObj.description,
|
|
rows: 4,
|
|
})
|
|
: /* @__PURE__ */ jsxRuntimeExports.jsx(Input, {
|
|
id: name,
|
|
type: inputType,
|
|
value: contentFields[name] || "",
|
|
onChange: (e) => handleFieldChange(name, e.target.value),
|
|
placeholder: propObj.description,
|
|
}),
|
|
],
|
|
},
|
|
name,
|
|
);
|
|
}),
|
|
}),
|
|
],
|
|
});
|
|
};
|
|
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", {
|
|
className: "space-y-4",
|
|
children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Tabs, {
|
|
value: activeTab,
|
|
onValueChange: setActiveTab,
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsList, {
|
|
className: "grid w-full grid-cols-4",
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsTrigger, {
|
|
value: "generate",
|
|
className: "gap-2",
|
|
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Wand2, { className: "h-4 w-4" }), "Generate"],
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsTrigger, {
|
|
value: "content",
|
|
className: "gap-2",
|
|
disabled: !currentGeneration,
|
|
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Edit3, { className: "h-4 w-4" }), "Content"],
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsTrigger, {
|
|
value: "html",
|
|
className: "gap-2",
|
|
disabled: !currentGeneration,
|
|
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Code, { className: "h-4 w-4" }), "HTML"],
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsTrigger, {
|
|
value: "history",
|
|
className: "gap-2",
|
|
disabled: (meta.generations?.length || 0) === 0,
|
|
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(History, { className: "h-4 w-4" }), "History"],
|
|
}),
|
|
],
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsContent, {
|
|
value: "generate",
|
|
className: "space-y-4",
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, {
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, {
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, {
|
|
className: "flex items-center gap-2",
|
|
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { className: "h-5 w-5 text-primary" }), "Generate Content Structure"],
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, {
|
|
children: "Describe the content you want to create. AI will generate both the form fields and HTML template.",
|
|
}),
|
|
],
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, {
|
|
className: "space-y-4",
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(Textarea, {
|
|
placeholder: "E.g., Create a testimonial card with author photo, quote, name, title, and 5-star rating...",
|
|
value: prompt,
|
|
onChange: (e) => setPrompt(e.target.value),
|
|
className: "min-h-[100px]",
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
|
|
className: "flex justify-between items-center",
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx("p", {
|
|
className: "text-sm text-muted-foreground",
|
|
children: currentGeneration ? "Generating will create a new version" : "Start by describing your content",
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, {
|
|
action: "create",
|
|
entity: "smart-block-generation",
|
|
onClick: handleGenerate,
|
|
disabled: isGenerating || !prompt.trim(),
|
|
children:
|
|
isGenerating ?
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
|
|
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Generating..."],
|
|
})
|
|
: /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
|
|
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Wand2, { className: "h-4 w-4 mr-2" }), "Generate"],
|
|
}),
|
|
}),
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
lastExplanation &&
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, {
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(CardHeader, {
|
|
className: "pb-2",
|
|
children: /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-sm", children: "AI Response" }),
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, {
|
|
children: /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-muted-foreground", children: lastExplanation }),
|
|
}),
|
|
],
|
|
}),
|
|
!currentGeneration &&
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
|
|
className: "text-center py-8 text-muted-foreground",
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(Wand2, { className: "h-12 w-12 mx-auto mb-4 opacity-50" }),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "No content structure generated yet" }),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm", children: "Enter a prompt above to get started" }),
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "content", children: renderContentFields() }),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(TabsContent, {
|
|
value: "html",
|
|
className: "space-y-4",
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, {
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(CardHeader, {
|
|
className: "pb-2",
|
|
children: /* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, {
|
|
className: "text-sm flex items-center justify-between",
|
|
children: [
|
|
"HTML Template",
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, {
|
|
variant: "outline",
|
|
children: [Object.keys(currentGeneration?.schema?.properties || {}).length, " fields"],
|
|
}),
|
|
],
|
|
}),
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, {
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(Monaco, { value: currentTemplate, onChange: handleTemplateChange, language: "html", height: "300px", theme: "vs-dark" }),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", {
|
|
className: "text-xs text-muted-foreground mt-2",
|
|
children: ["Use ", "{{fieldName}}", " placeholders to insert content values"],
|
|
}),
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(Card, {
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(CardHeader, {
|
|
className: "pb-2",
|
|
children: /* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, {
|
|
className: "text-sm flex items-center gap-2",
|
|
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { className: "h-4 w-4" }), "AI Assistant"],
|
|
}),
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, {
|
|
className: "space-y-2",
|
|
children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
|
|
className: "flex gap-2",
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(Input, {
|
|
placeholder: "E.g., Make the quote italic and add a shadow...",
|
|
value: assistPrompt,
|
|
onChange: (e) => setAssistPrompt(e.target.value),
|
|
onKeyDown: (e) => {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
handleAssist();
|
|
}
|
|
},
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, {
|
|
action: "edit",
|
|
entity: "smart-block-template",
|
|
onClick: handleAssist,
|
|
disabled: isAssisting || !assistPrompt.trim(),
|
|
size: "icon",
|
|
children:
|
|
isAssisting ?
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-4 w-4 animate-spin" })
|
|
: /* @__PURE__ */ jsxRuntimeExports.jsx(Send, { className: "h-4 w-4" }),
|
|
}),
|
|
],
|
|
}),
|
|
}),
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, {
|
|
value: "history",
|
|
children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, {
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, {
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { className: "text-sm", children: "Generation History" }),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: "Switch between previous generations or regenerate from a prompt" }),
|
|
],
|
|
}),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, {
|
|
children: /* @__PURE__ */ jsxRuntimeExports.jsx(ScrollArea, {
|
|
className: "h-[300px]",
|
|
children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", {
|
|
className: "space-y-2",
|
|
children: [...(meta.generations || [])].reverse().map((gen) =>
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
Button,
|
|
{
|
|
type: "button",
|
|
variant: "ghost",
|
|
action: "select",
|
|
entity: "smart-block-generation",
|
|
className: `w-full h-auto justify-start p-3 rounded-lg border text-left font-normal whitespace-normal transition-colors hover:text-foreground [&_svg]:size-3 ${gen.id === meta.currentGenerationId ? "bg-primary/10 border-primary hover:bg-primary/10" : "hover:bg-muted"}`,
|
|
onClick: () => handleSwitchGeneration(gen.id),
|
|
children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
|
|
className: "w-full flex items-start justify-between gap-2",
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", {
|
|
className: "flex-1 min-w-0",
|
|
children: [
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm font-medium truncate", children: gen.prompt }),
|
|
/* @__PURE__ */ jsxRuntimeExports.jsx("p", {
|
|
className: "text-xs text-muted-foreground",
|
|
children: new Date(gen.createdAt).toLocaleString(),
|
|
}),
|
|
],
|
|
}),
|
|
gen.id === meta.currentGenerationId &&
|
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, {
|
|
variant: "default",
|
|
className: "shrink-0",
|
|
children: [/* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "h-3 w-3 mr-1" }), "Active"],
|
|
}),
|
|
],
|
|
}),
|
|
},
|
|
gen.id,
|
|
),
|
|
),
|
|
}),
|
|
}),
|
|
}),
|
|
],
|
|
}),
|
|
}),
|
|
],
|
|
}),
|
|
});
|
|
}
|
|
|
|
export { SmartBlockEditor as default };
|