Spaces:
Sleeping
Sleeping
| /** | |
| * ArtifactExtractor.ts — Parse agent LLM responses to extract workspace artifacts (FASE 7) | |
| * | |
| * Detects code blocks, HTML pages, markdown headers, JSON blobs, CSV tables | |
| * in raw LLM text output and converts them to Artifact objects ready for storage. | |
| * | |
| * Heuristics (in priority order): | |
| * 1. Fenced code blocks with explicit language tag: ```python … ``` | |
| * 2. ```html … ``` → html kind | |
| * 3. ```json … ``` → json kind | |
| * 4. ```csv … ``` → csv kind | |
| * 5. ```svg … ``` → svg kind | |
| * 6. ```markdown … ``` or ``` … ``` that looks like markdown → markdown kind | |
| * 7. Long code block without tag (≥10 lines) → code / text kind | |
| */ | |
| import type { Artifact, ArtifactKind } from "./ArtifactStore"; | |
| // ── Language → kind mapping ─────────────────────────────────────────────────── | |
| const CODE_LANGS = new Set([ | |
| "python","py","javascript","js","typescript","ts","jsx","tsx", | |
| "rust","go","java","c","cpp","c++","c#","csharp","bash","sh","shell", | |
| "ruby","rb","php","swift","kotlin","r","scala","dart","lua", | |
| "yaml","toml","sql","graphql","regex", | |
| ]); | |
| function langToKind(lang: string): ArtifactKind { | |
| const l = lang.toLowerCase().trim(); | |
| if (l === "html") return "html"; | |
| if (l === "json") return "json"; | |
| if (l === "csv") return "csv"; | |
| if (l === "svg") return "svg"; | |
| if (l === "markdown" || l === "md") return "markdown"; | |
| if (CODE_LANGS.has(l)) return "code"; | |
| return "code"; // default | |
| } | |
| function inferKindFromContent(content: string): ArtifactKind { | |
| const trimmed = content.trim(); | |
| if (trimmed.startsWith("<html") || (trimmed.startsWith("<!") && /doctype/i.test(trimmed))) return "html"; | |
| if (trimmed.startsWith("{") || trimmed.startsWith("[")) { | |
| try { JSON.parse(trimmed); return "json"; } catch { /* not json */ } | |
| } | |
| if (trimmed.startsWith("<svg")) return "svg"; | |
| const lines = trimmed.split("\n"); | |
| const hasCommas = lines.slice(0, 5).every(l => l.includes(",")); | |
| if (hasCommas && lines.length >= 2) return "csv"; | |
| if (/^#{1,6} /m.test(trimmed)) return "markdown"; | |
| return "code"; | |
| } | |
| // ── Title inference ─────────────────────────────────────────────────────────── | |
| const TITLE_MAX = 48; | |
| function inferTitle(kind: ArtifactKind, language: string | undefined, content: string, idx: number): string { | |
| // Try to extract a meaningful name from the content | |
| const lines = content.trim().split("\n"); | |
| if (kind === "html") { | |
| const m = content.match(/<title>([^<]+)<\/title>/i); | |
| if (m) return m[1].trim().slice(0, TITLE_MAX); | |
| const h1 = content.match(/<h1[^>]*>([^<]+)<\/h1>/i); | |
| if (h1) return h1[1].trim().slice(0, TITLE_MAX); | |
| return "Pagina HTML"; | |
| } | |
| if (kind === "json") { | |
| try { | |
| const j = JSON.parse(content.trim()); | |
| if (j.name) return String(j.name).slice(0, TITLE_MAX); | |
| if (j.title) return String(j.title).slice(0, TITLE_MAX); | |
| } catch { /* ok */ } | |
| return "Dati JSON"; | |
| } | |
| if (kind === "csv") { | |
| const header = lines[0].replace(/,/g, " / ").trim(); | |
| return header.slice(0, TITLE_MAX) || "Tabella CSV"; | |
| } | |
| if (kind === "svg") return "Immagine SVG"; | |
| if (kind === "markdown") { | |
| const h1 = content.match(/^#{1,3} (.+)/m); | |
| if (h1) return h1[1].trim().slice(0, TITLE_MAX); | |
| return "Documento"; | |
| } | |
| // code — look for function name, class name, filename comment | |
| const commentFile = content.match(/^#\s+(?:file:\s*)?([a-zA-Z0-9_.\-/]+\.[a-z]{1,5})/m); | |
| if (commentFile) return commentFile[1].slice(0, TITLE_MAX); | |
| const fnName = content.match(/^(?:def|function|fn|class|func)\s+(\w+)/m); | |
| if (fnName) return `${language ?? "codice"}: ${fnName[1]}`.slice(0, TITLE_MAX); | |
| return `${language ?? kind} snippet ${idx + 1}`; | |
| } | |
| // ── Unique IDs ──────────────────────────────────────────────────────────────── | |
| let _counter = 0; | |
| function shortId(): string { | |
| return `${Date.now().toString(36)}_${(++_counter).toString(36)}`; | |
| } | |
| // ── Fenced code block regex ─────────────────────────────────────────────────── | |
| const FENCE_RE = /```([a-zA-Z0-9_+#\-.]*)\n([\s\S]*?)```/g; | |
| // ── Main extraction function ────────────────────────────────────────────────── | |
| export interface ExtractOptions { | |
| sessionId?: string; | |
| messageId?: string; | |
| minLines?: number; // minimum lines to consider an artifact (default 4) | |
| } | |
| export function extractArtifacts( | |
| text: string, | |
| opts: ExtractOptions = {}, | |
| ): Artifact[] { | |
| const { sessionId, messageId, minLines = 4 } = opts; | |
| const now = Date.now(); | |
| const found: Artifact[] = []; | |
| let idx = 0; | |
| let match: RegExpExecArray | null; | |
| FENCE_RE.lastIndex = 0; | |
| while ((match = FENCE_RE.exec(text)) !== null) { | |
| const rawLang = (match[1] ?? "").toLowerCase().trim(); | |
| const content = match[2].trim(); | |
| if (!content || content.split("\n").length < minLines) continue; | |
| const language = rawLang || undefined; | |
| const kind = rawLang ? langToKind(rawLang) : inferKindFromContent(content); | |
| const title = inferTitle(kind, language, content, idx); | |
| found.push({ | |
| id: shortId(), | |
| kind, | |
| language, | |
| title, | |
| content, | |
| sessionId, | |
| messageId, | |
| createdAt: now, | |
| updatedAt: now, | |
| version: 1, | |
| pinned: false, | |
| tags: language ? [language] : [], | |
| }); | |
| idx++; | |
| } | |
| return found; | |
| } | |
| /** | |
| * Quick check — does this text contain anything worth extracting? | |
| * Cheap scan before running full extraction. | |
| */ | |
| export function hasExtractableContent(text: string): boolean { | |
| return FENCE_RE.test(text); | |
| } | |