saivivek6 commited on
Commit
9703678
·
1 Parent(s): 7788ad7

Deploy: flow/flowchart chart kind (layered, arrowed); richer charts (on-chart value labels, rounded bars, gradient areas, stronger hover) across kinds; baseline iframe auto-resizes to chart's natural height via postMessage + fill normalization; baseline visual sizing prompt

Browse files
vivek/backend/combined_prompt.py CHANGED
@@ -188,7 +188,7 @@ def _chart_has_data(chart: dict[str, Any]) -> bool:
188
  return _nonempty_list(chart.get("candles"))
189
  if kind == "boxplot":
190
  return _nonempty_list(chart.get("boxes"))
191
- if kind in {"sankey", "graph"}:
192
  return _nonempty_list(chart.get("links"))
193
  if kind in {"tree", "mindmap", "org", "orgchart"}:
194
  # Accept the same variants the renderer reads: root | tree | data | top-level node | items.
 
188
  return _nonempty_list(chart.get("candles"))
189
  if kind == "boxplot":
190
  return _nonempty_list(chart.get("boxes"))
191
+ if kind in {"sankey", "graph", "flow", "flowchart", "process"}:
192
  return _nonempty_list(chart.get("links"))
193
  if kind in {"tree", "mindmap", "org", "orgchart"}:
194
  # Accept the same variants the renderer reads: root | tree | data | top-level node | items.
