import { FolkShape } from "./folk-shape"; import { css, html } from "./tags"; const styles = css` :host { background: var(--rs-bg-surface, #fff); color: var(--rs-text-primary, #1e293b); border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); min-width: 420px; min-height: 500px; } .header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: linear-gradient(135deg, #7c3aed, #a78bfa); color: white; border-radius: 8px 8px 0 0; font-size: 12px; font-weight: 600; cursor: move; } .header-title { display: flex; align-items: center; gap: 6px; } .header-actions { display: flex; gap: 4px; align-items: center; } .header-actions button { background: transparent; border: none; color: white; cursor: pointer; padding: 2px 6px; border-radius: 4px; font-size: 14px; } .header-actions button:hover { background: rgba(255, 255, 255, 0.2); } .state-badge { font-size: 9px; padding: 2px 6px; border-radius: 10px; text-transform: uppercase; letter-spacing: 0.5px; background: rgba(255,255,255,0.2); } .wrapper { height: 100%; overflow: hidden; display: flex; flex-direction: column; } .content { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; } .prompt-area { padding: 12px; border-bottom: 1px solid var(--rs-border, #e2e8f0); } .prompt-input { width: 100%; padding: 10px 12px; border: 2px solid var(--rs-input-border, #e2e8f0); border-radius: 8px; font-size: 13px; resize: none; outline: none; font-family: inherit; background: var(--rs-input-bg, #fff); color: var(--rs-input-text, inherit); box-sizing: border-box; } .prompt-input:focus { border-color: #7c3aed; } .btn-row { display: flex; gap: 6px; margin-top: 8px; } .btn { padding: 6px 14px; border-radius: 6px; border: none; font-size: 12px; font-weight: 600; cursor: pointer; transition: background 0.15s; } .btn-primary { background: linear-gradient(135deg, #7c3aed, #a78bfa); color: white; } .btn-primary:hover { background: linear-gradient(135deg, #6d28d9, #8b5cf6); } .btn-primary:disabled { opacity: 0.5; cursor: default; } .btn-secondary { background: var(--rs-bg-elevated, #f1f5f9); color: var(--rs-text-primary, #475569); } .btn-secondary:hover { background: var(--rs-bg-hover, #e2e8f0); } .status-area { flex: 1; overflow-y: auto; padding: 12px; font-size: 12px; min-height: 0; } .step { padding: 6px 0; border-bottom: 1px solid var(--rs-border, #f1f5f9); display: flex; align-items: flex-start; gap: 8px; } .step-icon { flex-shrink: 0; width: 18px; height: 18px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; margin-top: 1px; } .step-icon.thinking { background: #dbeafe; color: #2563eb; } .step-icon.executing { background: #fef3c7; color: #d97706; } .step-icon.done { background: #d1fae5; color: #059669; } .step-icon.error { background: #fee2e2; color: #dc2626; } .step-content { flex: 1; line-height: 1.4; } .step-tool { font-family: monospace; background: var(--rs-bg-elevated, #f1f5f9); padding: 1px 4px; border-radius: 3px; font-size: 11px; } .export-row { display: flex; gap: 6px; padding: 8px 12px; border-top: 1px solid var(--rs-border, #e2e8f0); justify-content: center; } .placeholder { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 2rem; color: #94a3b8; text-align: center; } .placeholder-icon { font-size: 2rem; } .spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid rgba(255,255,255,0.3); border-top-color: #fff; border-radius: 50%; animation: spin 0.8s linear infinite; vertical-align: middle; margin-right: 4px; } @keyframes spin { to { transform: rotate(360deg); } } `; declare global { interface HTMLElementTagNameMap { "folk-design-agent": FolkDesignAgent; } } type AgentState = "idle" | "planning" | "executing" | "verifying" | "done" | "error"; export class FolkDesignAgent extends FolkShape { static override tagName = "folk-design-agent"; static { const sheet = new CSSStyleSheet(); const parentRules = Array.from(FolkShape.styles.cssRules) .map((r) => r.cssText) .join("\n"); const childRules = Array.from(styles.cssRules) .map((r) => r.cssText) .join("\n"); sheet.replaceSync(`${parentRules}\n${childRules}`); this.styles = sheet; } #state: AgentState = "idle"; #abortController: AbortController | null = null; #promptInput: HTMLTextAreaElement | null = null; #statusArea: HTMLElement | null = null; #generateBtn: HTMLButtonElement | null = null; #stopBtn: HTMLButtonElement | null = null; #exportRow: HTMLElement | null = null; #stateBadge: HTMLElement | null = null; override createRenderRoot() { const root = super.createRenderRoot(); const wrapper = document.createElement("div"); wrapper.className = "wrapper"; wrapper.innerHTML = html`
🎯 rDesign Agent
Idle
📐 Enter a design brief to get started.
The agent will create a Scribus document step by step.
`; // Replace slot container with our wrapper const slot = root.querySelector("slot"); const containerDiv = slot?.parentElement as HTMLElement; if (containerDiv) containerDiv.replaceWith(wrapper); this.#promptInput = wrapper.querySelector(".prompt-input"); this.#statusArea = wrapper.querySelector('[data-ref="status-area"]'); this.#generateBtn = wrapper.querySelector('[data-ref="generate-btn"]'); this.#stopBtn = wrapper.querySelector('[data-ref="stop-btn"]'); this.#exportRow = wrapper.querySelector('[data-ref="export-row"]'); this.#stateBadge = wrapper.querySelector('[data-ref="state-badge"]'); // Set initial brief from attribute const brief = this.getAttribute("brief"); if (brief && this.#promptInput) this.#promptInput.value = brief; // Generate button this.#generateBtn?.addEventListener("click", (e) => { e.stopPropagation(); this.#generate(); }); // Stop button this.#stopBtn?.addEventListener("click", (e) => { e.stopPropagation(); this.#stop(); }); // Enter key this.#promptInput?.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); this.#generate(); } }); // Prevent canvas drag this.#promptInput?.addEventListener("pointerdown", (e) => e.stopPropagation()); this.#statusArea?.addEventListener("pointerdown", (e) => e.stopPropagation()); // Close button const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement; closeBtn?.addEventListener("click", (e) => { e.stopPropagation(); this.dispatchEvent(new CustomEvent("close")); }); return root; } #setState(state: AgentState) { this.#state = state; if (this.#stateBadge) this.#stateBadge.textContent = state.charAt(0).toUpperCase() + state.slice(1); const isWorking = state !== "idle" && state !== "done" && state !== "error"; if (this.#generateBtn) { this.#generateBtn.disabled = isWorking; this.#generateBtn.innerHTML = isWorking ? ' Working...' : "Generate Design"; } if (this.#stopBtn) this.#stopBtn.style.display = isWorking ? "" : "none"; if (this.#exportRow) this.#exportRow.style.display = state === "done" ? "" : "none"; if (this.#promptInput) this.#promptInput.disabled = isWorking; } #addStep(icon: string, cls: string, text: string) { if (!this.#statusArea) return; // Remove placeholder on first step const placeholder = this.#statusArea.querySelector(".placeholder"); if (placeholder) placeholder.remove(); const step = document.createElement("div"); step.className = "step"; step.innerHTML = `
${icon}
${text}
`; this.#statusArea.appendChild(step); this.#statusArea.scrollTop = this.#statusArea.scrollHeight; } async #generate() { const brief = this.#promptInput?.value.trim(); if (!brief || (this.#state !== "idle" && this.#state !== "done" && this.#state !== "error")) return; // Clear previous steps if (this.#statusArea) this.#statusArea.innerHTML = ""; this.#setState("planning"); this.#abortController = new AbortController(); try { const space = this.closest("[data-space]")?.getAttribute("data-space") || "demo"; const res = await fetch("/api/design-agent", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ brief, space }), signal: this.#abortController.signal, }); if (!res.ok || !res.body) { this.#addStep("!", "error", `Request failed: ${res.status}`); this.#setState("error"); return; } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.startsWith("data:")) { try { const data = JSON.parse(line.slice(5).trim()); this.#processEvent(data); } catch {} } } } if (this.#state !== "error") this.#setState("done"); } catch (e: any) { if (e.name !== "AbortError") { this.#addStep("!", "error", `Error: ${e.message}`); this.#setState("error"); } } this.#abortController = null; } #processEvent(data: any) { switch (data.action) { case "starting_scribus": this.#addStep("~", "thinking", data.status || "Starting Scribus..."); break; case "scribus_ready": this.#addStep("✓", "done", "Scribus ready"); break; case "thinking": this.#setState("planning"); this.#addStep("~", "thinking", data.status || "Thinking..."); break; case "executing": this.#setState("executing"); this.#addStep("▶", "executing", `${data.status || "Executing"}: ${data.tool}`); break; case "tool_result": if (data.result?.error) { this.#addStep("!", "error", `${data.tool} failed: ${data.result.error}`); } else { this.#addStep("✓", "done", `${data.tool} completed`); } break; case "verifying": this.#setState("verifying"); this.#addStep("~", "thinking", data.status || "Verifying..."); break; case "complete": this.#addStep("✓", "done", data.message || "Design complete"); break; case "done": this.#addStep("✓", "done", data.status || "Done!"); if (data.state?.frames) { this.#addStep("✓", "done", `${data.state.frames.length} frame(s) in document`); } break; case "error": this.#addStep("!", "error", data.error || "Unknown error"); this.#setState("error"); break; } } #stop() { this.#abortController?.abort(); this.#setState("idle"); this.#addStep("!", "error", "Stopped by user"); } } if (!customElements.get(FolkDesignAgent.tagName)) { customElements.define(FolkDesignAgent.tagName, FolkDesignAgent); }