@burdenoff/microfe-bigconsole 2026.827.1 → 2026.828.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ async function r(e) {
8
8
  let t = e.name.toLowerCase();
9
9
  if (n(e)) return await e.text();
10
10
  if (t.endsWith(".xlsx") || t.endsWith(".xls")) {
11
- let t = await import("../../node_modules/xlsx/xlsx.js"), n = await e.arrayBuffer(), r = t.read(n, { type: "array" }), i = r.SheetNames[0];
11
+ let t = await import("../../node_modules/@e965/xlsx/xlsx.js"), n = await e.arrayBuffer(), r = t.read(n, { type: "array" }), i = r.SheetNames[0];
12
12
  return i ? t.utils.sheet_to_csv(r.Sheets[i]) : "";
13
13
  }
14
14
  if (t.endsWith(".pdf") || e.type === "application/pdf") {
@@ -1 +1 @@
1
- {"version":3,"file":"attachmentExtract.js","names":[],"sources":["../../../src/bigconsole/assistant/attachmentExtract.ts"],"sourcesContent":["/**\n * Client-side extraction of an uploaded data file into a capped text/rows\n * preview that gets folded into the assistant prompt.\n *\n * Lives in the BigConsole product layer (not fe-libs) because it pulls in the\n * xlsx / pdfjs parsers — fe-libs stays parsing-free and only carries the raw\n * `File`s through `AssistantSendArgs.attachments`. Heavy parsers are loaded via\n * dynamic `import()` so they code-split and only download when a file is\n * actually attached.\n *\n * The agent receives a CAPPED preview — enough to design a DataSink schema from,\n * not the whole file — because the payload rides the `proxySandboxRequest`\n * gateway call (bounded execution budget) and the model has a context limit.\n */\n\nconst MAX_CHARS = 100_000; // ~100 KB of text per file\nconst MAX_ROWS = 200;\n\nfunction isTextLike(file: File): boolean {\n const name = file.name.toLowerCase();\n return (\n name.endsWith('.json') ||\n name.endsWith('.csv') ||\n name.endsWith('.txt') ||\n file.type === 'application/json' ||\n file.type === 'text/csv' ||\n file.type.startsWith('text/')\n );\n}\n\nasync function extractOne(file: File): Promise<string> {\n const name = file.name.toLowerCase();\n\n // JSON / CSV / plain text — native, no deps.\n if (isTextLike(file)) {\n return await file.text();\n }\n\n // Excel — SheetJS, dynamically imported. First sheet → CSV (compact + agent-friendly).\n if (name.endsWith('.xlsx') || name.endsWith('.xls')) {\n const XLSX = await import('xlsx');\n const buf = await file.arrayBuffer();\n const wb = XLSX.read(buf, { type: 'array' });\n const first = wb.SheetNames[0];\n if (!first) return '';\n return XLSX.utils.sheet_to_csv(wb.Sheets[first]);\n }\n\n // PDF — best-effort text-layer extraction via pdfjs (dynamically imported).\n // Unreliable for tabular data and useless for scanned PDFs; the caller shows a\n // graceful note if this throws. NOTE: pdfjs worker setup is Vite-specific and\n // must be verified against a running build.\n if (name.endsWith('.pdf') || file.type === 'application/pdf') {\n const pdfjs = await import('pdfjs-dist');\n const buf = await file.arrayBuffer();\n const doc = await pdfjs.getDocument({ data: new Uint8Array(buf) }).promise;\n const parts: string[] = [];\n const pages = Math.min(doc.numPages, 20);\n for (let page = 1; page <= pages; page += 1) {\n const content = await (await doc.getPage(page)).getTextContent();\n parts.push(content.items.map((item) => ('str' in item ? item.str : '')).join(' '));\n }\n return parts.join('\\n');\n }\n\n // Unknown extension → best-effort text.\n return await file.text();\n}\n\n/** Keep the first MAX_ROWS lines / MAX_CHARS chars so a huge file can't blow the\n * prompt. Returns whether anything was dropped so the agent knows it's a sample. */\nfunction limit(text: string): { text: string; truncated: boolean } {\n let out = text;\n let truncated = false;\n const lines = out.split(/\\r?\\n/);\n if (lines.length > MAX_ROWS) {\n out = lines.slice(0, MAX_ROWS).join('\\n');\n truncated = true;\n }\n if (out.length > MAX_CHARS) {\n out = out.slice(0, MAX_CHARS);\n truncated = true;\n }\n return { text: out, truncated };\n}\n\n/**\n * Extract every attachment and format them as fenced \"Attached data source\"\n * blocks the agent can read and turn into a DataSink. Files that fail to parse\n * (e.g. a scanned PDF) produce a short note rather than aborting the send.\n */\nexport async function buildAttachmentBlock(files: File[]): Promise<string> {\n const blocks: string[] = [];\n for (const file of files) {\n let raw: string;\n try {\n raw = await extractOne(file);\n } catch (err) {\n const reason = err instanceof Error ? err.message : 'unknown error';\n blocks.push(\n `--- Attached file \"${file.name}\" could not be auto-read (${reason}). ` +\n `Please paste its rows here instead. ---`\n );\n continue;\n }\n const { text, truncated } = limit(raw.trim());\n const note = truncated ? ` (sample — first ${MAX_ROWS} rows / ~${Math.round(MAX_CHARS / 1000)}KB)` : '';\n blocks.push(`--- Attached data source: ${file.name}${note} ---\\n${text}\\n--- end ${file.name} ---`);\n }\n return blocks.join('\\n\\n');\n}\n"],"mappings":";AAeA,IAAM,IAAY,KACZ,IAAW;AAEjB,SAAS,EAAW,GAAqB;CACvC,IAAM,IAAO,EAAK,KAAK,aAAa;AACpC,QACE,EAAK,SAAS,QAAQ,IACtB,EAAK,SAAS,OAAO,IACrB,EAAK,SAAS,OAAO,IACrB,EAAK,SAAS,sBACd,EAAK,SAAS,cACd,EAAK,KAAK,WAAW,QAAQ;;AAIjC,eAAe,EAAW,GAA6B;CACrD,IAAM,IAAO,EAAK,KAAK,aAAa;AAGpC,KAAI,EAAW,EAAK,CAClB,QAAO,MAAM,EAAK,MAAM;AAI1B,KAAI,EAAK,SAAS,QAAQ,IAAI,EAAK,SAAS,OAAO,EAAE;EACnD,IAAM,IAAO,MAAM,OAAO,oCACpB,IAAM,MAAM,EAAK,aAAa,EAC9B,IAAK,EAAK,KAAK,GAAK,EAAE,MAAM,SAAS,CAAC,EACtC,IAAQ,EAAG,WAAW;AAE5B,SADK,IACE,EAAK,MAAM,aAAa,EAAG,OAAO,GAAO,GAD7B;;AAQrB,KAAI,EAAK,SAAS,OAAO,IAAI,EAAK,SAAS,mBAAmB;EAC5D,IAAM,IAAQ,MAAM,OAAO,+CACrB,IAAM,MAAM,EAAK,aAAa,EAC9B,IAAM,MAAM,EAAM,YAAY,EAAE,MAAM,IAAI,WAAW,EAAI,EAAE,CAAC,CAAC,SAC7D,IAAkB,EAAE,EACpB,IAAQ,KAAK,IAAI,EAAI,UAAU,GAAG;AACxC,OAAK,IAAI,IAAO,GAAG,KAAQ,GAAO,KAAQ,GAAG;GAC3C,IAAM,IAAU,OAAO,MAAM,EAAI,QAAQ,EAAK,EAAE,gBAAgB;AAChE,KAAM,KAAK,EAAQ,MAAM,KAAK,MAAU,SAAS,IAAO,EAAK,MAAM,GAAI,CAAC,KAAK,IAAI,CAAC;;AAEpF,SAAO,EAAM,KAAK,KAAK;;AAIzB,QAAO,MAAM,EAAK,MAAM;;AAK1B,SAAS,EAAM,GAAoD;CACjE,IAAI,IAAM,GACN,IAAY,IACV,IAAQ,EAAI,MAAM,QAAQ;AAShC,QARI,EAAM,SAAS,MACjB,IAAM,EAAM,MAAM,GAAG,EAAS,CAAC,KAAK,KAAK,EACzC,IAAY,KAEV,EAAI,SAAS,MACf,IAAM,EAAI,MAAM,GAAG,EAAU,EAC7B,IAAY,KAEP;EAAE,MAAM;EAAK;EAAW;;AAQjC,eAAsB,EAAqB,GAAgC;CACzE,IAAM,IAAmB,EAAE;AAC3B,MAAK,IAAM,KAAQ,GAAO;EACxB,IAAI;AACJ,MAAI;AACF,OAAM,MAAM,EAAW,EAAK;WACrB,GAAK;GACZ,IAAM,IAAS,aAAe,QAAQ,EAAI,UAAU;AACpD,KAAO,KACL,sBAAsB,EAAK,KAAK,4BAA4B,EAAO,4CAEpE;AACD;;EAEF,IAAM,EAAE,SAAM,iBAAc,EAAM,EAAI,MAAM,CAAC,EACvC,IAAO,IAAY,oBAAoB,EAAS,WAAW,KAAK,MAAM,IAAY,IAAK,CAAC,OAAO;AACrG,IAAO,KAAK,6BAA6B,EAAK,OAAO,EAAK,QAAQ,EAAK,YAAY,EAAK,KAAK,MAAM;;AAErG,QAAO,EAAO,KAAK,OAAO"}
1
+ {"version":3,"file":"attachmentExtract.js","names":[],"sources":["../../../src/bigconsole/assistant/attachmentExtract.ts"],"sourcesContent":["/**\n * Client-side extraction of an uploaded data file into a capped text/rows\n * preview that gets folded into the assistant prompt.\n *\n * Lives in the BigConsole product layer (not fe-libs) because it pulls in the\n * xlsx / pdfjs parsers — fe-libs stays parsing-free and only carries the raw\n * `File`s through `AssistantSendArgs.attachments`. Heavy parsers are loaded via\n * dynamic `import()` so they code-split and only download when a file is\n * actually attached.\n *\n * The agent receives a CAPPED preview — enough to design a DataSink schema from,\n * not the whole file — because the payload rides the `proxySandboxRequest`\n * gateway call (bounded execution budget) and the model has a context limit.\n */\n\nconst MAX_CHARS = 100_000; // ~100 KB of text per file\nconst MAX_ROWS = 200;\n\nfunction isTextLike(file: File): boolean {\n const name = file.name.toLowerCase();\n return (\n name.endsWith('.json') ||\n name.endsWith('.csv') ||\n name.endsWith('.txt') ||\n file.type === 'application/json' ||\n file.type === 'text/csv' ||\n file.type.startsWith('text/')\n );\n}\n\nasync function extractOne(file: File): Promise<string> {\n const name = file.name.toLowerCase();\n\n // JSON / CSV / plain text — native, no deps.\n if (isTextLike(file)) {\n return await file.text();\n }\n\n // Excel — SheetJS, dynamically imported. First sheet → CSV (compact + agent-friendly).\n if (name.endsWith('.xlsx') || name.endsWith('.xls')) {\n const XLSX = await import('@e965/xlsx');\n const buf = await file.arrayBuffer();\n const wb = XLSX.read(buf, { type: 'array' });\n const first = wb.SheetNames[0];\n if (!first) return '';\n return XLSX.utils.sheet_to_csv(wb.Sheets[first]);\n }\n\n // PDF — best-effort text-layer extraction via pdfjs (dynamically imported).\n // Unreliable for tabular data and useless for scanned PDFs; the caller shows a\n // graceful note if this throws. NOTE: pdfjs worker setup is Vite-specific and\n // must be verified against a running build.\n if (name.endsWith('.pdf') || file.type === 'application/pdf') {\n const pdfjs = await import('pdfjs-dist');\n const buf = await file.arrayBuffer();\n const doc = await pdfjs.getDocument({ data: new Uint8Array(buf) }).promise;\n const parts: string[] = [];\n const pages = Math.min(doc.numPages, 20);\n for (let page = 1; page <= pages; page += 1) {\n const content = await (await doc.getPage(page)).getTextContent();\n parts.push(content.items.map((item) => ('str' in item ? item.str : '')).join(' '));\n }\n return parts.join('\\n');\n }\n\n // Unknown extension → best-effort text.\n return await file.text();\n}\n\n/** Keep the first MAX_ROWS lines / MAX_CHARS chars so a huge file can't blow the\n * prompt. Returns whether anything was dropped so the agent knows it's a sample. */\nfunction limit(text: string): { text: string; truncated: boolean } {\n let out = text;\n let truncated = false;\n const lines = out.split(/\\r?\\n/);\n if (lines.length > MAX_ROWS) {\n out = lines.slice(0, MAX_ROWS).join('\\n');\n truncated = true;\n }\n if (out.length > MAX_CHARS) {\n out = out.slice(0, MAX_CHARS);\n truncated = true;\n }\n return { text: out, truncated };\n}\n\n/**\n * Extract every attachment and format them as fenced \"Attached data source\"\n * blocks the agent can read and turn into a DataSink. Files that fail to parse\n * (e.g. a scanned PDF) produce a short note rather than aborting the send.\n */\nexport async function buildAttachmentBlock(files: File[]): Promise<string> {\n const blocks: string[] = [];\n for (const file of files) {\n let raw: string;\n try {\n raw = await extractOne(file);\n } catch (err) {\n const reason = err instanceof Error ? err.message : 'unknown error';\n blocks.push(\n `--- Attached file \"${file.name}\" could not be auto-read (${reason}). ` +\n `Please paste its rows here instead. ---`\n );\n continue;\n }\n const { text, truncated } = limit(raw.trim());\n const note = truncated ? ` (sample — first ${MAX_ROWS} rows / ~${Math.round(MAX_CHARS / 1000)}KB)` : '';\n blocks.push(`--- Attached data source: ${file.name}${note} ---\\n${text}\\n--- end ${file.name} ---`);\n }\n return blocks.join('\\n\\n');\n}\n"],"mappings":";AAeA,IAAM,IAAY,KACZ,IAAW;AAEjB,SAAS,EAAW,GAAqB;CACvC,IAAM,IAAO,EAAK,KAAK,aAAa;AACpC,QACE,EAAK,SAAS,QAAQ,IACtB,EAAK,SAAS,OAAO,IACrB,EAAK,SAAS,OAAO,IACrB,EAAK,SAAS,sBACd,EAAK,SAAS,cACd,EAAK,KAAK,WAAW,QAAQ;;AAIjC,eAAe,EAAW,GAA6B;CACrD,IAAM,IAAO,EAAK,KAAK,aAAa;AAGpC,KAAI,EAAW,EAAK,CAClB,QAAO,MAAM,EAAK,MAAM;AAI1B,KAAI,EAAK,SAAS,QAAQ,IAAI,EAAK,SAAS,OAAO,EAAE;EACnD,IAAM,IAAO,MAAM,OAAO,0CACpB,IAAM,MAAM,EAAK,aAAa,EAC9B,IAAK,EAAK,KAAK,GAAK,EAAE,MAAM,SAAS,CAAC,EACtC,IAAQ,EAAG,WAAW;AAE5B,SADK,IACE,EAAK,MAAM,aAAa,EAAG,OAAO,GAAO,GAD7B;;AAQrB,KAAI,EAAK,SAAS,OAAO,IAAI,EAAK,SAAS,mBAAmB;EAC5D,IAAM,IAAQ,MAAM,OAAO,+CACrB,IAAM,MAAM,EAAK,aAAa,EAC9B,IAAM,MAAM,EAAM,YAAY,EAAE,MAAM,IAAI,WAAW,EAAI,EAAE,CAAC,CAAC,SAC7D,IAAkB,EAAE,EACpB,IAAQ,KAAK,IAAI,EAAI,UAAU,GAAG;AACxC,OAAK,IAAI,IAAO,GAAG,KAAQ,GAAO,KAAQ,GAAG;GAC3C,IAAM,IAAU,OAAO,MAAM,EAAI,QAAQ,EAAK,EAAE,gBAAgB;AAChE,KAAM,KAAK,EAAQ,MAAM,KAAK,MAAU,SAAS,IAAO,EAAK,MAAM,GAAI,CAAC,KAAK,IAAI,CAAC;;AAEpF,SAAO,EAAM,KAAK,KAAK;;AAIzB,QAAO,MAAM,EAAK,MAAM;;AAK1B,SAAS,EAAM,GAAoD;CACjE,IAAI,IAAM,GACN,IAAY,IACV,IAAQ,EAAI,MAAM,QAAQ;AAShC,QARI,EAAM,SAAS,MACjB,IAAM,EAAM,MAAM,GAAG,EAAS,CAAC,KAAK,KAAK,EACzC,IAAY,KAEV,EAAI,SAAS,MACf,IAAM,EAAI,MAAM,GAAG,EAAU,EAC7B,IAAY,KAEP;EAAE,MAAM;EAAK;EAAW;;AAQjC,eAAsB,EAAqB,GAAgC;CACzE,IAAM,IAAmB,EAAE;AAC3B,MAAK,IAAM,KAAQ,GAAO;EACxB,IAAI;AACJ,MAAI;AACF,OAAM,MAAM,EAAW,EAAK;WACrB,GAAK;GACZ,IAAM,IAAS,aAAe,QAAQ,EAAI,UAAU;AACpD,KAAO,KACL,sBAAsB,EAAK,KAAK,4BAA4B,EAAO,4CAEpE;AACD;;EAEF,IAAM,EAAE,SAAM,iBAAc,EAAM,EAAI,MAAM,CAAC,EACvC,IAAO,IAAY,oBAAoB,EAAS,WAAW,KAAK,MAAM,IAAY,IAAK,CAAC,OAAO;AACrG,IAAO,KAAK,6BAA6B,EAAK,OAAO,EAAK,QAAQ,EAAK,YAAY,EAAK,KAAK,MAAM;;AAErG,QAAO,EAAO,KAAK,OAAO"}
@@ -1,6 +1,6 @@
1
1
  //#region src/bigconsole/customWidget/proposalSource.ts
2
2
  function e(e) {
3
- return e.filter((e) => !e.deletedAt).map(({ id: e, key: t, name: n }) => ({
3
+ return e.filter((e) => !e.deletedAt && e.isActive !== !1).map(({ id: e, key: t, name: n }) => ({
4
4
  id: e,
5
5
  key: t,
6
6
  name: n
@@ -1 +1 @@
1
- {"version":3,"file":"proposalSource.js","names":[],"sources":["../../../src/bigconsole/customWidget/proposalSource.ts"],"sourcesContent":["/**\n * Proposal source — the seam between the review UI and whatever produces\n * proposals (BOFF-5532 / BOFF-3021).\n *\n * The UI only ever talks to `CustomWidgetProposalSource`. Today the default\n * implementation derives a proposal locally from the prompt and the workspace's\n * real sinks and parsers, because the backend operation that will emit typed\n * proposals is owned by BOFF-3021's backend half and does not exist yet.\n *\n * When it lands, add a `graphqlProposalSource` here that calls it and swap the\n * value passed to the panel. Nothing in the review, edit, approve or persist\n * path changes: those already run against real sinks, real parsers and the real\n * `createBigConsoleWidget` mutation.\n */\n\nimport {\n CUSTOM_WIDGET_PROPOSAL_VERSION,\n type CustomWidgetProposal,\n type ProposableWidgetType,\n type ProposalFieldMapping,\n} from './proposalContract';\n\n/** A sink the current user may bind to, as the panel knows it. */\nexport interface ProposalCandidateSink {\n id: string;\n key: string;\n name?: string | null;\n}\n\n/**\n * Converts the workspace store into a proposal-safe candidate set. The API\n * excludes soft-deleted sinks, but this second boundary is intentional:\n * persisted client state can outlive an out-of-band deletion.\n */\nexport function selectActiveProposalSinks(\n sinks: ReadonlyArray<ProposalCandidateSink & { deletedAt?: string | null }>\n): ProposalCandidateSink[] {\n return sinks.filter((sink) => !sink.deletedAt).map(({ id, key, name }) => ({ id, key, name }));\n}\n\n/** A parser the current user may bind to. */\nexport interface ProposalCandidateParser {\n id: string;\n name: string;\n inputSinkKey: string;\n}\n\nexport interface ProposalRequest {\n prompt: string;\n sinks: readonly ProposalCandidateSink[];\n parsers: readonly ProposalCandidateParser[];\n}\n\nexport interface CustomWidgetProposalSource {\n /** Stable id, surfaced in the UI so reviewers know what produced a proposal. */\n readonly id: string;\n propose(request: ProposalRequest): Promise<CustomWidgetProposal>;\n}\n\n// ============================================================================\n// Local heuristic source\n// ============================================================================\n\n/**\n * Keyword → widget type. Ordered most-specific first so \"conversion funnel\"\n * matches funnel before a generic \"chart\" would win.\n */\nconst TYPE_KEYWORDS: ReadonlyArray<{ type: ProposableWidgetType; words: readonly string[] }> = [\n { type: 'funnel_chart', words: ['funnel', 'conversion', 'drop-off', 'dropoff', 'stage'] },\n { type: 'heatmap', words: ['heatmap', 'heat map', 'matrix', 'intensity', 'by hour', 'density'] },\n { type: 'retention', words: ['retention', 'cohort'] },\n { type: 'table', words: ['table', 'list of', 'rows', 'breakdown table', 'grid'] },\n { type: 'metric_card', words: ['kpi', 'single number', 'metric card', 'scorecard', 'total'] },\n { type: 'chart', words: ['chart', 'trend', 'over time', 'line', 'bar', 'area', 'pie', 'graph'] },\n];\n\nconst CHART_SUBTYPE_KEYWORDS: ReadonlyArray<{ chartType: string; words: readonly string[] }> = [\n { chartType: 'bar', words: ['bar'] },\n { chartType: 'area', words: ['area', 'cumulative'] },\n { chartType: 'pie', words: ['pie', 'composition', 'share', 'mix'] },\n { chartType: 'line', words: ['line', 'trend', 'over time'] },\n];\n\nfunction detectWidgetType(prompt: string): ProposableWidgetType {\n const lower = prompt.toLowerCase();\n for (const { type, words } of TYPE_KEYWORDS) {\n if (words.some((word) => lower.includes(word))) return type;\n }\n return 'chart';\n}\n\nfunction detectChartSubtype(prompt: string): string {\n const lower = prompt.toLowerCase();\n for (const { chartType, words } of CHART_SUBTYPE_KEYWORDS) {\n if (words.some((word) => lower.includes(word))) return chartType;\n }\n return 'line';\n}\n\n/**\n * Picks the sink whose key/name shares the most word-stems with the prompt.\n * Falls back to the first permitted sink so a proposal is always reviewable\n * rather than failing outright.\n */\nfunction pickSink(prompt: string, sinks: readonly ProposalCandidateSink[]): ProposalCandidateSink | undefined {\n if (sinks.length === 0) return undefined;\n const tokens = prompt\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((token) => token.length > 2);\n\n let best: { sink: ProposalCandidateSink; score: number } | undefined;\n for (const sink of sinks) {\n const haystack = `${sink.key} ${sink.name ?? ''}`.toLowerCase();\n const score = tokens.reduce((acc, token) => (haystack.includes(token) ? acc + 1 : acc), 0);\n if (!best || score > best.score) best = { sink, score };\n }\n return best?.sink ?? sinks[0];\n}\n\n/** Default field mapping per widget type, using the renderers' conventions. */\nfunction defaultFieldMapping(type: ProposableWidgetType): ProposalFieldMapping {\n switch (type) {\n case 'heatmap':\n return { xAxisField: 'column', yAxisField: 'row', valueField: 'value' };\n case 'funnel_chart':\n return { labelField: 'name', valueField: 'value' };\n case 'metric_card':\n return { valueField: 'value' };\n case 'table':\n return {};\n case 'retention':\n return { cohortField: 'cohort', valueField: 'value' };\n case 'chart':\n default:\n return { xAxisField: 'name', labelField: 'name', valueField: 'value' };\n }\n}\n\nfunction defaultConfig(type: ProposableWidgetType, prompt: string): Record<string, unknown> {\n const base: Record<string, unknown> = { showLegend: true, showTooltip: true };\n switch (type) {\n case 'chart':\n return { ...base, chartType: detectChartSubtype(prompt), showGrid: true };\n case 'funnel_chart':\n return { ...base, chartType: 'funnel', showLabels: true, showPercentages: true, orientation: 'vertical' };\n case 'heatmap':\n return { ...base, colorScheme: 'blue', showValues: true };\n case 'table':\n return { ...base, pageSize: 10, sortable: true };\n case 'metric_card':\n return { ...base, showTrend: true };\n default:\n return base;\n }\n}\n\n/** Turns the prompt's first clause into a widget title. */\nfunction deriveTitle(prompt: string): string {\n const cleaned = prompt\n .trim()\n .replace(/^(please\\s+)?(create|build|make|add|show me|show|give me)\\s+(an?\\s+|the\\s+)?/i, '')\n .split(/[.;\\n]/)[0]\n .trim();\n if (!cleaned) return 'Custom widget';\n const titled = cleaned.charAt(0).toUpperCase() + cleaned.slice(1);\n return titled.length > 60 ? `${titled.slice(0, 57)}…` : titled;\n}\n\nlet proposalCounter = 0;\n\n/**\n * Derives a proposal from the prompt and the workspace's real, permitted sinks\n * and parsers. Deterministic, so the review UI and its tests behave predictably.\n */\nexport const localProposalSource: CustomWidgetProposalSource = {\n id: 'local-heuristic-v1',\n\n async propose({ prompt, sinks, parsers }: ProposalRequest): Promise<CustomWidgetProposal> {\n const widgetType = detectWidgetType(prompt);\n const sink = pickSink(prompt, sinks);\n const parser = sink ? parsers.find((candidate) => candidate.inputSinkKey === sink.key) : undefined;\n\n proposalCounter += 1;\n\n return {\n version: CUSTOM_WIDGET_PROPOSAL_VERSION,\n id: `proposal-${proposalCounter}`,\n sourcePrompt: prompt,\n generatedBy: localProposalSource.id,\n widgetType,\n binding: {\n dataSinkId: sink?.id ?? '',\n dataSinkKey: sink?.key,\n parserId: parser?.id,\n parserName: parser?.name,\n },\n fieldMapping: defaultFieldMapping(widgetType),\n presentation: {\n title: deriveTitle(prompt),\n description: `Generated from: \"${prompt}\"`,\n config: defaultConfig(widgetType, prompt),\n },\n rationale: sink\n ? `Matched DataSink \"${sink.key}\"${parser ? ` and parser \"${parser.name}\"` : ' (no parser found for this sink)'}.`\n : 'No DataSink is available in this workspace — create one before saving this widget.',\n };\n },\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"proposalSource.js","names":[],"sources":["../../../src/bigconsole/customWidget/proposalSource.ts"],"sourcesContent":["/**\n * Proposal source — the seam between the review UI and whatever produces\n * proposals (BOFF-5532 / BOFF-3021).\n *\n * The UI only ever talks to `CustomWidgetProposalSource`. Today the default\n * implementation derives a proposal locally from the prompt and the workspace's\n * real sinks and parsers, because the backend operation that will emit typed\n * proposals is owned by BOFF-3021's backend half and does not exist yet.\n *\n * When it lands, add a `graphqlProposalSource` here that calls it and swap the\n * value passed to the panel. Nothing in the review, edit, approve or persist\n * path changes: those already run against real sinks, real parsers and the real\n * `createBigConsoleWidget` mutation.\n */\n\nimport {\n CUSTOM_WIDGET_PROPOSAL_VERSION,\n type CustomWidgetProposal,\n type ProposableWidgetType,\n type ProposalFieldMapping,\n} from './proposalContract';\n\n/** A sink the current user may bind to, as the panel knows it. */\nexport interface ProposalCandidateSink {\n id: string;\n key: string;\n name?: string | null;\n}\n\n/**\n * Converts the workspace store into a proposal-safe candidate set. The API\n * excludes soft-deleted sinks, but this second boundary is intentional:\n * persisted client state can outlive an out-of-band deletion, and a sink whose\n * deletion signal is missing from a lightweight list fragment must still be\n * excluded if its active flag has been cleared.\n */\nexport function selectActiveProposalSinks(\n sinks: ReadonlyArray<ProposalCandidateSink & { deletedAt?: string | null; isActive?: boolean | null }>\n): ProposalCandidateSink[] {\n return sinks\n .filter((sink) => !sink.deletedAt && sink.isActive !== false)\n .map(({ id, key, name }) => ({ id, key, name }));\n}\n\n/** A parser the current user may bind to. */\nexport interface ProposalCandidateParser {\n id: string;\n name: string;\n inputSinkKey: string;\n}\n\nexport interface ProposalRequest {\n prompt: string;\n sinks: readonly ProposalCandidateSink[];\n parsers: readonly ProposalCandidateParser[];\n}\n\nexport interface CustomWidgetProposalSource {\n /** Stable id, surfaced in the UI so reviewers know what produced a proposal. */\n readonly id: string;\n propose(request: ProposalRequest): Promise<CustomWidgetProposal>;\n}\n\n// ============================================================================\n// Local heuristic source\n// ============================================================================\n\n/**\n * Keyword → widget type. Ordered most-specific first so \"conversion funnel\"\n * matches funnel before a generic \"chart\" would win.\n */\nconst TYPE_KEYWORDS: ReadonlyArray<{ type: ProposableWidgetType; words: readonly string[] }> = [\n { type: 'funnel_chart', words: ['funnel', 'conversion', 'drop-off', 'dropoff', 'stage'] },\n { type: 'heatmap', words: ['heatmap', 'heat map', 'matrix', 'intensity', 'by hour', 'density'] },\n { type: 'retention', words: ['retention', 'cohort'] },\n { type: 'table', words: ['table', 'list of', 'rows', 'breakdown table', 'grid'] },\n { type: 'metric_card', words: ['kpi', 'single number', 'metric card', 'scorecard', 'total'] },\n { type: 'chart', words: ['chart', 'trend', 'over time', 'line', 'bar', 'area', 'pie', 'graph'] },\n];\n\nconst CHART_SUBTYPE_KEYWORDS: ReadonlyArray<{ chartType: string; words: readonly string[] }> = [\n { chartType: 'bar', words: ['bar'] },\n { chartType: 'area', words: ['area', 'cumulative'] },\n { chartType: 'pie', words: ['pie', 'composition', 'share', 'mix'] },\n { chartType: 'line', words: ['line', 'trend', 'over time'] },\n];\n\nfunction detectWidgetType(prompt: string): ProposableWidgetType {\n const lower = prompt.toLowerCase();\n for (const { type, words } of TYPE_KEYWORDS) {\n if (words.some((word) => lower.includes(word))) return type;\n }\n return 'chart';\n}\n\nfunction detectChartSubtype(prompt: string): string {\n const lower = prompt.toLowerCase();\n for (const { chartType, words } of CHART_SUBTYPE_KEYWORDS) {\n if (words.some((word) => lower.includes(word))) return chartType;\n }\n return 'line';\n}\n\n/**\n * Picks the sink whose key/name shares the most word-stems with the prompt.\n * Falls back to the first permitted sink so a proposal is always reviewable\n * rather than failing outright.\n */\nfunction pickSink(prompt: string, sinks: readonly ProposalCandidateSink[]): ProposalCandidateSink | undefined {\n if (sinks.length === 0) return undefined;\n const tokens = prompt\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((token) => token.length > 2);\n\n let best: { sink: ProposalCandidateSink; score: number } | undefined;\n for (const sink of sinks) {\n const haystack = `${sink.key} ${sink.name ?? ''}`.toLowerCase();\n const score = tokens.reduce((acc, token) => (haystack.includes(token) ? acc + 1 : acc), 0);\n if (!best || score > best.score) best = { sink, score };\n }\n return best?.sink ?? sinks[0];\n}\n\n/** Default field mapping per widget type, using the renderers' conventions. */\nfunction defaultFieldMapping(type: ProposableWidgetType): ProposalFieldMapping {\n switch (type) {\n case 'heatmap':\n return { xAxisField: 'column', yAxisField: 'row', valueField: 'value' };\n case 'funnel_chart':\n return { labelField: 'name', valueField: 'value' };\n case 'metric_card':\n return { valueField: 'value' };\n case 'table':\n return {};\n case 'retention':\n return { cohortField: 'cohort', valueField: 'value' };\n case 'chart':\n default:\n return { xAxisField: 'name', labelField: 'name', valueField: 'value' };\n }\n}\n\nfunction defaultConfig(type: ProposableWidgetType, prompt: string): Record<string, unknown> {\n const base: Record<string, unknown> = { showLegend: true, showTooltip: true };\n switch (type) {\n case 'chart':\n return { ...base, chartType: detectChartSubtype(prompt), showGrid: true };\n case 'funnel_chart':\n return { ...base, chartType: 'funnel', showLabels: true, showPercentages: true, orientation: 'vertical' };\n case 'heatmap':\n return { ...base, colorScheme: 'blue', showValues: true };\n case 'table':\n return { ...base, pageSize: 10, sortable: true };\n case 'metric_card':\n return { ...base, showTrend: true };\n default:\n return base;\n }\n}\n\n/** Turns the prompt's first clause into a widget title. */\nfunction deriveTitle(prompt: string): string {\n const cleaned = prompt\n .trim()\n .replace(/^(please\\s+)?(create|build|make|add|show me|show|give me)\\s+(an?\\s+|the\\s+)?/i, '')\n .split(/[.;\\n]/)[0]\n .trim();\n if (!cleaned) return 'Custom widget';\n const titled = cleaned.charAt(0).toUpperCase() + cleaned.slice(1);\n return titled.length > 60 ? `${titled.slice(0, 57)}…` : titled;\n}\n\nlet proposalCounter = 0;\n\n/**\n * Derives a proposal from the prompt and the workspace's real, permitted sinks\n * and parsers. Deterministic, so the review UI and its tests behave predictably.\n */\nexport const localProposalSource: CustomWidgetProposalSource = {\n id: 'local-heuristic-v1',\n\n async propose({ prompt, sinks, parsers }: ProposalRequest): Promise<CustomWidgetProposal> {\n const widgetType = detectWidgetType(prompt);\n const sink = pickSink(prompt, sinks);\n const parser = sink ? parsers.find((candidate) => candidate.inputSinkKey === sink.key) : undefined;\n\n proposalCounter += 1;\n\n return {\n version: CUSTOM_WIDGET_PROPOSAL_VERSION,\n id: `proposal-${proposalCounter}`,\n sourcePrompt: prompt,\n generatedBy: localProposalSource.id,\n widgetType,\n binding: {\n dataSinkId: sink?.id ?? '',\n dataSinkKey: sink?.key,\n parserId: parser?.id,\n parserName: parser?.name,\n },\n fieldMapping: defaultFieldMapping(widgetType),\n presentation: {\n title: deriveTitle(prompt),\n description: `Generated from: \"${prompt}\"`,\n config: defaultConfig(widgetType, prompt),\n },\n rationale: sink\n ? `Matched DataSink \"${sink.key}\"${parser ? ` and parser \"${parser.name}\"` : ' (no parser found for this sink)'}.`\n : 'No DataSink is available in this workspace — create one before saving this widget.',\n };\n },\n};\n"],"mappings":"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"}
@@ -391,6 +391,8 @@ ${e`
391
391
  version
392
392
  createdAt
393
393
  updatedAt
394
+ deletedAt
395
+ deletedBy
394
396
  linkedWorkflowCount
395
397
  }
396
398
  `, g = e`