vivek/backend/server.py CHANGED
@@ -797,8 +797,12 @@ _BASELINE_SYSTEM = (
797
  "- It MUST be a complete standalone HTML document (<!doctype html><html>...</html>) that renders on its own.\n"
798
  "- You MAY load ONE charting library from a CDN via a <script src=\"https://...\"> tag "
799
  "(e.g. Chart.js, Apache ECharts, or Plotly). Inline all of your OWN CSS and JS.\n"
800
- "- Make the chart responsive (width:100%), about 360px tall, on a transparent or white background, "
801
- "with a clean modern look and readable labels.\n"
 
 
 
 
802
  "- Use real, concrete data from your knowledge or the user's message — never placeholder 0,1,2,3.\n"
803
  "- The iframe is sandboxed (scripts only, no same-origin): do not use cookies, localStorage, or call "
804
  "any origin other than the single CDN you reference.\n"
 
797
  "- It MUST be a complete standalone HTML document (<!doctype html><html>...</html>) that renders on its own.\n"
798
  "- You MAY load ONE charting library from a CDN via a <script src=\"https://...\"> tag "
799
  "(e.g. Chart.js, Apache ECharts, or Plotly). Inline all of your OWN CSS and JS.\n"
800
+ "- The HTML renders in an <iframe> that AUTO-RESIZES to your content's height so give the chart a "
801
+ "CLEAR, defined size. Set html,body{margin:0;padding:0;width:100%;overflow:hidden;background:#fff} and "
802
+ "give the chart container width:100% and a sensible height of about 400px (a fixed height or an "
803
+ "aspect-ratio box — not 0). For Chart.js wrap the canvas in a height:400px div with maintainAspectRatio:false; "
804
+ "for Plotly use {responsive:true} with a 400px tall div; for ECharts give the container an explicit height "
805
+ "(~400px) and call chart.resize() on window resize. Clean modern look, readable labels, no page scrollbars.\n"
806
  "- Use real, concrete data from your knowledge or the user's message — never placeholder 0,1,2,3.\n"
807
  "- The iframe is sandboxed (scripts only, no same-origin): do not use cookies, localStorage, or call "
808
  "any origin other than the single CDN you reference.\n"
vivek/frontend-vue/dist/assets/{ActionRow-C5K-GzW5.js → ActionRow-B1R5m79B.js} RENAMED
@@ -1 +1 @@
1
- import{G as e,M as t,P as n,f as r,h as i,i as a,jt as o,u as s,v as c}from"./runtime-core.esm-bundler-olIhRSij.js";import{nr as l}from"./index-oLTRTdCR.js";var u={class:`flex flex-wrap gap-2`},d=c({__name:`ActionRow`,props:{block:{}},emits:[`action`],setup(c,{emit:d}){let f=d;function p(e){let t=String(e.label||e.intent||``).trim();t&&f(`action`,t)}return(d,f)=>(t(),r(`div`,u,[(t(!0),r(a,null,n(c.block.buttons||[],(n,r)=>(t(),s(l,{key:r,type:`button`,variant:`outline`,size:`sm`,title:n.intent?`Ask: ${n.label||n.intent}`:void 0,onClick:e=>p(n)},{default:e(()=>[i(o(n.label||`Action`),1)]),_:2},1032,[`title`,`onClick`]))),128))]))}});export{d as default};
 
1
+ import{G as e,M as t,P as n,f as r,h as i,i as a,jt as o,u as s,v as c}from"./runtime-core.esm-bundler-olIhRSij.js";import{nr as l}from"./index-nrc-cgMW.js";var u={class:`flex flex-wrap gap-2`},d=c({__name:`ActionRow`,props:{block:{}},emits:[`action`],setup(c,{emit:d}){let f=d;function p(e){let t=String(e.label||e.intent||``).trim();t&&f(`action`,t)}return(d,f)=>(t(),r(`div`,u,[(t(!0),r(a,null,n(c.block.buttons||[],(n,r)=>(t(),s(l,{key:r,type:`button`,variant:`outline`,size:`sm`,title:n.intent?`Ask: ${n.label||n.intent}`:void 0,onClick:e=>p(n)},{default:e(()=>[i(o(n.label||`Action`),1)]),_:2},1032,[`title`,`onClick`]))),128))]))}});export{d as default};
vivek/frontend-vue/dist/assets/{ChartBlock-CGlwF_DK.js → ChartBlock-2l17cTFi.js} RENAMED
The diff for this file is too large to render. See raw diff
 
vivek/frontend-vue/dist/assets/{TextBlock-D7ydM5dr.js → TextBlock-HD2UY_tu.js} RENAMED
@@ -1,3 +1,3 @@
1
- import{M as e,c as t,f as n,v as r}from"./runtime-core.esm-bundler-olIhRSij.js";import{r as i}from"./index-oLTRTdCR.js";var a=[`innerHTML`],o=i(r({__name:`TextBlock`,props:{block:{}},setup(r){let i=r;function o(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`)}function s(e){let t=o(e);return t=t.replace(/\*\*(.+?)\*\*/g,`<strong class="ws-accent">$1</strong>`),t=t.replace(/__(.+?)__/g,`<strong class="ws-accent">$1</strong>`),t=t.replace(/`([^`]+?)`/g,`<code class="ws-code">$1</code>`),t}let c=t(()=>{let e=String(i.block.content||``).replace(/\r\n/g,`
2
  `).trim();if(!e)return``;let t=e.split(`
3
  `),n=[],r=null,a=()=>{r&&=(n.push(`</${r}>`),null)};for(let e of t){let t=e.match(/^\s*#{1,3}\s+(.*)$/),i=e.match(/^\s*(\d+)[.)]\s+(.*)$/),o=e.match(/^\s*[-*•]\s+(.*)$/);t?(a(),n.push(`<div class="ws-h">${s(t[1])}</div>`)):i?(r!==`ol`&&(a(),n.push(`<ol class="ws-ol">`),r=`ol`),n.push(`<li>${s(i[2])}</li>`)):o?(r!==`ul`&&(a(),n.push(`<ul class="ws-bullets">`),r=`ul`),n.push(`<li>${s(o[1])}</li>`)):(a(),e.trim()&&n.push(`<p>${s(e)}</p>`))}return a(),n.join(``)});return(t,r)=>(e(),n(`div`,{class:`ws-text leading-relaxed motion-safe:transition-opacity motion-safe:duration-300`,innerHTML:c.value},null,8,a))}}),[[`__scopeId`,`data-v-51d1042a`]]);export{o as default};
 
1
+ import{M as e,c as t,f as n,v as r}from"./runtime-core.esm-bundler-olIhRSij.js";import{r as i}from"./index-nrc-cgMW.js";var a=[`innerHTML`],o=i(r({__name:`TextBlock`,props:{block:{}},setup(r){let i=r;function o(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`)}function s(e){let t=o(e);return t=t.replace(/\*\*(.+?)\*\*/g,`<strong class="ws-accent">$1</strong>`),t=t.replace(/__(.+?)__/g,`<strong class="ws-accent">$1</strong>`),t=t.replace(/`([^`]+?)`/g,`<code class="ws-code">$1</code>`),t}let c=t(()=>{let e=String(i.block.content||``).replace(/\r\n/g,`
2
  `).trim();if(!e)return``;let t=e.split(`
3
  `),n=[],r=null,a=()=>{r&&=(n.push(`</${r}>`),null)};for(let e of t){let t=e.match(/^\s*#{1,3}\s+(.*)$/),i=e.match(/^\s*(\d+)[.)]\s+(.*)$/),o=e.match(/^\s*[-*•]\s+(.*)$/);t?(a(),n.push(`<div class="ws-h">${s(t[1])}</div>`)):i?(r!==`ol`&&(a(),n.push(`<ol class="ws-ol">`),r=`ol`),n.push(`<li>${s(i[2])}</li>`)):o?(r!==`ul`&&(a(),n.push(`<ul class="ws-bullets">`),r=`ul`),n.push(`<li>${s(o[1])}</li>`)):(a(),e.trim()&&n.push(`<p>${s(e)}</p>`))}return a(),n.join(``)});return(t,r)=>(e(),n(`div`,{class:`ws-text leading-relaxed motion-safe:transition-opacity motion-safe:duration-300`,innerHTML:c.value},null,8,a))}}),[[`__scopeId`,`data-v-51d1042a`]]);export{o as default};
vivek/frontend-vue/dist/assets/{index-BZhxAMIz.css → index-CA-wuNor.css} RENAMED
The diff for this file is too large to render. See raw diff
 
vivek/frontend-vue/dist/assets/{index-oLTRTdCR.js → index-nrc-cgMW.js} RENAMED
The diff for this file is too large to render. See raw diff
 
vivek/frontend-vue/dist/index.html CHANGED
@@ -5,9 +5,9 @@
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>frontend-vue</title>
8
- <script type="module" crossorigin src="/assets/index-oLTRTdCR.js"></script>
9
  <link rel="modulepreload" crossorigin href="/assets/runtime-core.esm-bundler-olIhRSij.js">
10
- <link rel="stylesheet" crossorigin href="/assets/index-BZhxAMIz.css">
11
  </head>
12
  <body>
13
  <div id="app"></div>
 
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>frontend-vue</title>
8
+ <script type="module" crossorigin src="/assets/index-nrc-cgMW.js"></script>
9
  <link rel="modulepreload" crossorigin href="/assets/runtime-core.esm-bundler-olIhRSij.js">
10
+ <link rel="stylesheet" crossorigin href="/assets/index-CA-wuNor.css">
11
  </head>
12
  <body>
13
  <div id="app"></div>
vivek/frontend-vue/src/lib/echartsOption.ts CHANGED
@@ -32,8 +32,8 @@ export type ChartSpec = {
32
  root?: TreeNodeSpec
33
  candles?: (number | string)[][]
34
  boxes?: (number | string)[][]
35
- nodes?: ({ name?: string } | string)[]
36
- links?: { source?: string | number; target?: string | number; value?: number }[]
37
  }
38
 
39
  // Fixed enterprise palette — the SINGLE source of chart colors. The LLM never
@@ -86,6 +86,45 @@ function colorAt(i: number, total = 0): string {
86
  // in the widget JSON is ignored — colorAt() assigns deterministically by index,
87
  // so the look is identical on every render and in the HTML export.
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  function toNum(v: unknown): number | null {
90
  if (typeof v === 'number') return Number.isFinite(v) ? v : null
91
  if (typeof v === 'string') {
@@ -250,6 +289,7 @@ export function chartHasRenderableData(chart: ChartSpec): boolean {
250
  if (k === 'candlestick') return Array.isArray(c.candles) && c.candles.length > 0
251
  if (k === 'boxplot') return Array.isArray(c.boxes) && c.boxes.length > 0
252
  if (k === 'sankey' || k === 'graph') return Array.isArray(c.links) && c.links.length > 0
 
253
  if (k === 'treemap' || k === 'sunburst') return (c.items?.length || 0) > 0 || pieData(chart).length > 0
254
  if (k === 'pie' || k === 'donut' || k === 'funnel' || k === 'waterfall' || k === 'rose') return pieData(chart).length > 0
255
  if (k === 'tree' || k === 'mindmap' || k === 'org' || k === 'orgchart') return treeRoot(chart) != null
@@ -324,7 +364,7 @@ function buildEChartsOptionRaw(chart: ChartSpec, title = '', opts: { dark?: bool
324
  radius: k === 'donut' ? ['42%', '70%'] : '65%',
325
  center: ['50%', '46%'],
326
  data: d.map((x, i) => ({ ...x, itemStyle: { color: colorAt(i, d.length) } })),
327
- label: { fontSize: 10, color: tc },
328
  },
329
  ],
330
  }
@@ -347,7 +387,7 @@ function buildEChartsOptionRaw(chart: ChartSpec, title = '', opts: { dark?: bool
347
  bottom: 40,
348
  sort: 'descending',
349
  gap: 2,
350
- label: { show: true, position: 'inside', fontSize: 10 },
351
  data: d.map((x, i) => ({ ...x, itemStyle: { color: colorAt(i, d.length) } })),
352
  },
353
  ],
@@ -445,7 +485,7 @@ function buildEChartsOptionRaw(chart: ChartSpec, title = '', opts: { dark?: bool
445
  series: [
446
  k === 'sunburst'
447
  ? { type: 'sunburst', data: d, radius: [0, '92%'], label: { fontSize: 10 } }
448
- : { type: 'treemap', data: d, breadcrumb: { show: false }, roam: false, label: { fontSize: 11 } },
449
  ],
450
  }
451
  }
@@ -478,6 +518,99 @@ function buildEChartsOptionRaw(chart: ChartSpec, title = '', opts: { dark?: bool
478
  }
479
  }
480
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  if (k === 'waterfall') {
482
  const pts = pieData(chart)
483
  const cats = pts.map((p) => p.name)
@@ -565,7 +698,7 @@ function buildEChartsOptionRaw(chart: ChartSpec, title = '', opts: { dark?: bool
565
  roseType: 'area',
566
  itemStyle: { borderRadius: 4 },
567
  data: d.map((x, i) => ({ ...x, itemStyle: { color: colorAt(i, d.length) } })),
568
- label: { fontSize: 10, color: tc },
569
  },
570
  ],
571
  }
@@ -712,6 +845,11 @@ function buildEChartsOptionRaw(chart: ChartSpec, title = '', opts: { dark?: bool
712
  const cats = cartesianCategories(chart)
713
  const useCat = Array.isArray(cats) && cats.length > 0
714
 
 
 
 
 
 
715
  const eSeries = series.map((s, i) => {
716
  const seriesType = (
717
  isCombo ? (s.kind === 'line' ? 'line' : s.kind === 'bar' ? 'bar' : i === 0 ? 'bar' : 'line')
@@ -720,18 +858,35 @@ function buildEChartsOptionRaw(chart: ChartSpec, title = '', opts: { dark?: bool
720
  : 'line'
721
  ) as 'bar' | 'scatter' | 'line'
722
  const asLine = seriesType === 'line'
 
723
  return {
724
  name: s.name,
725
  type: seriesType,
726
  data: useCat ? s.points.map((p) => p[1]) : s.points,
727
  stack: isStacked ? 'total' : undefined,
728
- smooth: asLine,
729
- showSymbol: seriesType === 'scatter' || asLine,
730
- symbolSize: seriesType === 'scatter' ? 10 : 6,
731
- itemStyle: { color: s.color },
732
- areaStyle: isArea && asLine ? { opacity: 0.18, color: s.color } : undefined,
733
- lineStyle: asLine ? { width: 2 } : undefined,
734
- emphasis: { focus: 'series' as const },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
735
  animationDuration: 900,
736
  animationEasing: 'cubicOut' as const,
737
  }
 
32
  root?: TreeNodeSpec
33
  candles?: (number | string)[][]
34
  boxes?: (number | string)[][]
35
+ nodes?: ({ name?: string; level?: number } | string)[]
36
+ links?: { source?: string | number; target?: string | number; value?: number; label?: string }[]
37
  }
38
 
39
  // Fixed enterprise palette — the SINGLE source of chart colors. The LLM never
 
86
  // in the widget JSON is ignored — colorAt() assigns deterministically by index,
87
  // so the look is identical on every render and in the HTML export.
88
 
89
+ /** Compact human number for on-chart value labels: 47.5, 1.2k, 3.4M, 2.1B. */
90
+ function fmtCompact(v: unknown): string {
91
+ const n = Number(v)
92
+ if (!Number.isFinite(n)) return ''
93
+ const a = Math.abs(n)
94
+ if (a >= 1e9) return (n / 1e9).toFixed(a >= 1e10 ? 0 : 1).replace(/\.0$/, '') + 'B'
95
+ if (a >= 1e6) return (n / 1e6).toFixed(a >= 1e7 ? 0 : 1).replace(/\.0$/, '') + 'M'
96
+ if (a >= 1e3) return (n / 1e3).toFixed(a >= 1e4 ? 0 : 1).replace(/\.0$/, '') + 'k'
97
+ return String(Math.round(n * 100) / 100)
98
+ }
99
+
100
+ /** Add alpha to a hex or hsl() color (used for gradient area fills). */
101
+ function withAlpha(color: string, a: number): string {
102
+ if (!color) return color
103
+ if (color.startsWith('#')) {
104
+ let h = color.slice(1)
105
+ if (h.length === 3) h = h.split('').map((c) => c + c).join('')
106
+ const n = parseInt(h, 16)
107
+ return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a})`
108
+ }
109
+ if (color.startsWith('hsl(')) return color.replace('hsl(', 'hsla(').replace(')', `, ${a})`)
110
+ return color
111
+ }
112
+
113
+ /** Vertical (or horizontal) gradient object ECharts accepts directly in options. */
114
+ function areaGradient(color: string, horizontal = false): Record<string, unknown> {
115
+ return {
116
+ type: 'linear',
117
+ x: 0,
118
+ y: 0,
119
+ x2: horizontal ? 1 : 0,
120
+ y2: horizontal ? 0 : 1,
121
+ colorStops: [
122
+ { offset: 0, color: withAlpha(color, 0.38) },
123
+ { offset: 1, color: withAlpha(color, 0.03) },
124
+ ],
125
+ }
126
+ }
127
+
128
  function toNum(v: unknown): number | null {
129
  if (typeof v === 'number') return Number.isFinite(v) ? v : null
130
  if (typeof v === 'string') {
 
289
  if (k === 'candlestick') return Array.isArray(c.candles) && c.candles.length > 0
290
  if (k === 'boxplot') return Array.isArray(c.boxes) && c.boxes.length > 0
291
  if (k === 'sankey' || k === 'graph') return Array.isArray(c.links) && c.links.length > 0
292
+ if (k === 'flow' || k === 'flowchart' || k === 'process') return Array.isArray(c.links) && c.links.length > 0
293
  if (k === 'treemap' || k === 'sunburst') return (c.items?.length || 0) > 0 || pieData(chart).length > 0
294
  if (k === 'pie' || k === 'donut' || k === 'funnel' || k === 'waterfall' || k === 'rose') return pieData(chart).length > 0
295
  if (k === 'tree' || k === 'mindmap' || k === 'org' || k === 'orgchart') return treeRoot(chart) != null
 
364
  radius: k === 'donut' ? ['42%', '70%'] : '65%',
365
  center: ['50%', '46%'],
366
  data: d.map((x, i) => ({ ...x, itemStyle: { color: colorAt(i, d.length) } })),
367
+ label: { fontSize: 10, color: tc, formatter: '{b}: {d}%' },
368
  },
369
  ],
370
  }
 
387
  bottom: 40,
388
  sort: 'descending',
389
  gap: 2,
390
+ label: { show: true, position: 'inside', fontSize: 10, color: '#fff', formatter: '{b}: {c}' },
391
  data: d.map((x, i) => ({ ...x, itemStyle: { color: colorAt(i, d.length) } })),
392
  },
393
  ],
 
485
  series: [
486
  k === 'sunburst'
487
  ? { type: 'sunburst', data: d, radius: [0, '92%'], label: { fontSize: 10 } }
488
+ : { type: 'treemap', data: d, breadcrumb: { show: false }, roam: false, label: { fontSize: 11, formatter: '{b}\n{c}' } },
489
  ],
490
  }
491
  }
 
518
  }
519
  }
520
 
521
+ if (k === 'flow' || k === 'flowchart' || k === 'process') {
522
+ // Flowchart: directed graph laid out in LEFT→RIGHT layers (process steps),
523
+ // with arrowed edges. Nodes are boxes; the model supplies nodes + links
524
+ // (optionally a per-node `level` and per-link `label`).
525
+ type FlowNode = { name: string; level?: number }
526
+ const nodes: FlowNode[] = (chart?.nodes || []).map((n) =>
527
+ typeof n === 'string' ? { name: n } : { name: n.name || '', level: n.level },
528
+ )
529
+ const links = (chart?.links || []).map((l) => ({
530
+ source: String(l.source ?? ''),
531
+ target: String(l.target ?? ''),
532
+ label: l.label,
533
+ }))
534
+ const byName = new Map(nodes.map((n) => [n.name, n]))
535
+ for (const l of links) {
536
+ for (const e of [l.source, l.target]) {
537
+ if (e && !byName.has(e)) {
538
+ const nn = { name: e }
539
+ nodes.push(nn)
540
+ byName.set(e, nn)
541
+ }
542
+ }
543
+ }
544
+
545
+ // Depth per node: explicit `level`, else longest path from a source (relaxation).
546
+ const depth = new Map<string, number>()
547
+ const incoming = new Map<string, number>()
548
+ nodes.forEach((n) => incoming.set(n.name, 0))
549
+ links.forEach((l) => incoming.set(l.target, (incoming.get(l.target) || 0) + 1))
550
+ nodes.forEach((n) => depth.set(n.name, typeof n.level === 'number' ? n.level : incoming.get(n.name) === 0 ? 0 : 0))
551
+ for (let pass = 0; pass < nodes.length; pass++) {
552
+ let changed = false
553
+ for (const l of links) {
554
+ const d = Math.max(depth.get(l.target) || 0, (depth.get(l.source) || 0) + 1)
555
+ if (d !== depth.get(l.target)) {
556
+ depth.set(l.target, d)
557
+ changed = true
558
+ }
559
+ }
560
+ if (!changed) break
561
+ }
562
+
563
+ // Lay out: x by depth (column), y spread evenly within each column.
564
+ const cols = new Map<number, string[]>()
565
+ nodes.forEach((n) => {
566
+ const d = depth.get(n.name) || 0
567
+ if (!cols.has(d)) cols.set(d, [])
568
+ cols.get(d)!.push(n.name)
569
+ })
570
+ const maxDepth = Math.max(0, ...Array.from(cols.keys()))
571
+ const placed = nodes.map((n, i) => {
572
+ const d = depth.get(n.name) || 0
573
+ const col = cols.get(d)!
574
+ const row = col.indexOf(n.name)
575
+ const x = maxDepth > 0 ? (d / maxDepth) * 100 : 50
576
+ const y = col.length > 1 ? (row / (col.length - 1)) * 100 : 50
577
+ return {
578
+ name: n.name,
579
+ x,
580
+ y,
581
+ value: 1,
582
+ symbol: 'roundRect',
583
+ symbolSize: [Math.min(120, Math.max(54, n.name.length * 7)), 34] as [number, number],
584
+ itemStyle: { color: colorAt(i, nodes.length), borderColor: 'rgba(255,255,255,0.65)', borderWidth: 1 },
585
+ label: { show: true, color: '#fff', fontSize: 11, fontWeight: 600 as const, overflow: 'truncate' as const, width: 110 },
586
+ }
587
+ })
588
+
589
+ return {
590
+ backgroundColor: 'transparent',
591
+ textStyle: { color: tc, fontSize: 11 },
592
+ tooltip: { trigger: 'item' },
593
+ series: [
594
+ {
595
+ type: 'graph',
596
+ layout: 'none',
597
+ roam: true,
598
+ data: placed,
599
+ links: links.map((l) => ({
600
+ source: l.source,
601
+ target: l.target,
602
+ label: l.label ? { show: true, formatter: l.label, fontSize: 9, color: tc } : undefined,
603
+ lineStyle: { color: dark ? 'rgba(255,255,255,0.45)' : 'rgba(71,85,105,0.6)', width: 1.5, curveness: 0.05 },
604
+ })),
605
+ edgeSymbol: ['none', 'arrow'],
606
+ edgeSymbolSize: [0, 9],
607
+ emphasis: { focus: 'adjacency' },
608
+ label: { position: 'inside' },
609
+ },
610
+ ],
611
+ } as EChartsOption
612
+ }
613
+
614
  if (k === 'waterfall') {
615
  const pts = pieData(chart)
616
  const cats = pts.map((p) => p.name)
 
698
  roseType: 'area',
699
  itemStyle: { borderRadius: 4 },
700
  data: d.map((x, i) => ({ ...x, itemStyle: { color: colorAt(i, d.length) } })),
701
+ label: { fontSize: 10, color: tc, formatter: '{b}: {d}%' },
702
  },
703
  ],
704
  }
 
845
  const cats = cartesianCategories(chart)
846
  const useCat = Array.isArray(cats) && cats.length > 0
847
 
848
+ // Show value labels on the chart when it won't get cluttered.
849
+ const catCount = cats?.length ?? series[0]?.points.length ?? 0
850
+ const fewBars = catCount > 0 && catCount <= 12 && series.length <= 4
851
+ const fewPts = catCount > 0 && catCount <= 8 && series.length <= 2
852
+
853
  const eSeries = series.map((s, i) => {
854
  const seriesType = (
855
  isCombo ? (s.kind === 'line' ? 'line' : s.kind === 'bar' ? 'bar' : i === 0 ? 'bar' : 'line')
 
858
  : 'line'
859
  ) as 'bar' | 'scatter' | 'line'
860
  const asLine = seriesType === 'line'
861
+ const isBarSeries = seriesType === 'bar'
862
  return {
863
  name: s.name,
864
  type: seriesType,
865
  data: useCat ? s.points.map((p) => p[1]) : s.points,
866
  stack: isStacked ? 'total' : undefined,
867
+ smooth: asLine ? 0.35 : undefined,
868
+ showSymbol: seriesType === 'scatter' || (asLine && fewPts),
869
+ symbolSize: seriesType === 'scatter' ? 10 : 7,
870
+ // Numbers ON the chart so values are always visible (not only on hover).
871
+ label: isBarSeries
872
+ ? {
873
+ show: isStacked ? catCount <= 8 : fewBars,
874
+ position: (isStacked ? 'inside' : isHBar ? 'right' : 'top') as 'inside' | 'right' | 'top',
875
+ fontSize: 10,
876
+ fontWeight: 600 as const,
877
+ color: isStacked ? '#fff' : tc,
878
+ formatter: (p: any) => fmtCompact(p.value),
879
+ }
880
+ : asLine
881
+ ? { show: fewPts, position: 'top' as const, fontSize: 10, color: tc, formatter: (p: any) => fmtCompact(p.value) }
882
+ : undefined,
883
+ itemStyle: {
884
+ color: s.color,
885
+ borderRadius: isBarSeries ? (isHBar ? [0, 4, 4, 0] : [4, 4, 0, 0]) : undefined,
886
+ },
887
+ areaStyle: isArea && asLine ? { color: areaGradient(s.color) } : undefined,
888
+ lineStyle: asLine ? { width: 2.5 } : undefined,
889
+ emphasis: { focus: 'series' as const, scale: seriesType === 'scatter', itemStyle: { shadowBlur: 10, shadowColor: withAlpha(s.color, 0.5) } },
890
  animationDuration: 900,
891
  animationEasing: 'cubicOut' as const,
892
  }
vivek/frontend-vue/src/pages/Chat.vue CHANGED
@@ -102,6 +102,7 @@ type PlainMsg = {
102
  visualHtml?: string
103
  streaming?: boolean
104
  codeStream?: string
 
105
  }
106
 
107
  type ChatMessage = {
@@ -153,6 +154,17 @@ function closeTechPanels() {
153
 
154
  const baselineScrollEl = ref<HTMLDivElement | null>(null)
155
  const adaptiveScrollEl = ref<HTMLDivElement | null>(null)
 
 
 
 
 
 
 
 
 
 
 
156
  const baselineStickToBottom = ref(true)
157
  const adaptiveStickToBottom = ref(true)
158
  const streamPulseEl = ref<HTMLDivElement | null>(null)
@@ -377,6 +389,40 @@ async function submitReward(m: ChatMessage, reward: 0 | 1) {
377
  }
378
  }
379
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  /** Split the raw baseline stream into the answer text and the (possibly partial) HTML being typed. */
381
  function splitBaselineRaw(raw: string): { text: string; code: string } {
382
  const open = raw.indexOf('<VISUAL>')
@@ -445,7 +491,9 @@ async function runChatPlain(text: string): Promise<void> {
445
  msg().error = e.error || 'Baseline error'
446
  } else if (e.type === 'done') {
447
  msg().content = typeof e.response === 'string' ? e.response : msg().content
448
- msg().visualHtml = typeof e.visual_html === 'string' && e.visual_html ? e.visual_html : undefined
 
 
449
  msg().elapsed = typeof e.elapsed === 'number' ? e.elapsed : undefined
450
  msg().codeStream = ''
451
  msg().streaming = false
@@ -888,6 +936,7 @@ onMounted(async () => {
888
 
889
  window.addEventListener('keydown', onGlobalKeydown)
890
  window.addEventListener('chat:control', onShellControl as EventListener)
 
891
  })
892
 
893
  onUnmounted(() => {
@@ -895,6 +944,7 @@ onUnmounted(() => {
895
  killAnimationsOf(streamPulseEl.value)
896
  window.removeEventListener('keydown', onGlobalKeydown)
897
  window.removeEventListener('chat:control', onShellControl as EventListener)
 
898
  })
899
 
900
  function onShellControl(e: Event) {
@@ -1175,13 +1225,13 @@ function downloadLiveWidgetDraft(m: ChatMessage, idx: number) {
1175
  </div>
1176
  <iframe
1177
  v-if="!m.streaming && m.visualHtml"
1178
- :srcdoc="m.visualHtml"
1179
  sandbox="allow-scripts"
1180
  referrerpolicy="no-referrer"
1181
  loading="lazy"
1182
  title="Baseline visualization"
1183
  class="w-full mt-2 rounded-xl border bg-white"
1184
- style="height: clamp(340px, 52vh, 560px)"
1185
  />
1186
  <div v-if="m.elapsed != null" class="text-[10px] text-muted-foreground mt-1">{{ m.elapsed }}s</div>
1187
  </div>
 
102
  visualHtml?: string
103
  streaming?: boolean
104
  codeStream?: string
105
+ vizId?: string
106
  }
107
 
108
  type ChatMessage = {
 
154
 
155
  const baselineScrollEl = ref<HTMLDivElement | null>(null)
156
  const adaptiveScrollEl = ref<HTMLDivElement | null>(null)
157
+
158
+ // Baseline iframe auto-resize: each visual posts its content height (by id) and
159
+ // we grow/shrink that iframe to match.
160
+ const vizHeights = ref<Record<string, number>>({})
161
+ let vizCounter = 0
162
+ function onVizMessage(e: MessageEvent) {
163
+ const d = e.data
164
+ if (d && typeof d === 'object' && typeof d.__vizId === 'string' && typeof d.__vizHeight === 'number') {
165
+ vizHeights.value[d.__vizId] = Math.max(160, Math.min(1600, Math.round(d.__vizHeight)))
166
+ }
167
+ }
168
  const baselineStickToBottom = ref(true)
169
  const adaptiveStickToBottom = ref(true)
170
  const streamPulseEl = ref<HTMLDivElement | null>(null)
 
389
  }
390
  }
391
 
392
+ /**
393
+ * Prepare the LLM's HTML for a self-sizing iframe: normalize margins/scrollbars,
394
+ * and inject a small reporter that postMessages the content height (tagged with
395
+ * `id`) so the parent can grow/shrink the iframe to the chart's natural height.
396
+ */
397
+ function normalizeBaselineHtml(html?: string, id?: string): string {
398
+ if (!html) return ''
399
+ const css =
400
+ '<style id="__fit">' +
401
+ 'html,body{margin:0!important;padding:0!important;width:100%!important;background:#fff!important;overflow:hidden!important}' +
402
+ '*{box-sizing:border-box}body>*{max-width:100%}canvas{max-width:100%!important}' +
403
+ '.js-plotly-plot,.plot-container{width:100%!important}' +
404
+ '</style>'
405
+ const vid = (id || '').replace(/[^a-zA-Z0-9_-]/g, '')
406
+ const reporter =
407
+ '<script>(function(){var ID=' +
408
+ JSON.stringify(vid) +
409
+ ';function post(){try{var h=Math.max(document.documentElement.scrollHeight,(document.body?document.body.scrollHeight:0));' +
410
+ 'parent.postMessage({__vizId:ID,__vizHeight:h},"*");}catch(e){}}' +
411
+ 'window.addEventListener("load",function(){post();setTimeout(post,150);setTimeout(post,600);setTimeout(post,1500);});' +
412
+ 'window.addEventListener("resize",post);' +
413
+ 'if(window.ResizeObserver){try{new ResizeObserver(post).observe(document.documentElement);}catch(e){}}' +
414
+ 'setTimeout(post,300);})();<\/script>'
415
+ let out = html
416
+ if (/<\/head>/i.test(out)) out = out.replace(/<\/head>/i, css + '</head>')
417
+ else if (/<head[^>]*>/i.test(out)) out = out.replace(/<head[^>]*>/i, (m) => m + css)
418
+ else if (/<html[^>]*>/i.test(out)) out = out.replace(/<html[^>]*>/i, (m) => m + '<head>' + css + '</head>')
419
+ else if (/<body[^>]*>/i.test(out)) out = out.replace(/<body[^>]*>/i, (m) => m + css)
420
+ else out = css + out
421
+ if (/<\/body>/i.test(out)) out = out.replace(/<\/body>/i, reporter + '</body>')
422
+ else out = out + reporter
423
+ return out
424
+ }
425
+
426
  /** Split the raw baseline stream into the answer text and the (possibly partial) HTML being typed. */
427
  function splitBaselineRaw(raw: string): { text: string; code: string } {
428
  const open = raw.indexOf('<VISUAL>')
 
491
  msg().error = e.error || 'Baseline error'
492
  } else if (e.type === 'done') {
493
  msg().content = typeof e.response === 'string' ? e.response : msg().content
494
+ const html = typeof e.visual_html === 'string' && e.visual_html ? e.visual_html : undefined
495
+ msg().visualHtml = html
496
+ if (html) msg().vizId = 'viz' + ++vizCounter
497
  msg().elapsed = typeof e.elapsed === 'number' ? e.elapsed : undefined
498
  msg().codeStream = ''
499
  msg().streaming = false
 
936
 
937
  window.addEventListener('keydown', onGlobalKeydown)
938
  window.addEventListener('chat:control', onShellControl as EventListener)
939
+ window.addEventListener('message', onVizMessage)
940
  })
941
 
942
  onUnmounted(() => {
 
944
  killAnimationsOf(streamPulseEl.value)
945
  window.removeEventListener('keydown', onGlobalKeydown)
946
  window.removeEventListener('chat:control', onShellControl as EventListener)
947
+ window.removeEventListener('message', onVizMessage)
948
  })
949
 
950
  function onShellControl(e: Event) {
 
1225
  </div>
1226
  <iframe
1227
  v-if="!m.streaming && m.visualHtml"
1228
+ :srcdoc="normalizeBaselineHtml(m.visualHtml, m.vizId)"
1229
  sandbox="allow-scripts"
1230
  referrerpolicy="no-referrer"
1231
  loading="lazy"
1232
  title="Baseline visualization"
1233
  class="w-full mt-2 rounded-xl border bg-white"
1234
+ :style="{ height: (vizHeights[m.vizId || ''] || 420) + 'px', transition: 'height 0.2s ease' }"
1235
  />
1236
  <div v-if="m.elapsed != null" class="text-[10px] text-muted-foreground mt-1">{{ m.elapsed }}s</div>
1237
  </div>
vivek/frontend-vue/src/widget-registry.json CHANGED
@@ -52,6 +52,7 @@
52
  "sunburst",
53
  "sankey",
54
  "graph",
 
55
  "boxplot",
56
  "bubble",
57
  "histogram",
@@ -89,6 +90,8 @@
89
  " { \"type\": \"chart\", \"chart\": { \"kind\": \"bubble\", \"x_label\": \"Risk\", \"y_label\": \"Return\", \"series\": [ { \"name\": \"Funds\", \"values\": [ [8,12,500], [14,18,1200] ] } ] } }",
90
  "- sankey | graph (flows / network) -> \"nodes\" + \"links\":",
91
  " { \"type\": \"chart\", \"chart\": { \"kind\": \"sankey\", \"nodes\": [ {\"name\":\"Revenue\"}, {\"name\":\"COGS\"}, {\"name\":\"Gross Profit\"} ], \"links\": [ {\"source\":\"Revenue\",\"target\":\"COGS\",\"value\":40}, {\"source\":\"Revenue\",\"target\":\"Gross Profit\",\"value\":60} ] } }",
 
 
92
  "- histogram (frequency) -> like bar: \"x_categories\" (bins) + one series of counts.",
93
  "- timeseries (interactive time trend with zoom/pan) -> like line: \"x_categories\" (dates) + series of numbers. Best for many points over time.",
94
  "- rose (nightingale / 'pizza' chart — share with variable-radius slices) -> use \"items\": [ { \"label\", \"value\" } ].",
 
52
  "sunburst",
53
  "sankey",
54
  "graph",
55
+ "flow",
56
  "boxplot",
57
  "bubble",
58
  "histogram",
 
90
  " { \"type\": \"chart\", \"chart\": { \"kind\": \"bubble\", \"x_label\": \"Risk\", \"y_label\": \"Return\", \"series\": [ { \"name\": \"Funds\", \"values\": [ [8,12,500], [14,18,1200] ] } ] } }",
91
  "- sankey | graph (flows / network) -> \"nodes\" + \"links\":",
92
  " { \"type\": \"chart\", \"chart\": { \"kind\": \"sankey\", \"nodes\": [ {\"name\":\"Revenue\"}, {\"name\":\"COGS\"}, {\"name\":\"Gross Profit\"} ], \"links\": [ {\"source\":\"Revenue\",\"target\":\"COGS\",\"value\":40}, {\"source\":\"Revenue\",\"target\":\"Gross Profit\",\"value\":60} ] } }",
93
+ "- flow (a process / decision flowchart, laid out left-to-right with arrows) -> \"nodes\" + directed \"links\". Optionally give each node a \"level\" (0,1,2…) to force its column, and each link a \"label\" (e.g. \"Yes\"/\"No\"):",
94
+ " { \"type\": \"chart\", \"chart\": { \"kind\": \"flow\", \"nodes\": [ {\"name\":\"Start\"}, {\"name\":\"Review\"}, {\"name\":\"Approve\"}, {\"name\":\"Reject\"} ], \"links\": [ {\"source\":\"Start\",\"target\":\"Review\"}, {\"source\":\"Review\",\"target\":\"Approve\",\"label\":\"Yes\"}, {\"source\":\"Review\",\"target\":\"Reject\",\"label\":\"No\"} ] } }",
95
  "- histogram (frequency) -> like bar: \"x_categories\" (bins) + one series of counts.",
96
  "- timeseries (interactive time trend with zoom/pan) -> like line: \"x_categories\" (dates) + series of numbers. Best for many points over time.",
97
  "- rose (nightingale / 'pizza' chart — share with variable-radius slices) -> use \"items\": [ { \"label\", \"value\" } ].",