@carto/ps-react-ui 4.11.3 → 4.12.0
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.
- package/dist/chat.js +962 -733
- package/dist/chat.js.map +1 -1
- package/dist/csv-item-hH_Gt7ur.js +32 -0
- package/dist/csv-item-hH_Gt7ur.js.map +1 -0
- package/dist/png-item-9dNbB37T.js +57 -0
- package/dist/png-item-9dNbB37T.js.map +1 -0
- package/dist/table-B3ZWWhJt.js +383 -0
- package/dist/table-B3ZWWhJt.js.map +1 -0
- package/dist/types/chat/containers/chat-footer.d.ts +1 -1
- package/dist/types/chat/containers/styles.d.ts +79 -12
- package/dist/types/chat/index.d.ts +1 -1
- package/dist/types/chat/types.d.ts +21 -0
- package/dist/types/chat/use-typewriter.d.ts +5 -3
- package/dist/types/widgets-v2/actions/download/constants.d.ts +12 -0
- package/dist/types/widgets-v2/actions/download/csv-item.d.ts +38 -0
- package/dist/types/widgets-v2/actions/download/icons.d.ts +6 -0
- package/dist/types/widgets-v2/actions/download/index.d.ts +3 -1
- package/dist/types/widgets-v2/actions/index.d.ts +1 -1
- package/dist/widgets-v2/actions.js +40 -36
- package/dist/widgets-v2/actions.js.map +1 -1
- package/dist/widgets-v2/bar.js +77 -84
- package/dist/widgets-v2/bar.js.map +1 -1
- package/dist/widgets-v2/category.js +50 -55
- package/dist/widgets-v2/category.js.map +1 -1
- package/dist/widgets-v2/formula.js +37 -43
- package/dist/widgets-v2/formula.js.map +1 -1
- package/dist/widgets-v2/histogram.js +138 -144
- package/dist/widgets-v2/histogram.js.map +1 -1
- package/dist/widgets-v2/markdown.js +18 -17
- package/dist/widgets-v2/markdown.js.map +1 -1
- package/dist/widgets-v2/pie.js +67 -73
- package/dist/widgets-v2/pie.js.map +1 -1
- package/dist/widgets-v2/scatterplot.js +75 -81
- package/dist/widgets-v2/scatterplot.js.map +1 -1
- package/dist/widgets-v2/spread.js +36 -41
- package/dist/widgets-v2/spread.js.map +1 -1
- package/dist/widgets-v2/table.js +46 -55
- package/dist/widgets-v2/table.js.map +1 -1
- package/dist/widgets-v2/timeseries.js +81 -87
- package/dist/widgets-v2/timeseries.js.map +1 -1
- package/dist/widgets-v2.js +1 -1
- package/package.json +1 -1
- package/src/chat/bubbles/styles.ts +5 -1
- package/src/chat/containers/chat-content.tsx +4 -1
- package/src/chat/containers/chat-footer.test.tsx +59 -0
- package/src/chat/containers/chat-footer.tsx +124 -36
- package/src/chat/containers/styles.ts +107 -16
- package/src/chat/feedback/styles.ts +11 -4
- package/src/chat/index.ts +1 -0
- package/src/chat/types.ts +22 -0
- package/src/chat/use-typewriter.ts +32 -24
- package/src/widgets-v2/actions/download/constants.ts +14 -0
- package/src/widgets-v2/actions/download/csv-item.test.tsx +77 -0
- package/src/widgets-v2/actions/download/csv-item.tsx +71 -0
- package/src/widgets-v2/actions/download/icons.tsx +10 -1
- package/src/widgets-v2/actions/download/index.ts +3 -1
- package/src/widgets-v2/actions/download/png-item.tsx +2 -1
- package/src/widgets-v2/actions/index.ts +5 -0
- package/src/widgets-v2/bar/download.tsx +16 -22
- package/src/widgets-v2/category/download.test.ts +9 -0
- package/src/widgets-v2/category/download.ts +16 -20
- package/src/widgets-v2/formula/download.tsx +23 -29
- package/src/widgets-v2/histogram/download.ts +22 -26
- package/src/widgets-v2/markdown/{download.ts → download.tsx} +5 -2
- package/src/widgets-v2/pie/download.ts +16 -20
- package/src/widgets-v2/scatterplot/download.ts +16 -20
- package/src/widgets-v2/spread/download.ts +23 -27
- package/src/widgets-v2/table/download.test.ts +10 -0
- package/src/widgets-v2/table/download.ts +11 -15
- package/src/widgets-v2/table/helpers.test.ts +19 -0
- package/src/widgets-v2/table/helpers.ts +7 -12
- package/src/widgets-v2/timeseries/download.ts +36 -40
- package/dist/png-item-BE9uEqlD.js +0 -45
- package/dist/png-item-BE9uEqlD.js.map +0 -1
- package/dist/table-C9IMbTr0.js +0 -385
- package/dist/table-C9IMbTr0.js.map +0 -1
- package/dist/types/chat/feedback/styles.d.ts +0 -211
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeseries.js","sources":["../../src/widgets-v2/timeseries/options.ts","../../src/widgets-v2/timeseries/skeleton.tsx","../../src/widgets-v2/timeseries/download.ts"],"sourcesContent":["import type { EChartsOption } from 'echarts'\nimport * as echarts from 'echarts'\nimport type { CallbackDataParams } from 'echarts/types/dist/shared'\nimport {\n buildGridConfig,\n buildLegendConfig,\n createTooltipFormatter,\n createTooltipPositioner,\n niceNum,\n} from '../../widgets/utils/chart-config'\nimport { ZOOM_LAYOUT } from '../actions/zoom-toggle'\nimport type { OptionFactory } from '../echart'\nimport { mergeOptions, resolveThemeColor } from '../utils'\nimport { positionDataZoomForLegend } from '../utils/data-zoom-layout'\nimport type {\n TimeseriesEChartsOption,\n TimeseriesOptionFactoryInput,\n TimeseriesOptionsInput,\n TimeseriesWidgetData,\n} from './types'\n\n/**\n * Builds the **structural** ECharts option for a timeseries widget —\n * time x-axis, value y-axis, themed tooltip, themed legend, CARTO color\n * palette. Mirrors {@link import('../bar/options').barOptions} so all\n * four ECharts widgets share v1 look-and-feel.\n *\n * Intentional deviations from bar (timeseries-specific):\n * - **X-axis is `type: 'time'`** (not 'category'). ECharts handles\n * uneven sample spacing and zoom-level-aware label formatting.\n * `labelFormatter` is wrapped so the consumer sees a `Date`, not\n * a numeric timestamp.\n * - **Tooltip body reads `{ name, value }` rows**, same as bar, but\n * the `name` may arrive as `Date | number | string`. The\n * `labelFormatter` receives a `Date` regardless.\n *\n * Intentionally data-agnostic: no series, no dataset, no `legend.show`\n * (those depend on data and are added by the option factory's merge\n * phase via {@link createTimeseriesOptionFactory}).\n */\nexport function timeseriesOptions({\n theme,\n formatter,\n labelFormatter,\n}: TimeseriesOptionsInput): TimeseriesEChartsOption {\n // Closure shared between yAxis min/max callbacks and the label formatter,\n // so only the rounded extents are labelled (matches v1 + bar).\n let niceMin = 0\n let niceMax = 1\n\n return {\n grid: {\n left: parseInt(theme.spacing(1)),\n top: parseInt(theme.spacing(3)),\n right: parseInt(theme.spacing(1)),\n // Default: no legend. Merger bumps this when there are >1 series.\n ...buildGridConfig(false, theme),\n containLabel: true,\n },\n tooltip: {\n trigger: 'axis',\n backgroundColor: theme.palette.grey[900],\n borderWidth: 0,\n padding: [parseInt(theme.spacing(1)), parseInt(theme.spacing(1))],\n textStyle: {\n color: theme.palette.common.white,\n fontSize: 11,\n fontFamily: theme.typography.caption.fontFamily,\n },\n axisPointer: { type: 'line' },\n position: createTooltipPositioner(theme),\n formatter: buildTimeseriesTooltipFormatter(formatter, labelFormatter),\n },\n // Legend styling baked here; `show` is toggled by the merger based on\n // series count.\n legend: {\n ...buildLegendConfig({ hasLegend: false }),\n },\n axisPointer: { lineStyle: { color: theme.palette.grey[400] } },\n color: [\n theme.palette.secondary.main,\n ...Object.values(\n (theme.palette as { qualitative?: { bold?: Record<string, string> } })\n .qualitative?.bold ?? {},\n ),\n ],\n xAxis: {\n type: 'time',\n axisLine: { show: false },\n axisTick: { show: false },\n axisLabel: {\n padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],\n margin: 0,\n hideOverlap: true,\n ...(labelFormatter && {\n formatter: (value: number) => labelFormatter(new Date(value)),\n }),\n },\n },\n yAxis: {\n type: 'value',\n min: (extent: { min: number }) => {\n niceMin = extent.min < 0 ? niceNum(extent.min) : 0\n return niceMin\n },\n max: (extent: { min: number; max: number }) => {\n niceMax = extent.max <= 0 ? 1 : niceNum(extent.max)\n return niceMax\n },\n axisLine: { show: false },\n axisTick: { show: false },\n splitLine: {\n show: true,\n lineStyle: { color: theme.palette.black?.[4] ?? theme.palette.divider },\n },\n axisLabel: {\n fontSize: theme.typography.overlineDelicate?.fontSize,\n fontFamily: theme.typography.overlineDelicate?.fontFamily,\n margin: parseInt(theme.spacing(1)),\n show: true,\n showMaxLabel: true,\n showMinLabel: true,\n verticalAlign: 'bottom',\n inside: true,\n formatter: (value: number) => {\n if (value !== niceMax && value !== niceMin) return ''\n if (value === 0) return ''\n return formatter ? formatter(value) : String(value)\n },\n },\n },\n } as TimeseriesEChartsOption\n}\n\n/**\n * Returns the timeseries widget's {@link OptionFactory} — one closure\n * that owns BOTH phases of option construction:\n *\n * - **Structural phase** (`option == null`) — builds the theme-aware\n * structural option via {@link timeseriesOptions}, optionally merging\n * the consumer-supplied `optionsOverride`. Called once by Provider to\n * seed `rawOptions` in the store.\n * - **Merge phase** (`option != null`) — fuses post-pipeline `state.data`\n * (`TimeseriesWidgetData`) into the option via the dataset API: one\n * dataset per series, each series referencing its dataset by index,\n * encoded by `name` (x — time) and `value` (y). Mirrors {@link import('../bar/options').createBarOptionFactory}:\n * series-template merge for `addStack`, reactive formatters from\n * `ctx`, `niceNum`-rounded y-axis bounds at fusion time, and\n * `positionDataZoomForLegend` layout for ZoomToggle sliders.\n */\nexport function createTimeseriesOptionFactory(\n options: TimeseriesOptionFactoryInput,\n): OptionFactory {\n const { theme, formatter, labelFormatter, optionsOverride } = options\n const series = options.series\n const smooth = options.smooth ?? true\n const area = options.area ?? false\n const selection = options.selection\n const selectionSet =\n selection && selection.length > 0\n ? new Set<string | number>(selection)\n : null\n // `name` may be Date | number | string. Normalize to the same type the\n // selection is keyed on (Date → ms) so Set lookups match.\n const normalizeName = (n: Date | number | string): string | number =>\n n instanceof Date ? n.getTime() : n\n return (option, data, ctx) => {\n if (option == null) {\n const structural = timeseriesOptions({ theme, formatter, labelFormatter })\n return optionsOverride\n ? (mergeOptions(\n structural as unknown as Record<string, unknown>,\n optionsOverride as Partial<Record<string, unknown>>,\n ) as EChartsOption)\n : structural\n }\n\n const seriesArr = Array.isArray(data) ? (data as TimeseriesWidgetData) : []\n if (seriesArr.length === 0) {\n return { ...option, dataset: [], series: [] }\n }\n const hasLegend = seriesArr.length > 1\n const seriesTemplates = Array.isArray(option.series) ? option.series : []\n const broadcastTemplate = seriesTemplates[0] ?? {}\n const baseYAxis =\n typeof option.yAxis === 'object' && !Array.isArray(option.yAxis)\n ? option.yAxis\n : {}\n const baseGrid =\n typeof option.grid === 'object' && !Array.isArray(option.grid)\n ? option.grid\n : {}\n const baseTooltip =\n typeof option.tooltip === 'object' && !Array.isArray(option.tooltip)\n ? option.tooltip\n : {}\n const baseLegend =\n typeof option.legend === 'object' && !Array.isArray(option.legend)\n ? option.legend\n : {}\n\n // Reactive (live store) formatter from ctx — distinct from the\n // closure-time `formatter` captured for the structural-build branch\n // above. RelativeData can install a percent formatter on the store\n // after the factory was constructed; the merge phase reads `ctx` to\n // pick that up. `labelFormatter` (Date → string) is structural-only —\n // not relativizable — so the merge branch reads the closure-time value.\n const liveFormatter = ctx?.formatter\n\n // Closure shared between the yAxis min/max callbacks and the label\n // formatter, so only the rounded extents are labelled (matches v1 +\n // bar). Delegating the extent to ECharts (rather than precomputing\n // scalars from the raw data) keeps stacked lines inside the plot: when\n // StackToggle marks the series, ECharts feeds the *post-stack* extent\n // to these callbacks, so `niceNum` rounds the stacked total.\n let niceMin = 0\n let niceMax = 1\n\n // Zoom slider layout: when ZoomToggle has installed `dataZoom`, push the\n // slider above the legend (if any) and reserve room in the grid below.\n const dataZoomLayout = positionDataZoomForLegend(option.dataZoom, hasLegend)\n const fallbackBottom =\n typeof baseGrid.bottom === 'number' ? baseGrid.bottom : 24\n const baseBottom = hasLegend ? 56 : fallbackBottom\n const gridBottom = dataZoomLayout\n ? baseBottom + ZOOM_LAYOUT.sliderHeight + ZOOM_LAYOUT.sliderGap\n : baseBottom\n\n // Dim non-selected points via `series.itemStyle.color`. Per-row\n // `itemStyle` on dataset object-rows is silently ignored when\n // `series.encode` is in play.\n //\n // We *always* emit `itemStyle.color` (a passthrough when nothing is\n // selected), not conditionally — dropping the key between renders\n // would let ECharts' default merge keep the previous callback alive\n // and points would stay dimmed forever after an external clear.\n // Always emitting lets normal merge swap the callback in place, no\n // `replaceMerge` and no entry-animation flash on selection on/off.\n const dimItemStyle = {\n color: (params: CallbackDataParams) => {\n const datum = params.value as\n | { name?: Date | number | string }\n | undefined\n const raw = datum?.name\n const base = params.color as string\n if (!selectionSet || raw == null) return base\n return selectionSet.has(normalizeName(raw))\n ? base\n : echarts.color.modifyAlpha(base, 0.15)\n },\n }\n\n return {\n ...option,\n dataset: seriesArr.map((s) => ({ source: s as readonly object[] })),\n series: seriesArr.map((_, i) => {\n const template =\n (seriesTemplates[i] as object | undefined) ??\n (broadcastTemplate as object)\n // For line series, set BOTH `series[i].color` (legend swatch +\n // markers) AND `series[i].lineStyle.color` (the line itself) so\n // the override paints everywhere a series has a colour slot.\n const overrideColor = resolveThemeColor(theme, series?.[i]?.color)\n return {\n ...(typeof template === 'object' ? template : {}),\n type: 'line' as const,\n datasetIndex: i,\n name: series?.[i]?.name ?? `Series ${i + 1}`,\n encode: { x: 'name', y: 'value' },\n smooth,\n // When a selection is active, surface markers so the per-point\n // color callback has something to dim — a continuous line would\n // hide the visual selection feedback.\n showSymbol: selectionSet != null,\n ...(area ? { areaStyle: {} } : {}),\n emphasis: { focus: 'series' },\n itemStyle: dimItemStyle,\n ...(overrideColor\n ? { color: overrideColor, lineStyle: { color: overrideColor } }\n : {}),\n }\n }),\n legend: { ...baseLegend, show: hasLegend },\n grid: { ...baseGrid, bottom: gridBottom },\n ...(dataZoomLayout ? { dataZoom: dataZoomLayout } : {}),\n yAxis: {\n ...baseYAxis,\n min: (extent: { min: number }) => {\n niceMin = extent.min < 0 ? niceNum(extent.min) : 0\n return niceMin\n },\n max: (extent: { min: number; max: number }) => {\n niceMax = extent.max <= 0 ? 1 : niceNum(extent.max)\n return niceMax\n },\n axisLabel: {\n ...((baseYAxis as { axisLabel?: object }).axisLabel ?? {}),\n formatter: (value: number) => {\n if (value !== niceMax && value !== niceMin) return ''\n if (value === 0) return ''\n return liveFormatter ? liveFormatter(value) : String(value)\n },\n },\n } as EChartsOption['yAxis'],\n tooltip: {\n ...baseTooltip,\n formatter: buildTimeseriesTooltipFormatter(\n liveFormatter,\n labelFormatter,\n ),\n },\n } as EChartsOption\n }\n}\n\n/**\n * Tooltip formatter for the timeseries `{ name, value }` row shape.\n * `name` arrives as `Date | number | string` (the time-axis stores the\n * raw value the consumer supplied). The consumer's `labelFormatter`\n * expects a `Date`, so we coerce non-Date values via `new Date(...)`\n * before invoking it.\n */\nfunction buildTimeseriesTooltipFormatter(\n formatter: ((value: number) => string) | undefined,\n labelFormatter: ((value: Date) => string) | undefined,\n) {\n return createTooltipFormatter((item) => {\n const row = item.value as\n | { name?: Date | number | string; value?: number }\n | undefined\n const raw = row?.value\n const formattedValue =\n typeof raw === 'number' && formatter ? formatter(raw) : (raw ?? '')\n const marker = typeof item.marker === 'string' ? item.marker : ''\n const seriesName = item.seriesName ? `${item.seriesName}: ` : ''\n const rawName = row?.name ?? item.name\n const name =\n labelFormatter && rawName != null\n ? labelFormatter(rawName instanceof Date ? rawName : new Date(rawName))\n : (rawName ?? '')\n return { name: String(name), seriesName, marker, value: formattedValue }\n })\n}\n","import { Box, Skeleton } from '@mui/material'\nimport type { SxProps, Theme } from '@mui/material'\n\nconst styles = {\n container: {\n display: 'flex',\n flexDirection: 'column',\n justifyContent: 'space-between',\n gap: ({ spacing }) => spacing(1),\n height: ({ spacing }) => spacing(38),\n },\n graph: {\n position: 'relative',\n flex: '1 1 auto',\n width: '100%',\n },\n segmentA: {\n position: 'absolute',\n left: 0,\n right: '70%',\n top: '60%',\n height: 4,\n },\n segmentB: {\n position: 'absolute',\n left: '25%',\n right: '40%',\n top: '35%',\n height: 4,\n },\n segmentC: {\n position: 'absolute',\n left: '55%',\n right: 0,\n top: '50%',\n height: 4,\n },\n legend: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(2),\n height: ({ spacing }) => spacing(5),\n },\n legendItem: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(1.5),\n },\n} satisfies Record<string, SxProps<Theme>>\n\n/**\n * Loading state for the Timeseries widget. Mirrors a line chart's\n * silhouette — three thin horizontal segments at staggered offsets to\n * suggest a moving line, plus a 2-dot legend stub. Sibling-consistent\n * with Bar / Histogram / Scatter skeletons (column-flex container with\n * legend strip below).\n */\nexport function TimeseriesSkeleton() {\n return (\n <Box sx={styles.container}>\n <Box sx={styles.graph}>\n <Skeleton variant='rectangular' sx={styles.segmentA} />\n <Skeleton variant='rectangular' sx={styles.segmentB} />\n <Skeleton variant='rectangular' sx={styles.segmentC} />\n </Box>\n <Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>\n </Box>\n )\n}\n","import {\n buildPngDownloadItem,\n downloadToCSV,\n type DownloadItem,\n} from '../actions/download'\nimport type { TimeseriesWidgetData } from './types'\n\n/**\n * Download menu items for the Timeseries widget. Always includes a CSV\n * item with `time, series_1, series_2, …` columns (one row per unique time\n * across all series; ISO-8601 strings for `Date`/numeric times). When\n * `getCaptureEl` is supplied, prepends a PNG item that rasterises the\n * captured element via `html2canvas`.\n */\nexport function createTimeseriesDownloadConfig(args: {\n filename: string\n getData: () => TimeseriesWidgetData\n seriesNames?: readonly string[]\n getCaptureEl?: () => HTMLElement | null\n pngPixelRatio?: number\n pngBackgroundColor?: string | null\n}): DownloadItem[] {\n const items: DownloadItem[] = []\n if (args.getCaptureEl) {\n items.push(\n buildPngDownloadItem({\n filename: args.filename,\n getCaptureEl: args.getCaptureEl,\n pixelRatio: args.pngPixelRatio,\n backgroundColor: args.pngBackgroundColor,\n }),\n )\n }\n items.push({\n id: 'csv',\n label: 'Download as CSV',\n resolve: () => {\n const data = args.getData()\n const seriesCount = data.length\n\n // Collect every unique time, preserving insertion order.\n const timeKeys: (Date | number | string)[] = []\n const seenKeys = new Set<string>()\n for (const series of data) {\n for (const point of series) {\n const key = String(point.name)\n if (!seenKeys.has(key)) {\n seenKeys.add(key)\n timeKeys.push(point.name)\n }\n }\n }\n\n // Build a quick lookup per series for O(rows × series) emit.\n const lookups = data.map(\n (series) => new Map(series.map((p) => [String(p.name), p.value])),\n )\n\n const header: unknown[] = ['time']\n for (let i = 0; i < seriesCount; i++) {\n header.push(args.seriesNames?.[i] ?? `series_${i + 1}`)\n }\n const rows: unknown[][] = [header]\n for (const key of timeKeys) {\n const row: unknown[] = [formatTime(key)]\n const lookupKey = String(key)\n for (const lookup of lookups) row.push(lookup.get(lookupKey) ?? '')\n rows.push(row)\n }\n\n const handle = downloadToCSV(rows)\n return Promise.resolve({\n url: handle.url,\n filename: `${args.filename}.csv`,\n revoke: handle.revoke,\n })\n },\n })\n return items\n}\n\nfunction formatTime(v: Date | number | string): string {\n if (v instanceof Date) return v.toISOString()\n if (typeof v === 'number') return new Date(v).toISOString()\n return v\n}\n"],"names":["timeseriesOptions","theme","formatter","labelFormatter","niceMin","niceMax","grid","left","parseInt","spacing","top","right","buildGridConfig","containLabel","tooltip","trigger","backgroundColor","palette","grey","borderWidth","padding","textStyle","color","common","white","fontSize","fontFamily","typography","caption","axisPointer","type","position","createTooltipPositioner","buildTimeseriesTooltipFormatter","legend","buildLegendConfig","hasLegend","lineStyle","secondary","main","Object","values","qualitative","bold","xAxis","axisLine","show","axisTick","axisLabel","margin","hideOverlap","value","Date","yAxis","min","extent","niceNum","max","splitLine","black","divider","overlineDelicate","showMaxLabel","showMinLabel","verticalAlign","inside","String","createTimeseriesOptionFactory","options","optionsOverride","series","smooth","area","selection","selectionSet","length","Set","normalizeName","n","getTime","option","data","ctx","structural","mergeOptions","seriesArr","Array","isArray","dataset","seriesTemplates","broadcastTemplate","baseYAxis","baseGrid","baseTooltip","baseLegend","liveFormatter","dataZoomLayout","positionDataZoomForLegend","dataZoom","fallbackBottom","bottom","baseBottom","gridBottom","ZOOM_LAYOUT","sliderHeight","sliderGap","dimItemStyle","params","raw","name","base","has","echarts","modifyAlpha","map","s","source","_","i","template","overrideColor","resolveThemeColor","datasetIndex","encode","x","y","showSymbol","areaStyle","emphasis","focus","itemStyle","createTooltipFormatter","item","row","formattedValue","marker","seriesName","rawName","styles","container","display","flexDirection","justifyContent","gap","height","graph","flex","width","segmentA","segmentB","segmentC","alignItems","legendItem","TimeseriesSkeleton","$","_c","t0","Symbol","for","jsxs","Box","jsx","Skeleton","t1","_temp","createTimeseriesDownloadConfig","args","items","getCaptureEl","push","buildPngDownloadItem","filename","pixelRatio","pngPixelRatio","pngBackgroundColor","id","label","resolve","getData","seriesCount","timeKeys","seenKeys","point","key","add","lookups","Map","p","header","seriesNames","rows","formatTime","lookupKey","lookup","get","handle","downloadToCSV","Promise","url","revoke","v","toISOString"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"timeseries.js","sources":["../../src/widgets-v2/timeseries/options.ts","../../src/widgets-v2/timeseries/skeleton.tsx","../../src/widgets-v2/timeseries/download.ts"],"sourcesContent":["import type { EChartsOption } from 'echarts'\nimport * as echarts from 'echarts'\nimport type { CallbackDataParams } from 'echarts/types/dist/shared'\nimport {\n buildGridConfig,\n buildLegendConfig,\n createTooltipFormatter,\n createTooltipPositioner,\n niceNum,\n} from '../../widgets/utils/chart-config'\nimport { ZOOM_LAYOUT } from '../actions/zoom-toggle'\nimport type { OptionFactory } from '../echart'\nimport { mergeOptions, resolveThemeColor } from '../utils'\nimport { positionDataZoomForLegend } from '../utils/data-zoom-layout'\nimport type {\n TimeseriesEChartsOption,\n TimeseriesOptionFactoryInput,\n TimeseriesOptionsInput,\n TimeseriesWidgetData,\n} from './types'\n\n/**\n * Builds the **structural** ECharts option for a timeseries widget —\n * time x-axis, value y-axis, themed tooltip, themed legend, CARTO color\n * palette. Mirrors {@link import('../bar/options').barOptions} so all\n * four ECharts widgets share v1 look-and-feel.\n *\n * Intentional deviations from bar (timeseries-specific):\n * - **X-axis is `type: 'time'`** (not 'category'). ECharts handles\n * uneven sample spacing and zoom-level-aware label formatting.\n * `labelFormatter` is wrapped so the consumer sees a `Date`, not\n * a numeric timestamp.\n * - **Tooltip body reads `{ name, value }` rows**, same as bar, but\n * the `name` may arrive as `Date | number | string`. The\n * `labelFormatter` receives a `Date` regardless.\n *\n * Intentionally data-agnostic: no series, no dataset, no `legend.show`\n * (those depend on data and are added by the option factory's merge\n * phase via {@link createTimeseriesOptionFactory}).\n */\nexport function timeseriesOptions({\n theme,\n formatter,\n labelFormatter,\n}: TimeseriesOptionsInput): TimeseriesEChartsOption {\n // Closure shared between yAxis min/max callbacks and the label formatter,\n // so only the rounded extents are labelled (matches v1 + bar).\n let niceMin = 0\n let niceMax = 1\n\n return {\n grid: {\n left: parseInt(theme.spacing(1)),\n top: parseInt(theme.spacing(3)),\n right: parseInt(theme.spacing(1)),\n // Default: no legend. Merger bumps this when there are >1 series.\n ...buildGridConfig(false, theme),\n containLabel: true,\n },\n tooltip: {\n trigger: 'axis',\n backgroundColor: theme.palette.grey[900],\n borderWidth: 0,\n padding: [parseInt(theme.spacing(1)), parseInt(theme.spacing(1))],\n textStyle: {\n color: theme.palette.common.white,\n fontSize: 11,\n fontFamily: theme.typography.caption.fontFamily,\n },\n axisPointer: { type: 'line' },\n position: createTooltipPositioner(theme),\n formatter: buildTimeseriesTooltipFormatter(formatter, labelFormatter),\n },\n // Legend styling baked here; `show` is toggled by the merger based on\n // series count.\n legend: {\n ...buildLegendConfig({ hasLegend: false }),\n },\n axisPointer: { lineStyle: { color: theme.palette.grey[400] } },\n color: [\n theme.palette.secondary.main,\n ...Object.values(\n (theme.palette as { qualitative?: { bold?: Record<string, string> } })\n .qualitative?.bold ?? {},\n ),\n ],\n xAxis: {\n type: 'time',\n axisLine: { show: false },\n axisTick: { show: false },\n axisLabel: {\n padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],\n margin: 0,\n hideOverlap: true,\n ...(labelFormatter && {\n formatter: (value: number) => labelFormatter(new Date(value)),\n }),\n },\n },\n yAxis: {\n type: 'value',\n min: (extent: { min: number }) => {\n niceMin = extent.min < 0 ? niceNum(extent.min) : 0\n return niceMin\n },\n max: (extent: { min: number; max: number }) => {\n niceMax = extent.max <= 0 ? 1 : niceNum(extent.max)\n return niceMax\n },\n axisLine: { show: false },\n axisTick: { show: false },\n splitLine: {\n show: true,\n lineStyle: { color: theme.palette.black?.[4] ?? theme.palette.divider },\n },\n axisLabel: {\n fontSize: theme.typography.overlineDelicate?.fontSize,\n fontFamily: theme.typography.overlineDelicate?.fontFamily,\n margin: parseInt(theme.spacing(1)),\n show: true,\n showMaxLabel: true,\n showMinLabel: true,\n verticalAlign: 'bottom',\n inside: true,\n formatter: (value: number) => {\n if (value !== niceMax && value !== niceMin) return ''\n if (value === 0) return ''\n return formatter ? formatter(value) : String(value)\n },\n },\n },\n } as TimeseriesEChartsOption\n}\n\n/**\n * Returns the timeseries widget's {@link OptionFactory} — one closure\n * that owns BOTH phases of option construction:\n *\n * - **Structural phase** (`option == null`) — builds the theme-aware\n * structural option via {@link timeseriesOptions}, optionally merging\n * the consumer-supplied `optionsOverride`. Called once by Provider to\n * seed `rawOptions` in the store.\n * - **Merge phase** (`option != null`) — fuses post-pipeline `state.data`\n * (`TimeseriesWidgetData`) into the option via the dataset API: one\n * dataset per series, each series referencing its dataset by index,\n * encoded by `name` (x — time) and `value` (y). Mirrors {@link import('../bar/options').createBarOptionFactory}:\n * series-template merge for `addStack`, reactive formatters from\n * `ctx`, `niceNum`-rounded y-axis bounds at fusion time, and\n * `positionDataZoomForLegend` layout for ZoomToggle sliders.\n */\nexport function createTimeseriesOptionFactory(\n options: TimeseriesOptionFactoryInput,\n): OptionFactory {\n const { theme, formatter, labelFormatter, optionsOverride } = options\n const series = options.series\n const smooth = options.smooth ?? true\n const area = options.area ?? false\n const selection = options.selection\n const selectionSet =\n selection && selection.length > 0\n ? new Set<string | number>(selection)\n : null\n // `name` may be Date | number | string. Normalize to the same type the\n // selection is keyed on (Date → ms) so Set lookups match.\n const normalizeName = (n: Date | number | string): string | number =>\n n instanceof Date ? n.getTime() : n\n return (option, data, ctx) => {\n if (option == null) {\n const structural = timeseriesOptions({ theme, formatter, labelFormatter })\n return optionsOverride\n ? (mergeOptions(\n structural as unknown as Record<string, unknown>,\n optionsOverride as Partial<Record<string, unknown>>,\n ) as EChartsOption)\n : structural\n }\n\n const seriesArr = Array.isArray(data) ? (data as TimeseriesWidgetData) : []\n if (seriesArr.length === 0) {\n return { ...option, dataset: [], series: [] }\n }\n const hasLegend = seriesArr.length > 1\n const seriesTemplates = Array.isArray(option.series) ? option.series : []\n const broadcastTemplate = seriesTemplates[0] ?? {}\n const baseYAxis =\n typeof option.yAxis === 'object' && !Array.isArray(option.yAxis)\n ? option.yAxis\n : {}\n const baseGrid =\n typeof option.grid === 'object' && !Array.isArray(option.grid)\n ? option.grid\n : {}\n const baseTooltip =\n typeof option.tooltip === 'object' && !Array.isArray(option.tooltip)\n ? option.tooltip\n : {}\n const baseLegend =\n typeof option.legend === 'object' && !Array.isArray(option.legend)\n ? option.legend\n : {}\n\n // Reactive (live store) formatter from ctx — distinct from the\n // closure-time `formatter` captured for the structural-build branch\n // above. RelativeData can install a percent formatter on the store\n // after the factory was constructed; the merge phase reads `ctx` to\n // pick that up. `labelFormatter` (Date → string) is structural-only —\n // not relativizable — so the merge branch reads the closure-time value.\n const liveFormatter = ctx?.formatter\n\n // Closure shared between the yAxis min/max callbacks and the label\n // formatter, so only the rounded extents are labelled (matches v1 +\n // bar). Delegating the extent to ECharts (rather than precomputing\n // scalars from the raw data) keeps stacked lines inside the plot: when\n // StackToggle marks the series, ECharts feeds the *post-stack* extent\n // to these callbacks, so `niceNum` rounds the stacked total.\n let niceMin = 0\n let niceMax = 1\n\n // Zoom slider layout: when ZoomToggle has installed `dataZoom`, push the\n // slider above the legend (if any) and reserve room in the grid below.\n const dataZoomLayout = positionDataZoomForLegend(option.dataZoom, hasLegend)\n const fallbackBottom =\n typeof baseGrid.bottom === 'number' ? baseGrid.bottom : 24\n const baseBottom = hasLegend ? 56 : fallbackBottom\n const gridBottom = dataZoomLayout\n ? baseBottom + ZOOM_LAYOUT.sliderHeight + ZOOM_LAYOUT.sliderGap\n : baseBottom\n\n // Dim non-selected points via `series.itemStyle.color`. Per-row\n // `itemStyle` on dataset object-rows is silently ignored when\n // `series.encode` is in play.\n //\n // We *always* emit `itemStyle.color` (a passthrough when nothing is\n // selected), not conditionally — dropping the key between renders\n // would let ECharts' default merge keep the previous callback alive\n // and points would stay dimmed forever after an external clear.\n // Always emitting lets normal merge swap the callback in place, no\n // `replaceMerge` and no entry-animation flash on selection on/off.\n const dimItemStyle = {\n color: (params: CallbackDataParams) => {\n const datum = params.value as\n | { name?: Date | number | string }\n | undefined\n const raw = datum?.name\n const base = params.color as string\n if (!selectionSet || raw == null) return base\n return selectionSet.has(normalizeName(raw))\n ? base\n : echarts.color.modifyAlpha(base, 0.15)\n },\n }\n\n return {\n ...option,\n dataset: seriesArr.map((s) => ({ source: s as readonly object[] })),\n series: seriesArr.map((_, i) => {\n const template =\n (seriesTemplates[i] as object | undefined) ??\n (broadcastTemplate as object)\n // For line series, set BOTH `series[i].color` (legend swatch +\n // markers) AND `series[i].lineStyle.color` (the line itself) so\n // the override paints everywhere a series has a colour slot.\n const overrideColor = resolveThemeColor(theme, series?.[i]?.color)\n return {\n ...(typeof template === 'object' ? template : {}),\n type: 'line' as const,\n datasetIndex: i,\n name: series?.[i]?.name ?? `Series ${i + 1}`,\n encode: { x: 'name', y: 'value' },\n smooth,\n // When a selection is active, surface markers so the per-point\n // color callback has something to dim — a continuous line would\n // hide the visual selection feedback.\n showSymbol: selectionSet != null,\n ...(area ? { areaStyle: {} } : {}),\n emphasis: { focus: 'series' },\n itemStyle: dimItemStyle,\n ...(overrideColor\n ? { color: overrideColor, lineStyle: { color: overrideColor } }\n : {}),\n }\n }),\n legend: { ...baseLegend, show: hasLegend },\n grid: { ...baseGrid, bottom: gridBottom },\n ...(dataZoomLayout ? { dataZoom: dataZoomLayout } : {}),\n yAxis: {\n ...baseYAxis,\n min: (extent: { min: number }) => {\n niceMin = extent.min < 0 ? niceNum(extent.min) : 0\n return niceMin\n },\n max: (extent: { min: number; max: number }) => {\n niceMax = extent.max <= 0 ? 1 : niceNum(extent.max)\n return niceMax\n },\n axisLabel: {\n ...((baseYAxis as { axisLabel?: object }).axisLabel ?? {}),\n formatter: (value: number) => {\n if (value !== niceMax && value !== niceMin) return ''\n if (value === 0) return ''\n return liveFormatter ? liveFormatter(value) : String(value)\n },\n },\n } as EChartsOption['yAxis'],\n tooltip: {\n ...baseTooltip,\n formatter: buildTimeseriesTooltipFormatter(\n liveFormatter,\n labelFormatter,\n ),\n },\n } as EChartsOption\n }\n}\n\n/**\n * Tooltip formatter for the timeseries `{ name, value }` row shape.\n * `name` arrives as `Date | number | string` (the time-axis stores the\n * raw value the consumer supplied). The consumer's `labelFormatter`\n * expects a `Date`, so we coerce non-Date values via `new Date(...)`\n * before invoking it.\n */\nfunction buildTimeseriesTooltipFormatter(\n formatter: ((value: number) => string) | undefined,\n labelFormatter: ((value: Date) => string) | undefined,\n) {\n return createTooltipFormatter((item) => {\n const row = item.value as\n | { name?: Date | number | string; value?: number }\n | undefined\n const raw = row?.value\n const formattedValue =\n typeof raw === 'number' && formatter ? formatter(raw) : (raw ?? '')\n const marker = typeof item.marker === 'string' ? item.marker : ''\n const seriesName = item.seriesName ? `${item.seriesName}: ` : ''\n const rawName = row?.name ?? item.name\n const name =\n labelFormatter && rawName != null\n ? labelFormatter(rawName instanceof Date ? rawName : new Date(rawName))\n : (rawName ?? '')\n return { name: String(name), seriesName, marker, value: formattedValue }\n })\n}\n","import { Box, Skeleton } from '@mui/material'\nimport type { SxProps, Theme } from '@mui/material'\n\nconst styles = {\n container: {\n display: 'flex',\n flexDirection: 'column',\n justifyContent: 'space-between',\n gap: ({ spacing }) => spacing(1),\n height: ({ spacing }) => spacing(38),\n },\n graph: {\n position: 'relative',\n flex: '1 1 auto',\n width: '100%',\n },\n segmentA: {\n position: 'absolute',\n left: 0,\n right: '70%',\n top: '60%',\n height: 4,\n },\n segmentB: {\n position: 'absolute',\n left: '25%',\n right: '40%',\n top: '35%',\n height: 4,\n },\n segmentC: {\n position: 'absolute',\n left: '55%',\n right: 0,\n top: '50%',\n height: 4,\n },\n legend: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(2),\n height: ({ spacing }) => spacing(5),\n },\n legendItem: {\n display: 'flex',\n alignItems: 'center',\n gap: ({ spacing }) => spacing(1.5),\n },\n} satisfies Record<string, SxProps<Theme>>\n\n/**\n * Loading state for the Timeseries widget. Mirrors a line chart's\n * silhouette — three thin horizontal segments at staggered offsets to\n * suggest a moving line, plus a 2-dot legend stub. Sibling-consistent\n * with Bar / Histogram / Scatter skeletons (column-flex container with\n * legend strip below).\n */\nexport function TimeseriesSkeleton() {\n return (\n <Box sx={styles.container}>\n <Box sx={styles.graph}>\n <Skeleton variant='rectangular' sx={styles.segmentA} />\n <Skeleton variant='rectangular' sx={styles.segmentB} />\n <Skeleton variant='rectangular' sx={styles.segmentC} />\n </Box>\n <Box sx={styles.legend}>\n {[0, 1].map((i) => (\n <Box key={`legend-${i}`} sx={styles.legendItem}>\n <Skeleton variant='circular' width={8} height={8} />\n <Skeleton width={48} height={8} />\n </Box>\n ))}\n </Box>\n </Box>\n )\n}\n","import {\n buildCsvDownloadItem,\n buildPngDownloadItem,\n type DownloadItem,\n} from '../actions/download'\nimport type { TimeseriesWidgetData } from './types'\n\n/**\n * Download menu items for the Timeseries widget. Always includes a CSV\n * item with `time, series_1, series_2, …` columns (one row per unique time\n * across all series; ISO-8601 strings for `Date`/numeric times). When\n * `getCaptureEl` is supplied, prepends a PNG item that rasterises the\n * captured element via `html2canvas`.\n */\nexport function createTimeseriesDownloadConfig(args: {\n filename: string\n getData: () => TimeseriesWidgetData\n seriesNames?: readonly string[]\n getCaptureEl?: () => HTMLElement | null\n pngPixelRatio?: number\n pngBackgroundColor?: string | null\n}): DownloadItem[] {\n const items: DownloadItem[] = []\n if (args.getCaptureEl) {\n items.push(\n buildPngDownloadItem({\n filename: args.filename,\n getCaptureEl: args.getCaptureEl,\n pixelRatio: args.pngPixelRatio,\n backgroundColor: args.pngBackgroundColor,\n }),\n )\n }\n items.push(\n buildCsvDownloadItem({\n filename: args.filename,\n getRows: () => {\n const data = args.getData()\n const seriesCount = data.length\n\n // Collect every unique time, preserving insertion order.\n const timeKeys: (Date | number | string)[] = []\n const seenKeys = new Set<string>()\n for (const series of data) {\n for (const point of series) {\n const key = String(point.name)\n if (!seenKeys.has(key)) {\n seenKeys.add(key)\n timeKeys.push(point.name)\n }\n }\n }\n\n // Build a quick lookup per series for O(rows × series) emit.\n const lookups = data.map(\n (series) => new Map(series.map((p) => [String(p.name), p.value])),\n )\n\n const header: unknown[] = ['time']\n for (let i = 0; i < seriesCount; i++) {\n header.push(args.seriesNames?.[i] ?? `series_${i + 1}`)\n }\n const rows: unknown[][] = [header]\n for (const key of timeKeys) {\n const row: unknown[] = [formatTime(key)]\n const lookupKey = String(key)\n for (const lookup of lookups) row.push(lookup.get(lookupKey) ?? '')\n rows.push(row)\n }\n\n return rows\n },\n }),\n )\n return items\n}\n\nfunction formatTime(v: Date | number | string): string {\n if (v instanceof Date) return v.toISOString()\n if (typeof v === 'number') return new Date(v).toISOString()\n return v\n}\n"],"names":["timeseriesOptions","theme","formatter","labelFormatter","niceMin","niceMax","grid","left","parseInt","spacing","top","right","buildGridConfig","containLabel","tooltip","trigger","backgroundColor","palette","grey","borderWidth","padding","textStyle","color","common","white","fontSize","fontFamily","typography","caption","axisPointer","type","position","createTooltipPositioner","buildTimeseriesTooltipFormatter","legend","buildLegendConfig","hasLegend","lineStyle","secondary","main","Object","values","qualitative","bold","xAxis","axisLine","show","axisTick","axisLabel","margin","hideOverlap","value","Date","yAxis","min","extent","niceNum","max","splitLine","black","divider","overlineDelicate","showMaxLabel","showMinLabel","verticalAlign","inside","String","createTimeseriesOptionFactory","options","optionsOverride","series","smooth","area","selection","selectionSet","length","Set","normalizeName","n","getTime","option","data","ctx","structural","mergeOptions","seriesArr","Array","isArray","dataset","seriesTemplates","broadcastTemplate","baseYAxis","baseGrid","baseTooltip","baseLegend","liveFormatter","dataZoomLayout","positionDataZoomForLegend","dataZoom","fallbackBottom","bottom","baseBottom","gridBottom","ZOOM_LAYOUT","sliderHeight","sliderGap","dimItemStyle","params","raw","name","base","has","echarts","modifyAlpha","map","s","source","_","i","template","overrideColor","resolveThemeColor","datasetIndex","encode","x","y","showSymbol","areaStyle","emphasis","focus","itemStyle","createTooltipFormatter","item","row","formattedValue","marker","seriesName","rawName","styles","container","display","flexDirection","justifyContent","gap","height","graph","flex","width","segmentA","segmentB","segmentC","alignItems","legendItem","TimeseriesSkeleton","$","_c","t0","Symbol","for","jsxs","Box","jsx","Skeleton","t1","_temp","createTimeseriesDownloadConfig","args","items","getCaptureEl","push","buildPngDownloadItem","filename","pixelRatio","pngPixelRatio","pngBackgroundColor","buildCsvDownloadItem","getRows","getData","seriesCount","timeKeys","seenKeys","point","key","add","lookups","Map","p","header","seriesNames","rows","formatTime","lookupKey","lookup","get","v","toISOString"],"mappings":"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"}
|
package/dist/widgets-v2.js
CHANGED
|
@@ -23,7 +23,7 @@ import "react-markdown";
|
|
|
23
23
|
import { b as ut, a as gt } from "./spread-DYNpzgh_.js";
|
|
24
24
|
import { a as ht, R as yt } from "./range-l4fNHLEg.js";
|
|
25
25
|
import { f as xt, C as bt } from "./category-Dnd2_j0x.js";
|
|
26
|
-
import { c as vt, T as St } from "./table-
|
|
26
|
+
import { c as vt, T as St } from "./table-B3ZWWhJt.js";
|
|
27
27
|
function wt(t) {
|
|
28
28
|
const e = T(31), {
|
|
29
29
|
id: n,
|
package/package.json
CHANGED
|
@@ -10,10 +10,14 @@ ChatMessageOverflow.displayName = 'ChatMessageOverflow'
|
|
|
10
10
|
|
|
11
11
|
export const styles = {
|
|
12
12
|
agentMessageContainer: (theme: Theme) => ({
|
|
13
|
+
...theme.typography.body2,
|
|
13
14
|
width: '100%',
|
|
14
15
|
display: 'flex',
|
|
15
16
|
flexDirection: 'column',
|
|
16
17
|
alignItems: 'flex-start',
|
|
18
|
+
// Separate the stacked turn blocks (tools → answer text → actions) per the
|
|
19
|
+
// Figma "Chat Turn" spec (gap-8).
|
|
20
|
+
gap: theme.spacing(1),
|
|
17
21
|
paddingRight: theme.spacing(4),
|
|
18
22
|
'& + .PsChat--agent-message': {
|
|
19
23
|
marginTop: theme.spacing(1),
|
|
@@ -84,7 +88,7 @@ export const styles = {
|
|
|
84
88
|
padding: ({ spacing }) => spacing(0.75, 1.5),
|
|
85
89
|
borderRadius: ({ spacing }) => spacing(2, 2, 0.25, 2),
|
|
86
90
|
border: ({ palette }) => `1px solid ${palette.divider}`,
|
|
87
|
-
backgroundColor: ({ palette }) => palette.background.
|
|
91
|
+
backgroundColor: ({ palette }) => palette.background.paper,
|
|
88
92
|
color: ({ palette }) => palette.text.primary,
|
|
89
93
|
width: 'fit-content',
|
|
90
94
|
},
|
|
@@ -93,7 +93,10 @@ export const ChatContent = forwardRef<ChatContentRef, ChatContentProps>(
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
const mo = new MutationObserver(() => {
|
|
96
|
-
|
|
96
|
+
// Plain `?? =` rather than `??=`: the React Compiler can't yet lower
|
|
97
|
+
// logical-assignment operators, and bailing out would drop memoization
|
|
98
|
+
// for this scroll container (which re-renders on every message).
|
|
99
|
+
rafId = rafId ?? requestAnimationFrame(check)
|
|
97
100
|
})
|
|
98
101
|
mo.observe(root, {
|
|
99
102
|
childList: true,
|
|
@@ -31,4 +31,63 @@ describe('ChatFooter', () => {
|
|
|
31
31
|
fireEvent.change(input, { target: { value: 'hello' } })
|
|
32
32
|
expect(onChange).toHaveBeenCalledWith('hello')
|
|
33
33
|
})
|
|
34
|
+
|
|
35
|
+
const MODELS = [
|
|
36
|
+
{ value: 'gpt-4o', label: 'gpt-4o' },
|
|
37
|
+
{ value: 'gemini-2.5-pro', label: 'gemini-2.5-pro' },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
test('no model selector when models is empty/undefined', () => {
|
|
41
|
+
render(<ChatFooter {...defaultProps} />)
|
|
42
|
+
expect(screen.queryByLabelText('Select model')).toBeNull()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('no model selector when only one model is available', () => {
|
|
46
|
+
render(
|
|
47
|
+
<ChatFooter
|
|
48
|
+
{...defaultProps}
|
|
49
|
+
models={[{ value: 'gpt-4o', label: 'gpt-4o' }]}
|
|
50
|
+
selectedModel='gpt-4o'
|
|
51
|
+
/>,
|
|
52
|
+
)
|
|
53
|
+
expect(screen.queryByLabelText('Select model')).toBeNull()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('renders the selected model label', () => {
|
|
57
|
+
render(
|
|
58
|
+
<ChatFooter {...defaultProps} models={MODELS} selectedModel='gpt-4o' />,
|
|
59
|
+
)
|
|
60
|
+
const trigger = screen.getByLabelText('Select model')
|
|
61
|
+
expect(trigger.textContent).toContain('gpt-4o')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('opens the menu and fires onModelChange', () => {
|
|
65
|
+
const onModelChange = vi.fn()
|
|
66
|
+
render(
|
|
67
|
+
<ChatFooter
|
|
68
|
+
{...defaultProps}
|
|
69
|
+
models={MODELS}
|
|
70
|
+
selectedModel='gpt-4o'
|
|
71
|
+
onModelChange={onModelChange}
|
|
72
|
+
/>,
|
|
73
|
+
)
|
|
74
|
+
fireEvent.click(screen.getByLabelText('Select model'))
|
|
75
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'gemini-2.5-pro' }))
|
|
76
|
+
expect(onModelChange).toHaveBeenCalledWith('gemini-2.5-pro')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('model selector is disabled while generating', () => {
|
|
80
|
+
render(
|
|
81
|
+
<ChatFooter
|
|
82
|
+
{...defaultProps}
|
|
83
|
+
models={MODELS}
|
|
84
|
+
selectedModel='gpt-4o'
|
|
85
|
+
isGenerating
|
|
86
|
+
onStop={vi.fn()}
|
|
87
|
+
/>,
|
|
88
|
+
)
|
|
89
|
+
expect(
|
|
90
|
+
screen.getByLabelText('Select model').hasAttribute('disabled'),
|
|
91
|
+
).toBeTruthy()
|
|
92
|
+
})
|
|
34
93
|
})
|
|
@@ -1,16 +1,25 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
1
2
|
import {
|
|
2
3
|
Box,
|
|
3
|
-
|
|
4
|
+
Button,
|
|
4
5
|
FormControl,
|
|
5
6
|
FormHelperText,
|
|
6
7
|
IconButton,
|
|
8
|
+
InputBase,
|
|
9
|
+
Menu,
|
|
10
|
+
MenuItem,
|
|
11
|
+
Typography,
|
|
7
12
|
} from '@mui/material'
|
|
8
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
ArrowUpwardOutlined,
|
|
15
|
+
StopCircleOutlined,
|
|
16
|
+
KeyboardArrowDown,
|
|
17
|
+
} from '@mui/icons-material'
|
|
9
18
|
import type { ChatFooterProps } from '../types'
|
|
10
19
|
import { styles } from './styles'
|
|
11
20
|
|
|
12
21
|
const DEFAULT_CAPTION =
|
|
13
|
-
'Responses are AI
|
|
22
|
+
'Responses are AI-generated. Please verify key information.'
|
|
14
23
|
|
|
15
24
|
export function ChatFooter({
|
|
16
25
|
value,
|
|
@@ -22,9 +31,29 @@ export function ChatFooter({
|
|
|
22
31
|
placeholder = 'Type a message...',
|
|
23
32
|
labels = {},
|
|
24
33
|
caption = DEFAULT_CAPTION,
|
|
34
|
+
models,
|
|
35
|
+
selectedModel,
|
|
36
|
+
onModelChange,
|
|
37
|
+
startToolbarSlot,
|
|
38
|
+
endToolbarSlot,
|
|
25
39
|
sx,
|
|
26
40
|
}: ChatFooterProps) {
|
|
41
|
+
const [modelAnchor, setModelAnchor] = useState<HTMLElement | null>(null)
|
|
27
42
|
const canSend = value.trim() && !disabled && !isGenerating
|
|
43
|
+
// Only worth showing when there's an actual choice — a single option is dead UI.
|
|
44
|
+
const showModelSelector = (models?.length ?? 0) > 1
|
|
45
|
+
const modelLabel = labels.model ?? 'Select model'
|
|
46
|
+
// Visible trigger text: the matched option's label, the raw value as a
|
|
47
|
+
// fallback, or the placeholder when nothing is selected yet.
|
|
48
|
+
const selectedLabel =
|
|
49
|
+
models?.find((m) => m.value === selectedModel)?.label ??
|
|
50
|
+
(selectedModel && selectedModel.length > 0 ? selectedModel : modelLabel)
|
|
51
|
+
|
|
52
|
+
// The slots row (start slot + model selector + end slot) renders under the input
|
|
53
|
+
// only when there's something to show; otherwise the grid is just input | send.
|
|
54
|
+
const hasSlots =
|
|
55
|
+
showModelSelector || Boolean(startToolbarSlot) || Boolean(endToolbarSlot)
|
|
56
|
+
|
|
28
57
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
29
58
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
30
59
|
e.preventDefault()
|
|
@@ -34,41 +63,100 @@ export function ChatFooter({
|
|
|
34
63
|
}
|
|
35
64
|
}
|
|
36
65
|
|
|
66
|
+
const sendButton =
|
|
67
|
+
isGenerating && onStop ? (
|
|
68
|
+
<IconButton
|
|
69
|
+
size='small'
|
|
70
|
+
onClick={onStop}
|
|
71
|
+
disabled={disabled}
|
|
72
|
+
aria-label={labels.stop ?? 'Stop'}
|
|
73
|
+
>
|
|
74
|
+
<StopCircleOutlined />
|
|
75
|
+
</IconButton>
|
|
76
|
+
) : (
|
|
77
|
+
<IconButton
|
|
78
|
+
size='small'
|
|
79
|
+
variant='contained'
|
|
80
|
+
color='primary'
|
|
81
|
+
onClick={onSend}
|
|
82
|
+
disabled={!canSend}
|
|
83
|
+
aria-label={labels.send ?? 'Send'}
|
|
84
|
+
>
|
|
85
|
+
<ArrowUpwardOutlined />
|
|
86
|
+
</IconButton>
|
|
87
|
+
)
|
|
88
|
+
|
|
37
89
|
return (
|
|
38
90
|
<FormControl fullWidth sx={{ ...styles.footerWrapper, ...sx }}>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
91
|
+
{/* Two grid areas: [input + slots] | [send button, aligned to the end]. */}
|
|
92
|
+
<Box sx={styles.footerBox}>
|
|
93
|
+
<Box sx={styles.footerMain}>
|
|
94
|
+
<InputBase
|
|
95
|
+
multiline
|
|
96
|
+
minRows={1}
|
|
97
|
+
maxRows={8}
|
|
98
|
+
value={value}
|
|
99
|
+
onChange={(e) => onChange(e.target.value)}
|
|
100
|
+
onKeyDown={handleKeyDown}
|
|
101
|
+
placeholder={placeholder}
|
|
102
|
+
disabled={disabled || isGenerating}
|
|
103
|
+
sx={styles.footerInput}
|
|
104
|
+
/>
|
|
105
|
+
{hasSlots && (
|
|
106
|
+
<Box sx={styles.footerSlots}>
|
|
107
|
+
{startToolbarSlot}
|
|
108
|
+
{showModelSelector && (
|
|
109
|
+
<>
|
|
110
|
+
<Button
|
|
111
|
+
size='small'
|
|
112
|
+
variant='text'
|
|
113
|
+
color='inherit'
|
|
114
|
+
disabled={disabled || isGenerating}
|
|
115
|
+
onClick={(e) => setModelAnchor(e.currentTarget)}
|
|
116
|
+
aria-label={modelLabel}
|
|
117
|
+
aria-haspopup='menu'
|
|
118
|
+
aria-expanded={modelAnchor ? 'true' : undefined}
|
|
119
|
+
endIcon={<KeyboardArrowDown />}
|
|
120
|
+
sx={styles.modelSelector}
|
|
121
|
+
>
|
|
122
|
+
<Typography
|
|
123
|
+
variant='caption'
|
|
124
|
+
component='span'
|
|
125
|
+
color='inherit'
|
|
126
|
+
sx={styles.modelSelectorLabel}
|
|
127
|
+
>
|
|
128
|
+
{selectedLabel}
|
|
129
|
+
</Typography>
|
|
130
|
+
</Button>
|
|
131
|
+
<Menu
|
|
132
|
+
anchorEl={modelAnchor}
|
|
133
|
+
open={Boolean(modelAnchor)}
|
|
134
|
+
onClose={() => setModelAnchor(null)}
|
|
135
|
+
variant='menu'
|
|
136
|
+
elevation={8}
|
|
137
|
+
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
|
|
138
|
+
transformOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
|
139
|
+
>
|
|
140
|
+
{models?.map((m) => (
|
|
141
|
+
<MenuItem
|
|
142
|
+
key={m.value}
|
|
143
|
+
selected={m.value === selectedModel}
|
|
144
|
+
onClick={() => {
|
|
145
|
+
onModelChange?.(m.value)
|
|
146
|
+
setModelAnchor(null)
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{m.label}
|
|
150
|
+
</MenuItem>
|
|
151
|
+
))}
|
|
152
|
+
</Menu>
|
|
153
|
+
</>
|
|
154
|
+
)}
|
|
155
|
+
{endToolbarSlot}
|
|
156
|
+
</Box>
|
|
157
|
+
)}
|
|
158
|
+
</Box>
|
|
159
|
+
<Box sx={styles.footerSend}>{sendButton}</Box>
|
|
72
160
|
</Box>
|
|
73
161
|
{caption ? (
|
|
74
162
|
<FormHelperText sx={styles.footerCaption}>{caption}</FormHelperText>
|
|
@@ -20,24 +20,115 @@ export const styles = {
|
|
|
20
20
|
},
|
|
21
21
|
footerWrapper: {
|
|
22
22
|
padding: ({ spacing }) => spacing(1),
|
|
23
|
-
position: 'relative',
|
|
24
|
-
},
|
|
25
|
-
footerCorner: {
|
|
26
|
-
position: 'absolute',
|
|
27
|
-
bottom: '38px',
|
|
28
|
-
right: `max(16px, calc(50% - ${CHAT_MAX_WIDTH / 2}px + 6px))`,
|
|
29
|
-
margin: '0 auto',
|
|
30
23
|
},
|
|
31
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Rounded input box, matching the reference ChatFooter. A two-column grid:
|
|
26
|
+
* main column (input + optional slots) | send button, bottom-aligned so it
|
|
27
|
+
* stays at the end as the input grows. NO vertical padding — the textarea's own
|
|
28
|
+
* padding sets the ~32px single-line height, so the box hugs the input.
|
|
29
|
+
*/
|
|
30
|
+
footerBox: {
|
|
32
31
|
maxWidth: CHAT_MAX_WIDTH,
|
|
32
|
+
width: '100%',
|
|
33
33
|
margin: '0 auto',
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
display: 'grid',
|
|
35
|
+
gridTemplateColumns: '1fr auto',
|
|
36
|
+
alignItems: 'end',
|
|
37
|
+
columnGap: ({ spacing }) => spacing(0.5),
|
|
38
|
+
backgroundColor: ({ palette }) =>
|
|
39
|
+
palette.default?.background ?? 'rgba(44, 48, 50, 0.04)',
|
|
40
|
+
borderRadius: ({ spacing }) => spacing(0.5),
|
|
41
|
+
paddingInline: ({ spacing }) => spacing(0.5),
|
|
42
|
+
},
|
|
43
|
+
/** Main grid area: input stacked above the (optional) slots row. */
|
|
44
|
+
footerMain: {
|
|
45
|
+
minWidth: 0,
|
|
46
|
+
display: 'flex',
|
|
47
|
+
flexDirection: 'column',
|
|
48
|
+
gap: ({ spacing }) => spacing(0.5),
|
|
49
|
+
},
|
|
50
|
+
/**
|
|
51
|
+
* Send-button grid area, bottom-aligned (matches the reference flex-end row) so
|
|
52
|
+
* the button stays at the bottom as the input grows. The small bottom padding
|
|
53
|
+
* keeps it off the box edge — in the single-line state it reads as vertically
|
|
54
|
+
* centered (4px top / 4px bottom around the 24px button in the 32px row).
|
|
55
|
+
*/
|
|
56
|
+
footerSend: {
|
|
57
|
+
alignSelf: 'end',
|
|
58
|
+
paddingBottom: ({ spacing }) => spacing(0.5),
|
|
59
|
+
'& .MuiIconButton-root': {
|
|
60
|
+
padding: ({ spacing }) => spacing(0.5),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
/** The chrome-less multiline input (no underline / no filled affordance). */
|
|
64
|
+
footerInput: {
|
|
65
|
+
width: '100%',
|
|
66
|
+
typography: 'body2',
|
|
67
|
+
// The Meridian theme styles multiline InputBase as a tall "comment field"
|
|
68
|
+
// (min-height: 96px on the root, 12px/14px textarea padding). Override both to
|
|
69
|
+
// get the compact chat field: kill the root min-height and use the reference's
|
|
70
|
+
// 6px/10px padding — both need !important to beat the theme override. Height
|
|
71
|
+
// then comes from MUI's TextareaAutosize (minRows=1/maxRows=8): one line
|
|
72
|
+
// (~32px) → grows → scrolls.
|
|
73
|
+
//
|
|
74
|
+
// TextareaAutosize renders a hidden shadow <textarea> (it carries
|
|
75
|
+
// `aria-hidden="true"`, the same class, and an inline `padding: 0`) to measure
|
|
76
|
+
// the content height; MUI then applies that height to the visible input. Getting
|
|
77
|
+
// the compact ~32px single line right needs the two rules below split apart:
|
|
78
|
+
//
|
|
79
|
+
// • Font metrics (size/line-height/letter-spacing) go on BOTH textareas. The
|
|
80
|
+
// shadow measures with its own CSS, so if it isn't pinned to body2 it
|
|
81
|
+
// measures at the default ~24px line-height and the box ends up too tall.
|
|
82
|
+
// • Padding goes on the VISIBLE input ONLY (`:not([aria-hidden])`). A
|
|
83
|
+
// `!important` padding that also hit the shadow would clobber its measurement
|
|
84
|
+
// `padding: 0`, double-counting the padding (~44px instead of 32px).
|
|
85
|
+
//
|
|
86
|
+
// `!important` is required on both to beat the Meridian theme's tall
|
|
87
|
+
// "comment field" multiline overrides (12px/14px padding, larger metrics); the
|
|
88
|
+
// root `min-height: 0` kills that theme's 96px min-height.
|
|
89
|
+
minHeight: '0 !important',
|
|
90
|
+
'& textarea': {
|
|
91
|
+
fontSize: ({ typography }) => `${typography.body2.fontSize} !important`,
|
|
92
|
+
lineHeight: ({ typography }) =>
|
|
93
|
+
`${typography.body2.lineHeight} !important`,
|
|
94
|
+
letterSpacing: ({ typography }) =>
|
|
95
|
+
`${typography.body2.letterSpacing} !important`,
|
|
96
|
+
},
|
|
97
|
+
'& textarea:not([aria-hidden])': {
|
|
98
|
+
resize: 'none',
|
|
99
|
+
padding: ({ spacing }) => `${spacing(0.75, 1.25)} !important`,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
/**
|
|
103
|
+
* Slots row under the input: start slot + model selector + end slot. The left
|
|
104
|
+
* padding lines the row's content up with the input text above it: the input
|
|
105
|
+
* text is inset ~12px from the box edge (InputBase root padding + the textarea's
|
|
106
|
+
* 10px left padding) while the model-selector text button only insets ~8px, so a
|
|
107
|
+
* 4px indent here makes the slot label share the placeholder's left edge instead
|
|
108
|
+
* of sitting further out.
|
|
109
|
+
*/
|
|
110
|
+
footerSlots: {
|
|
111
|
+
display: 'flex',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
gap: ({ spacing }) => spacing(0.5),
|
|
114
|
+
minWidth: 0,
|
|
115
|
+
flexWrap: 'wrap',
|
|
116
|
+
paddingLeft: ({ spacing }) => spacing(0.5),
|
|
117
|
+
paddingBottom: ({ spacing }) => spacing(0.5),
|
|
118
|
+
},
|
|
119
|
+
modelSelector: {
|
|
120
|
+
textTransform: 'none',
|
|
121
|
+
color: ({ palette }) => palette.text.secondary,
|
|
122
|
+
maxWidth: '100%',
|
|
123
|
+
minWidth: 0,
|
|
124
|
+
'& .MuiButton-endIcon': {
|
|
125
|
+
marginLeft: ({ spacing }) => spacing(0.25),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
modelSelectorLabel: {
|
|
129
|
+
overflow: 'hidden',
|
|
130
|
+
textOverflow: 'ellipsis',
|
|
131
|
+
whiteSpace: 'nowrap',
|
|
41
132
|
},
|
|
42
133
|
footerCaption: {
|
|
43
134
|
textAlign: 'center',
|
|
@@ -51,7 +142,7 @@ export const styles = {
|
|
|
51
142
|
width: '100%',
|
|
52
143
|
maxHeight: '100%',
|
|
53
144
|
pt: 1,
|
|
54
|
-
pb:
|
|
145
|
+
pb: 3,
|
|
55
146
|
px: 2,
|
|
56
147
|
display: 'flex',
|
|
57
148
|
flexDirection: 'column',
|
|
@@ -121,8 +121,12 @@ export const styles = {
|
|
|
121
121
|
padding: ({ spacing }) => spacing(1),
|
|
122
122
|
borderRadius: ({ spacing }) => spacing(0.5),
|
|
123
123
|
backgroundColor: ({ palette }) => palette.background.default,
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
fontFamily: ({ typography }) =>
|
|
125
|
+
typography.code3?.fontFamily ??
|
|
126
|
+
typography.code1?.fontFamily ??
|
|
127
|
+
'monospace',
|
|
128
|
+
fontSize: ({ typography }) => typography.code3?.fontSize ?? '0.75rem',
|
|
129
|
+
lineHeight: ({ typography }) => typography.code3?.lineHeight ?? 1.333,
|
|
126
130
|
whiteSpace: 'pre-wrap',
|
|
127
131
|
wordBreak: 'break-word',
|
|
128
132
|
overflowY: 'auto',
|
|
@@ -160,8 +164,11 @@ export const styles = {
|
|
|
160
164
|
padding: ({ spacing }) => spacing(1),
|
|
161
165
|
background: ({ palette, spacing }) =>
|
|
162
166
|
`linear-gradient(to right, ${palette.action.hover} calc(${spacing(1)} + 3em), ${palette.background.default} calc(${spacing(1)} + 3em))`,
|
|
163
|
-
fontFamily:
|
|
164
|
-
|
|
167
|
+
fontFamily: ({ typography }) =>
|
|
168
|
+
typography.code3?.fontFamily ??
|
|
169
|
+
typography.code1?.fontFamily ??
|
|
170
|
+
'monospace',
|
|
171
|
+
fontSize: ({ typography }) => typography.code3?.fontSize ?? '0.75rem',
|
|
165
172
|
whiteSpace: 'pre-wrap',
|
|
166
173
|
wordBreak: 'break-word',
|
|
167
174
|
lineHeight: 1.6,
|