{"repo": "banjito/active-website-software", "files": {"supabase/functions/generate-form-template/index.ts": {"2aeb17bb552d11757a33bf77cb9c8ab69b819d9b": {"t": 1785745596.3615165, "n": 9647, "h": "a546acb3d03f2287", "c": "/// \n// @ts-ignore deno: types are resolved at runtime\nimport { serve } from \"https://deno.land/std@0.168.0/http/server.ts\";\n// Local TS linting shim (for non-Deno editors)\ndeclare const Deno: {\n env: { get: (name: string) => string | undefined };\n};\n\nconst corsHeaders = {\n \"Access-Control-Allow-Origin\": \"*\",\n \"Access-Control-Allow-Headers\":\n \"authorization, x-client-info, apikey, content-type\",\n};\n\n// DeepSeek (OpenAI-compatible API). \"deepseek-chat\" (V3) is fast + supports\n// JSON mode; switch to \"deepseek-reasoner\" (R1) for harder mapping — but note\n// the reasoner does NOT support response_format json_object, so drop that below.\nconst MODEL = \"deepseek-chat\";\nconst DEEPSEEK_URL = \"https://api.deepseek.com/chat/completions\";\n\n// ---------------------------------------------------------------------------\n// The template schema the model must emit. Kept in sync with\n// src/lib/types/customForms.ts — only the fields the builder actually reads.\n// ---------------------------------------------------------------------------\nconst SCHEMA = `You output a CustomFormTemplate. TypeScript shape of the fields you may use:\n\ninterface CustomFormTemplate {\n name: string; // report title, e.g. \"3-Set Low Voltage Cable Test Report (ATS)\"\n description?: string;\n netaSection?: string; // e.g. \"7.3.3\"\n structure: {\n sections: SectionConfig[];\n settings: { includePassFail: boolean; includeJobInfo: boolean; includePrintHeader: boolean; pageBreakAfterSection?: boolean };\n };\n}\n\ninterface SectionConfig {\n id: string; // unique slug, e.g. \"job-info\", \"electrical-tests\"\n componentType: ComponentType;\n title: string;\n order: number; // 0-based render order\n showInPrint: boolean; // almost always true\n referenceCode?: string; // short code used in formulas: JD (Job Details), ETI, VAM, TEU, COM, etc.\n // table components:\n columns?: ColumnConfig[];\n rows?: number;\n allowAddRows?: boolean;\n allowRemoveRows?: boolean;\n // single-field components (comments):\n field?: FieldConfig;\n // grouped-field components (nameplate / custom table label strip / test equipment):\n fields?: FieldConfig[];\n layout?: 'single-column'|'two-column'|'three-column'|'four-column'|'five-column'|'grid';\n // visual & mechanical inspection:\n checklistItems?: { id: string; netaSection?: string; description: string; resultOptions: string[] }[];\n}\n\ninterface ColumnConfig { id: string; label: string; field: FieldConfig; width?: string }\n\ninterface FieldConfig {\n id: string;\n label: string;\n type: FieldType;\n required?: boolean;\n readOnly?: boolean;\n options?: { label: string; value: string }[]; // for SELECT\n unit?: string;\n unitOptions?: string[];\n cellBehavior?: 'user'|'populate'|'calculate'; // table cells\n calculation?: { formula: string; dependsOn: string[] }; // when cellBehavior='calculate'\n}\n\nenum FieldType { TEXT='text', NUMBER='number', DATE='date', SELECT='select', TEXTAREA='textarea', CHECKBOX='checkbox', CALCULATED='calculated', TEMPERATURE_HUMIDITY='temperature-humidity' }\n\nComponentType is one of the ids given in the COMPONENT CATALOG below.`;\n\nconst INSTRUCTIONS = `You convert a hard-coded electrical test report (a React .tsx file) into a Custom Form Builder template (JSON).\n\nRules:\n1. Read the report source and reproduce its sections IN RENDER ORDER (the order of the
headings / JSX blocks).\n2. Start with a Job Information section (componentType \"job-info\", referenceCode \"JD\") unless the report clearly has none. Use the catalog default for it.\n3. For every other section pick the CLOSEST componentType from the COMPONENT CATALOG. Use a catalog entry's defaultConfig as your starting point, then adapt titles/columns/rows/options to match the report. Only fall back to \"custom-table\" when nothing else fits (e.g. a label→value strip of nameplate/cable data: N columns, 1 row, allowAddRows/allowRemoveRows false).\n4. Reproduce dropdown options exactly from the report's *_OPTIONS arrays. Inspection result options usually are: Satisfactory, Unsatisfactory, Cleaned, See Comments, Not Applicable.\n5. Row counts: use the report's number of test sets/rows (Array.from({length:N}) / TOTAL_ROWS). Fixed tables set allowAddRows=false, allowRemoveRows=false.\n6. Temperature-corrected columns: set the reading columns first (positional order matters), then each corrected column uses cellBehavior \"calculate\" with calculation.formula referencing the reading column by position and the TCF, e.g. \"{ETI.C5}*{JD.tcf}\". {REF.Cn} = column n of the section with referenceCode REF (1-based). Lowercase {JD.tcf}.\n7. Always include a Test Equipment Used section (componentType \"test-equipment\") and a Comments section (componentType \"comments\") near the end if the report has them.\n8. Give every section a stable, unique kebab-case id and a unique referenceCode.\n\nOutput ONLY the JSON object for the CustomFormTemplate — no prose, no markdown fences, no commentary.`;\n\nserve(async (req) => {\n if (req.method === \"OPTIONS\") {\n return new Response(\"ok\", { headers: corsHeaders });\n }\n\n try {\n if (req.method !== \"POST\") {\n return json({ error: \"Method not allowed\" }, 405);\n }\n\n const apiKey = Deno.env.get(\"DEEPSEEK_API_KEY\");\n if (!apiKey) throw new Error(\"DEEPSEEK_API_KEY is not set\");\n\n const { reportName, reportSource, componentCatalog } = await req.json();\n if (!reportSource || typeof reportSource !== \"string\") {\n return json({ error: \"reportSource (string) is required\" }, 400);\n }\n if (!componentCatalog) {\n return json({ error: \"componentCatalog is required\" }, 400);\n }\n\n const catalogText =\n typeof componentCatalog === \"string\"\n ? componentCatalog\n : JSON.stringify(componentCatalog);\n\n const userText = `Report file: ${reportName || \"unknown.tsx\"}\n\nCOMPONENT CATALOG (available componentTypes and their default configs):\n${catalogText}\n\nREPORT SOURCE:\n\\`\\`\\`tsx\n${reportSource}\n\\`\\`\\`\n\nGenerate the CustomFormTemplate JSON now.`;\n\n const body = {\n model: MODEL,\n max_tokens: 8192, // deepseek-chat output ceiling\n stream: true,\n temperature: 0,\n response_format: { type: \"json_object\" },\n messages: [\n { role: \"system\", content: `${INSTRUCTIONS}\\n\\n${SCHEMA}` },\n { role: \"user\", content: userText },\n ],\n };\n\n const resp = await fetch(DEEPSEEK_URL, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n if (!resp.ok || !resp.body) {\n const errText = await resp.text();\n console.error(\"DeepSeek API error:\", resp.status, errText);\n return json(\n { error: `DeepSeek API error (${resp.status})`, detail: errText },\n 502,\n );\n }\n\n // Accumulate text_delta events from the SSE stream.\n const rawText = await collectStreamedText(resp.body);\n const template = parseTemplate(rawText);\n if (!template) {\n console.error(\"Could not parse template JSON. Raw head:\", rawText.slice(0, 500));\n return json(\n { error: \"Model did not return valid template JSON\", raw: rawText.slice(0, 2000) },\n 502,\n );\n }\n\n return json({ template });\n } catch (err) {\n console.error(\"generate-form-template failed:\", err);\n return json({ error: String((err as Error).message || err) }, 500);\n }\n});\n\nfunction json(payload: unknown, status = 200): Response {\n return new Response(JSON.stringify(payload), {\n status,\n headers: { ...corsHeaders, \"content-type\": \"application/json\" },\n });\n}\n\n/** Read the DeepSeek (OpenAI-format) SSE stream and concatenate delta content. */\nasync function collectStreamedText(\n stream: ReadableStream,\n): Promise {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n let out = \"\";\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n // SSE events are separated by a blank line.\n let sep;\n while ((sep = buffer.indexOf(\"\\n\\n\")) !== -1) {\n const event = buffer.slice(0, sep);\n buffer = buffer.slice(sep + 2);\n for (const line of event.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) continue;\n const data = trimmed.slice(5).trim();\n if (!data || data === \"[DONE]\") continue;\n try {\n const parsed = JSON.parse(data);\n // Ignore reasoning_content (deepseek-reasoner) — keep only answer text.\n const delta = parsed.choices?.[0]?.delta?.content;\n if (typeof delta === \"string\") out += delta;\n } catch {\n // ignore keep-alive / non-JSON lines\n }\n }\n }\n }\n return out;\n}\n\n/** Extract and parse the JSON template object from the model's raw output. */\nfunction parseTemplate(raw: string): Record | null {\n let text = raw.trim();\n // Strip markdown fences if the model added them despite instructions.\n const fence = text.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\n if (fence) text = fence[1].trim();\n // Fall back to the first {...} span.\n if (!text.startsWith(\"{\")) {\n const first = text.indexOf(\"{\");\n const last = text.lastIndexOf(\"}\");\n if (first === -1 || last === -1) return null;\n text = text.slice(first, last + 1);\n }\n try {\n const obj = JSON.parse(text);\n if (obj && typeof obj === \"object\" && obj.structure) return obj;\n return null;\n } catch {\n return null;\n }\n}\n"}, "ffff9250d6dd1af9378cccb87f60d605be87a0ba": {"t": 1785754615.2484007, "n": 9647, "h": "a546acb3d03f2287", "c": "/// \n// @ts-ignore deno: types are resolved at runtime\nimport { serve } from \"https://deno.land/std@0.168.0/http/server.ts\";\n// Local TS linting shim (for non-Deno editors)\ndeclare const Deno: {\n env: { get: (name: string) => string | undefined };\n};\n\nconst corsHeaders = {\n \"Access-Control-Allow-Origin\": \"*\",\n \"Access-Control-Allow-Headers\":\n \"authorization, x-client-info, apikey, content-type\",\n};\n\n// DeepSeek (OpenAI-compatible API). \"deepseek-chat\" (V3) is fast + supports\n// JSON mode; switch to \"deepseek-reasoner\" (R1) for harder mapping — but note\n// the reasoner does NOT support response_format json_object, so drop that below.\nconst MODEL = \"deepseek-chat\";\nconst DEEPSEEK_URL = \"https://api.deepseek.com/chat/completions\";\n\n// ---------------------------------------------------------------------------\n// The template schema the model must emit. Kept in sync with\n// src/lib/types/customForms.ts — only the fields the builder actually reads.\n// ---------------------------------------------------------------------------\nconst SCHEMA = `You output a CustomFormTemplate. TypeScript shape of the fields you may use:\n\ninterface CustomFormTemplate {\n name: string; // report title, e.g. \"3-Set Low Voltage Cable Test Report (ATS)\"\n description?: string;\n netaSection?: string; // e.g. \"7.3.3\"\n structure: {\n sections: SectionConfig[];\n settings: { includePassFail: boolean; includeJobInfo: boolean; includePrintHeader: boolean; pageBreakAfterSection?: boolean };\n };\n}\n\ninterface SectionConfig {\n id: string; // unique slug, e.g. \"job-info\", \"electrical-tests\"\n componentType: ComponentType;\n title: string;\n order: number; // 0-based render order\n showInPrint: boolean; // almost always true\n referenceCode?: string; // short code used in formulas: JD (Job Details), ETI, VAM, TEU, COM, etc.\n // table components:\n columns?: ColumnConfig[];\n rows?: number;\n allowAddRows?: boolean;\n allowRemoveRows?: boolean;\n // single-field components (comments):\n field?: FieldConfig;\n // grouped-field components (nameplate / custom table label strip / test equipment):\n fields?: FieldConfig[];\n layout?: 'single-column'|'two-column'|'three-column'|'four-column'|'five-column'|'grid';\n // visual & mechanical inspection:\n checklistItems?: { id: string; netaSection?: string; description: string; resultOptions: string[] }[];\n}\n\ninterface ColumnConfig { id: string; label: string; field: FieldConfig; width?: string }\n\ninterface FieldConfig {\n id: string;\n label: string;\n type: FieldType;\n required?: boolean;\n readOnly?: boolean;\n options?: { label: string; value: string }[]; // for SELECT\n unit?: string;\n unitOptions?: string[];\n cellBehavior?: 'user'|'populate'|'calculate'; // table cells\n calculation?: { formula: string; dependsOn: string[] }; // when cellBehavior='calculate'\n}\n\nenum FieldType { TEXT='text', NUMBER='number', DATE='date', SELECT='select', TEXTAREA='textarea', CHECKBOX='checkbox', CALCULATED='calculated', TEMPERATURE_HUMIDITY='temperature-humidity' }\n\nComponentType is one of the ids given in the COMPONENT CATALOG below.`;\n\nconst INSTRUCTIONS = `You convert a hard-coded electrical test report (a React .tsx file) into a Custom Form Builder template (JSON).\n\nRules:\n1. Read the report source and reproduce its sections IN RENDER ORDER (the order of the
headings / JSX blocks).\n2. Start with a Job Information section (componentType \"job-info\", referenceCode \"JD\") unless the report clearly has none. Use the catalog default for it.\n3. For every other section pick the CLOSEST componentType from the COMPONENT CATALOG. Use a catalog entry's defaultConfig as your starting point, then adapt titles/columns/rows/options to match the report. Only fall back to \"custom-table\" when nothing else fits (e.g. a label→value strip of nameplate/cable data: N columns, 1 row, allowAddRows/allowRemoveRows false).\n4. Reproduce dropdown options exactly from the report's *_OPTIONS arrays. Inspection result options usually are: Satisfactory, Unsatisfactory, Cleaned, See Comments, Not Applicable.\n5. Row counts: use the report's number of test sets/rows (Array.from({length:N}) / TOTAL_ROWS). Fixed tables set allowAddRows=false, allowRemoveRows=false.\n6. Temperature-corrected columns: set the reading columns first (positional order matters), then each corrected column uses cellBehavior \"calculate\" with calculation.formula referencing the reading column by position and the TCF, e.g. \"{ETI.C5}*{JD.tcf}\". {REF.Cn} = column n of the section with referenceCode REF (1-based). Lowercase {JD.tcf}.\n7. Always include a Test Equipment Used section (componentType \"test-equipment\") and a Comments section (componentType \"comments\") near the end if the report has them.\n8. Give every section a stable, unique kebab-case id and a unique referenceCode.\n\nOutput ONLY the JSON object for the CustomFormTemplate — no prose, no markdown fences, no commentary.`;\n\nserve(async (req) => {\n if (req.method === \"OPTIONS\") {\n return new Response(\"ok\", { headers: corsHeaders });\n }\n\n try {\n if (req.method !== \"POST\") {\n return json({ error: \"Method not allowed\" }, 405);\n }\n\n const apiKey = Deno.env.get(\"DEEPSEEK_API_KEY\");\n if (!apiKey) throw new Error(\"DEEPSEEK_API_KEY is not set\");\n\n const { reportName, reportSource, componentCatalog } = await req.json();\n if (!reportSource || typeof reportSource !== \"string\") {\n return json({ error: \"reportSource (string) is required\" }, 400);\n }\n if (!componentCatalog) {\n return json({ error: \"componentCatalog is required\" }, 400);\n }\n\n const catalogText =\n typeof componentCatalog === \"string\"\n ? componentCatalog\n : JSON.stringify(componentCatalog);\n\n const userText = `Report file: ${reportName || \"unknown.tsx\"}\n\nCOMPONENT CATALOG (available componentTypes and their default configs):\n${catalogText}\n\nREPORT SOURCE:\n\\`\\`\\`tsx\n${reportSource}\n\\`\\`\\`\n\nGenerate the CustomFormTemplate JSON now.`;\n\n const body = {\n model: MODEL,\n max_tokens: 8192, // deepseek-chat output ceiling\n stream: true,\n temperature: 0,\n response_format: { type: \"json_object\" },\n messages: [\n { role: \"system\", content: `${INSTRUCTIONS}\\n\\n${SCHEMA}` },\n { role: \"user\", content: userText },\n ],\n };\n\n const resp = await fetch(DEEPSEEK_URL, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n if (!resp.ok || !resp.body) {\n const errText = await resp.text();\n console.error(\"DeepSeek API error:\", resp.status, errText);\n return json(\n { error: `DeepSeek API error (${resp.status})`, detail: errText },\n 502,\n );\n }\n\n // Accumulate text_delta events from the SSE stream.\n const rawText = await collectStreamedText(resp.body);\n const template = parseTemplate(rawText);\n if (!template) {\n console.error(\"Could not parse template JSON. Raw head:\", rawText.slice(0, 500));\n return json(\n { error: \"Model did not return valid template JSON\", raw: rawText.slice(0, 2000) },\n 502,\n );\n }\n\n return json({ template });\n } catch (err) {\n console.error(\"generate-form-template failed:\", err);\n return json({ error: String((err as Error).message || err) }, 500);\n }\n});\n\nfunction json(payload: unknown, status = 200): Response {\n return new Response(JSON.stringify(payload), {\n status,\n headers: { ...corsHeaders, \"content-type\": \"application/json\" },\n });\n}\n\n/** Read the DeepSeek (OpenAI-format) SSE stream and concatenate delta content. */\nasync function collectStreamedText(\n stream: ReadableStream,\n): Promise {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n let out = \"\";\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n // SSE events are separated by a blank line.\n let sep;\n while ((sep = buffer.indexOf(\"\\n\\n\")) !== -1) {\n const event = buffer.slice(0, sep);\n buffer = buffer.slice(sep + 2);\n for (const line of event.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) continue;\n const data = trimmed.slice(5).trim();\n if (!data || data === \"[DONE]\") continue;\n try {\n const parsed = JSON.parse(data);\n // Ignore reasoning_content (deepseek-reasoner) — keep only answer text.\n const delta = parsed.choices?.[0]?.delta?.content;\n if (typeof delta === \"string\") out += delta;\n } catch {\n // ignore keep-alive / non-JSON lines\n }\n }\n }\n }\n return out;\n}\n\n/** Extract and parse the JSON template object from the model's raw output. */\nfunction parseTemplate(raw: string): Record | null {\n let text = raw.trim();\n // Strip markdown fences if the model added them despite instructions.\n const fence = text.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\n if (fence) text = fence[1].trim();\n // Fall back to the first {...} span.\n if (!text.startsWith(\"{\")) {\n const first = text.indexOf(\"{\");\n const last = text.lastIndexOf(\"}\");\n if (first === -1 || last === -1) return null;\n text = text.slice(first, last + 1);\n }\n try {\n const obj = JSON.parse(text);\n if (obj && typeof obj === \"object\" && obj.structure) return obj;\n return null;\n } catch {\n return null;\n }\n}\n"}}}}