/** * Smart Block Editor * * AI-powered block editor that generates content structure and HTML templates * from natural language prompts. */ import React, { useState, useCallback, useMemo } from "react"; import { // Types type BlockEditorProps, type Generation, type SmartBlockMeta, // Components Card, CardContent, CardDescription, CardHeader, CardTitle, Tabs, TabsContent, TabsList, TabsTrigger, ScrollArea, Button, Input, Textarea, Label, Badge, Monaco, Loader2, Sparkles, Check, Code, History, Wand2, Send, Edit3, // Hooks useAIGenerate, useAIAssist, } from "@block-ninja/ui"; function SmartBlockEditor({ content, onChange }: BlockEditorProps) { 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(); // Extract meta and current generation const meta = useMemo(() => { return (content._meta as SmartBlockMeta) ?? { currentGenerationId: "", generations: [] }; }, [content._meta]); const currentGeneration = useMemo(() => meta.generations?.find((g: Generation) => g.id === meta.currentGenerationId), [meta]); const currentTemplate = currentGeneration?.template || ""; // Get content fields (excluding _meta) const contentFields = useMemo(() => { const fields: Record = {}; for (const [key, value] of Object.entries(content)) { if (!key.startsWith("_")) { fields[key] = value; } } return fields; }, [content]); // Handle generate const handleGenerate = useCallback(async () => { if (!prompt.trim()) return; try { const result = await generate(prompt, currentTemplate); const newGeneration: Generation = { id: result.generationId, prompt: prompt, schema: result.schema, template: result.template, createdAt: new Date().toISOString(), }; const updatedMeta: SmartBlockMeta = { currentGenerationId: result.generationId, generations: [...(meta.generations || []), newGeneration], }; // Clear old content fields and add new schema defaults const newContent: Record = { _meta: updatedMeta }; if (result.schema?.properties) { for (const key of Object.keys(result.schema.properties as Record)) { newContent[key] = ""; } } onChange(newContent); setLastExplanation(result.explanation); setPrompt(""); setActiveTab("content"); } catch (error) { console.error("Generation failed:", error); } }, [prompt, currentTemplate, meta, generate, onChange]); // Handle assist const handleAssist = useCallback(async () => { if (!assistPrompt.trim() || !currentTemplate) return; try { const result = await assist(assistPrompt, currentTemplate, currentGeneration?.schema ? JSON.stringify(currentGeneration.schema) : undefined); // Update the current generation's template const updatedGenerations = (meta.generations || []).map((g: Generation) => (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]); // Handle template change (direct edit) const handleTemplateChange = useCallback( (newTemplate: string | undefined) => { if (!newTemplate || !currentGeneration) return; const updatedGenerations = (meta.generations || []).map((g: Generation) => (g.id === meta.currentGenerationId ? { ...g, template: newTemplate } : g)); onChange({ ...content, _meta: { ...meta, generations: updatedGenerations }, }); }, [currentGeneration, meta, content, onChange], ); // Handle switching generation const handleSwitchGeneration = useCallback( (generationId: string) => { const generation = meta.generations?.find((g: Generation) => g.id === generationId); if (!generation) return; // Clear current content and set defaults from new schema const newContent: Record = { _meta: { ...meta, currentGenerationId: generationId }, }; if (generation.schema?.properties) { for (const key of Object.keys(generation.schema.properties as Record)) { newContent[key] = content[key] ?? ""; } } onChange(newContent); setActiveTab("content"); }, [meta, content, onChange], ); // Handle content field change const handleFieldChange = useCallback( (fieldName: string, value: unknown) => { onChange({ ...content, [fieldName]: value }); }, [content, onChange], ); // Render content fields based on schema const renderContentFields = () => { const schema = currentGeneration?.schema as Record | undefined; if (!schema?.properties) { return (

Generate content structure first

); } const properties = Object.entries(schema.properties as Record); const requiredFields = new Set((schema.required as string[]) || []); return ( Content Fields {properties.map(([name, prop]) => { const propObj = prop as Record; const isRequired = requiredFields.has(name); const editor = (propObj["x-editor"] as string) || "text"; let inputType = "text"; if (editor === "number") { inputType = "number"; } else if (editor === "url") { inputType = "url"; } return (
{editor === "textarea" ?