/* global React */
const { useState, useEffect, useRef, useMemo } = React;
// ============================================================
// MCP Skills catalog
// ============================================================
const SKILLS = [
{ id: "scaffold", name: "scaffold", group: "structure", desc: { es: "Genera andamiaje base", en: "Generates base scaffolding", fr: "Génère l'échafaudage de base" } },
{ id: "ui-kit", name: "ui-kit", group: "ui", desc: { es: "Inserta componentes UI", en: "Insert UI components", fr: "Insérer des composants UI" } },
{ id: "type-pair", name: "type-pair", group: "ui", desc: { es: "Pareja tipográfica", en: "Type pairing", fr: "Appariement typo" } },
{ id: "color-palette", name: "color-palette", group: "ui", desc: { es: "Paleta armoniosa", en: "Harmonious palette", fr: "Palette harmonieuse" } },
{ id: "copywriter", name: "copywriter", group: "content", desc: { es: "Escribe textos", en: "Writes copy", fr: "Rédige les textes" } },
{ id: "seo-meta", name: "seo-meta", group: "content", desc: { es: "Metadatos SEO", en: "SEO metadata", fr: "Métadonnées SEO" } },
{ id: "auth", name: "auth", group: "data", desc: { es: "Autenticación", en: "Authentication", fr: "Authentification" } },
{ id: "db-schema", name: "db-schema", group: "data", desc: { es: "Esquema de base de datos", en: "Database schema", fr: "Schéma BDD" } },
{ id: "ai-chat", name: "ai-chat", group: "ai", desc: { es: "Chat con IA", en: "AI chat", fr: "Chat IA" } },
{ id: "ai-rag", name: "ai-rag", group: "ai", desc: { es: "Recuperación + IA", en: "Retrieval + AI", fr: "Récup. + IA" } },
{ id: "responsive", name: "responsive", group: "ui", desc: { es: "Adapta a móvil", en: "Mobile-friendly", fr: "Adapter mobile" } },
{ id: "a11y", name: "a11y", group: "quality", desc: { es: "Accesibilidad", en: "Accessibility", fr: "Accessibilité" } },
{ id: "test", name: "test", group: "quality", desc: { es: "Pruebas básicas", en: "Basic tests", fr: "Tests basiques" } },
{ id: "deploy", name: "deploy", group: "ops", desc: { es: "Despliegue", en: "Deployment", fr: "Déploiement" } },
];
const GROUP_LABELS = {
structure: { es: "Estructura", en: "Structure", fr: "Structure" },
ui: { es: "Interfaz", en: "UI", fr: "Interface" },
content: { es: "Contenido", en: "Content", fr: "Contenu" },
data: { es: "Datos", en: "Data", fr: "Données" },
ai: { es: "IA", en: "AI", fr: "IA" },
quality: { es: "Calidad", en: "Quality", fr: "Qualité" },
ops: { es: "Operación", en: "Ops", fr: "Ops" },
};
// ============================================================
// Sample code per project type — short, illustrative
// ============================================================
function makeSampleCode(state) {
const t = state.title || "my-project";
if (state.projectType === "landing") {
return [
``,
``,
`
`,
` `,
` ${t}`,
` `,
` `,
` `,
` `,
` ${state.goal || "A bold idea, ready to ship."}
`,
` ${state.desc || "Tell the world what makes this different."}
`,
` Get early access →`,
` `,
` `,
` Fast
Built for speed.
`,
` Quiet
No noise, just signal.
`,
` Yours
You own everything.
`,
` `,
` `,
` `,
``,
];
}
if (state.projectType === "ai") {
return [
`// agent/system.md`,
`// You are an assistant for "${t}".`,
`// Goal: ${state.goal || "help the user, kindly."}`,
``,
`import { stream } from "../lib/ai.js";`,
`import { tools } from "./tools.json";`,
``,
`export async function chat(messages) {`,
` const system = await load("agent/system.md");`,
` return stream({`,
` provider: "${state.aiProvider || "openai"}",`,
` system,`,
` tools,`,
` messages,`,
` });`,
`}`,
];
}
if (state.projectType === "app") {
return [
`// src/App.jsx`,
`import { Routes, Route } from "./routes/router.js";`,
`import Home from "./routes/Home.jsx";`,
`import Dashboard from "./routes/Dashboard.jsx";`,
``,
`export default function App() {`,
` return (`,
` `,
` } />`,
` } />`,
` `,
` );`,
`}`,
];
}
// site
return [
``,
``,
``,
` `,
` `,
` ${t} — home`,
` `,
` `,
` `,
` `,
` `,
` ${t}
`,
` ${state.desc || "A small site with care."}
`,
` `,
` `,
` `,
``,
];
}
// ============================================================
// Editor screen
// ============================================================
function Editor({ state, onDeploy, onBack }) {
const { t, lang } = useT();
const tree = STRUCTURES[state.projectType] || STRUCTURES.landing;
const initialFile = (tree.find(n => !n.dir) || tree[0]).name;
const [activeFile, setActiveFile] = useState(initialFile);
const [skillQuery, setSkillQuery] = useState("");
const [agentMessages, setAgentMessages] = useState(() => seedAgentMessages(state, lang));
const [agentInput, setAgentInput] = useState("");
const [agentBusy, setAgentBusy] = useState(false);
const [activeSkill, setActiveSkill] = useState(null);
const [tutor, setTutor] = useState(null);
const [tab, setTab] = useState("preview"); // preview | console
const codeLines = useMemo(() => makeSampleCode(state), [state.projectType, state.title, state.goal, state.desc, state.aiProvider]);
// Tutor surfaces depending on context
useEffect(() => {
const id = setTimeout(() => setTutor(getTutorTip(activeFile, lang)), 1400);
return () => clearTimeout(id);
}, [activeFile, lang]);
const filteredSkills = SKILLS.filter(s => s.name.includes(skillQuery.toLowerCase()) || s.group.includes(skillQuery.toLowerCase()));
const grouped = useMemo(() => {
const out = {};
for (const s of filteredSkills) {
out[s.group] = out[s.group] || [];
out[s.group].push(s);
}
return out;
}, [filteredSkills]);
const callSkill = (skill) => {
setActiveSkill(skill.id);
setAgentBusy(true);
setAgentMessages((m) => [...m, { role: "user", text: `/${skill.name}`, kind: "skill" }]);
setTimeout(() => {
setAgentMessages((m) => [...m, {
role: "agent",
text: {
es: `Aplicando \`${skill.name}\` en ${activeFile}. Editando contenido sin cambiar la estructura.`,
en: `Applying \`${skill.name}\` to ${activeFile}. Editing content without changing the structure.`,
fr: `Application de \`${skill.name}\` sur ${activeFile}. Modification du contenu sans changer la structure.`,
}[lang],
kind: "edit",
file: activeFile,
skill: skill.name,
}]);
setAgentBusy(false);
}, 1200);
};
const sendAgent = () => {
if (!agentInput.trim()) return;
setAgentMessages((m) => [...m, { role: "user", text: agentInput }]);
setAgentBusy(true);
setAgentInput("");
setTimeout(() => {
setAgentMessages((m) => [...m, {
role: "agent",
text: {
es: "Entendido. Voy a llamar las skills necesarias y editar los archivos correspondientes.",
en: "Got it. I'll call the needed skills and edit the relevant files.",
fr: "Compris. J'appelle les skills nécessaires et modifie les fichiers concernés.",
}[lang],
}]);
setAgentBusy(false);
}, 1100);
};
return (
{/* LEFT: files + skills */}
{/* CENTER: code editor */}
{codeLines.map((line, i) => (
{String(i + 1).padStart(2, "0")}
))}
{agentBusy && (
··
▍ {t.ed_thinking}
)}
utf-8 · lf · {codeLines.length} lines
edited by agent · just now
{/* RIGHT: agent + preview */}
{tutor &&
setTutor(null)} />}
);
}
function PreviewBox({ state }) {
const t = state.title || "untitled";
return (
{state.projectType === "ai" ? (
{state.goal || "How can I help today?"}
…
▍
) : state.projectType === "app" ? (
) : (
{t}
{(state.desc || "A new beginning, gently typed.").slice(0, 80)}
— continue —
)}
);
}
function ConsoleBox() {
const lines = [
"[mcp] connected · 14 skills available",
"[scaffold] generated 7 files",
"[ui-kit] inserted Button, Card, Pill",
"[type-pair] Fraunces × Geist",
"[agent] watching for edits…",
"ready.",
];
return (
{lines.map((l, i) => (
{String(10 + i).padStart(2, "0")}:42:0{i}
{l}
))}
);
}
function TutorBubble({ tip, onDismiss }) {
const { t } = useT();
return (
{t.tutor_title}
{tip.body}
{tip.skills.map((s) => (
/{s}
))}
);
}
function getTutorTip(file, lang) {
const tips = {
es: {
body: `Estás editando ${file}. Puedes invocar skills escribiendo \`/\` o haciendo clic en el panel izquierdo.`,
skills: ["ui-kit", "copywriter", "responsive"],
},
en: {
body: `You're editing ${file}. Invoke skills by typing \`/\` or clicking them on the left.`,
skills: ["ui-kit", "copywriter", "responsive"],
},
fr: {
body: `Vous éditez ${file}. Invoquez des skills en tapant \`/\` ou via le panneau de gauche.`,
skills: ["ui-kit", "copywriter", "responsive"],
},
};
return tips[lang];
}
function seedAgentMessages(state, lang) {
const greet = {
es: `Listo. Tengo "${state.title || "tu proyecto"}" cargado y la estructura de ${state.projectType}. Voy a empezar por ${defaultFirstFile(state.projectType)}.`,
en: `Ready. I loaded "${state.title || "your project"}" with the ${state.projectType} structure. Starting with ${defaultFirstFile(state.projectType)}.`,
fr: `Prêt. "${state.title || "votre projet"}" chargé avec la structure ${state.projectType}. Je commence par ${defaultFirstFile(state.projectType)}.`,
}[lang];
return [
{ role: "agent", text: greet },
{ role: "agent", kind: "edit", text: { es: "Aplicando type-pair y color-palette…", en: "Applying type-pair and color-palette…", fr: "Application de type-pair et color-palette…" }[lang], skill: "type-pair", file: defaultFirstFile(state.projectType) },
];
}
function defaultFirstFile(type) {
return ({ landing: "index.html", site: "pages/index.html", app: "src/App.jsx", ai: "agent/system.md" })[type] || "index.html";
}
// Lightweight token highlighter (regex-based, very small)
function highlight(line) {
const esc = (s) => s.replace(/&/g, "&").replace(//g, ">");
let h = esc(line);
// comments
h = h.replace(/(\/\/[^\n]*|)/g, '$1');
// strings
h = h.replace(/("[^"]*"|'[^']*')/g, '$1');
// tags
h = h.replace(/(<\/?[a-zA-Z][^&]*?>)/g, '$1');
// keywords
h = h.replace(/\b(import|from|export|default|function|const|let|return|async|await|if|else|class|new)\b/g, '$1');
return h;
}
Object.assign(window, { Editor, SKILLS });