@svgrid/enterprise 2.0.4 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +18 -6
  2. package/dist/cdn/svgrid-enterprise.svelte-external.js +14035 -6842
  3. package/dist/node/studio.js +7888 -2459
  4. package/package.json +9 -4
  5. package/src/SvGridMasterDetail.svelte +24 -3
  6. package/src/SvGridScheduler.svelte +4410 -0
  7. package/src/SvPivotDesigner.svelte +1990 -1045
  8. package/src/SvSchemaChart.svelte +10 -9
  9. package/src/ai.test.ts +522 -522
  10. package/src/ai.ts +202 -2
  11. package/src/export.ts +7 -1
  12. package/src/index.ts +409 -384
  13. package/src/install.ts +10 -0
  14. package/src/pivot-chart.test.ts +86 -0
  15. package/src/pivot-chart.ts +112 -0
  16. package/src/scheduler.ts +37 -0
  17. package/src/scheduling.test.ts +194 -0
  18. package/src/scheduling.ts +293 -0
  19. package/src/sources/filters.ts +6 -0
  20. package/src/studio/HANDLERS-DESIGN.md +142 -0
  21. package/src/studio/cli.ts +7 -2
  22. package/src/studio/emit-project.test.ts +1447 -13
  23. package/src/studio/emit-project.ts +3995 -1273
  24. package/src/studio/emit-schema.ts +146 -29
  25. package/src/studio/index.ts +320 -195
  26. package/src/studio/project.test.ts +370 -0
  27. package/src/studio/project.ts +1146 -26
  28. package/src/studio/sample-data.ts +4 -1
  29. package/src/studio/samples/ats.ts +2 -2
  30. package/src/studio/samples/clinic.ts +4 -2
  31. package/src/studio/samples/crm.ts +16 -8
  32. package/src/studio/samples/events.ts +4 -2
  33. package/src/studio/samples/fleet.ts +4 -2
  34. package/src/studio/samples/gym.ts +4 -2
  35. package/src/studio/samples/hr.ts +3 -1
  36. package/src/studio/samples/live-data.ts +308 -308
  37. package/src/studio/samples/projects.ts +2 -2
  38. package/src/studio/samples/restaurant.ts +4 -2
  39. package/src/studio/samples/samples.test.ts +13 -5
  40. package/src/studio/samples/shared.ts +346 -305
  41. package/src/studio/samples/support.ts +3 -1
  42. package/src/studio/scaffold.test.ts +15 -1
  43. package/src/studio/scaffold.ts +16 -0
  44. package/src/studio/themes.ts +7 -0
  45. package/src/studio/ui-components.ts +472 -0
  46. package/src/sveltekit/transport.test.ts +26 -0
  47. package/src/sveltekit/transport.ts +50 -5
  48. package/dist/designer/assets/index-Dp44bTid.js +0 -939
  49. package/dist/designer/assets/index-RJp6x8tw.css +0 -1
  50. package/dist/designer/assets/jszip.min-CjMo-QGg.js +0 -2
  51. package/dist/designer/index.html +0 -13
package/src/ai.ts CHANGED
@@ -60,7 +60,7 @@ export type AIRequest = {
60
60
  maxOutputTokens?: number
61
61
  }
62
62
 
63
- export type AITask = 'filter' | 'smart-fill' | 'summarize' | 'classify' | 'export' | 'anomaly'
63
+ export type AITask = 'filter' | 'smart-fill' | 'summarize' | 'classify' | 'export' | 'anomaly' | 'chart'
64
64
 
65
65
  let provider: AIProvider | null = null
66
66
 
@@ -807,7 +807,157 @@ export async function aiFindAnomalies<
807
807
  }
808
808
 
809
809
  // ---------------------------------------------------------------------------
810
- // 7. Mock provider for examples + tests
810
+ // 7. Natural-language chart ("chart this")
811
+ // ---------------------------------------------------------------------------
812
+
813
+ export type AIChartType = 'bar' | 'line' | 'area' | 'pie'
814
+
815
+ export type AIChartPlan = {
816
+ type: AIChartType
817
+ /** Group-by (category-axis) column field, or null. */
818
+ dimension: string | null
819
+ /** Split-by column field: one series per distinct value, or null. */
820
+ series: string | null
821
+ /** Measure (value-axis) column field, or null. */
822
+ measure: string | null
823
+ reduce: 'sum' | 'avg' | 'count'
824
+ stacked: boolean
825
+ logScale: boolean
826
+ timeAxis: boolean
827
+ valueFormat: 'number' | 'currency' | 'percent'
828
+ rationale: string
829
+ }
830
+
831
+ export type AIChartOptions = {
832
+ /** Apply the plan to the grid's chart panel (open + configure). Default false. */
833
+ apply?: boolean
834
+ signal?: AbortSignal
835
+ }
836
+
837
+ /**
838
+ * Translate a natural-language request into a chart plan for the grid's built-in
839
+ * `charting` panel. Validates every field against the real schema, then (with
840
+ * `apply: true`) pushes it into the panel through `api.configureChart`.
841
+ */
842
+ export async function aiChart<
843
+ TFeatures extends TableFeatures,
844
+ TData extends RowData,
845
+ >(
846
+ api: SvGridApi<TFeatures, TData>,
847
+ query: string,
848
+ opts: AIChartOptions = {},
849
+ ): Promise<AIChartPlan> {
850
+ assertEnterpriseLicensed('AI assistant')
851
+ const schema = buildColumnSchema(api)
852
+ const prompt =
853
+ `You are a charting assistant for a data grid. Translate the user's request ` +
854
+ `into a strict-JSON chart plan the grid can render.\n\n` +
855
+ `Columns:\n${schemaToPromptBlock(schema)}\n\n` +
856
+ `Output JSON schema:\n` +
857
+ `{ "type": "bar"|"line"|"area"|"pie", ` +
858
+ `"dimension": "<a categorical column to group by>", ` +
859
+ `"series": "<a second categorical column to split into series, or null>", ` +
860
+ `"measure": "<a numeric column to aggregate>", ` +
861
+ `"reduce": "sum"|"avg"|"count", ` +
862
+ `"stacked": true|false, ` +
863
+ `"logScale": true|false, ` +
864
+ `"timeAxis": true|false, ` +
865
+ `"valueFormat": "number"|"currency"|"percent", ` +
866
+ `"rationale": "<one-sentence explanation>" }\n\n` +
867
+ `Rules: pick "dimension" and "series" from text/date columns and "measure" ` +
868
+ `from a numeric column; use only column names from the list above; ` +
869
+ `"stacked"/"stack" -> stacked:true; "log"/"logarithmic" -> logScale:true; ` +
870
+ `"over time"/"by date"/a date dimension -> timeAxis:true; ` +
871
+ `money/revenue/price/$ -> valueFormat:"currency"; rate/%/share -> valueFormat:"percent"; ` +
872
+ `else valueFormat:"number". default reduce is "sum". ` +
873
+ `Return JSON only, no prose.\n\n` +
874
+ `User request: ${query}`
875
+
876
+ const plan = await callJSON<AIChartPlan>({
877
+ prompt,
878
+ task: 'chart',
879
+ signal: opts.signal,
880
+ maxOutputTokens: 300,
881
+ })
882
+
883
+ const valid = new Set(schema.map((c) => c.field))
884
+ const numericFields = new Set(schema.filter((c) => c.type === 'number').map((c) => c.field))
885
+ const CHART_TYPES: AIChartType[] = ['bar', 'line', 'area', 'pie']
886
+ plan.type = CHART_TYPES.includes(plan.type) ? plan.type : 'bar'
887
+ plan.dimension = plan.dimension && valid.has(plan.dimension) ? plan.dimension : null
888
+ plan.series = plan.series && valid.has(plan.series) && plan.series !== plan.dimension ? plan.series : null
889
+ plan.measure =
890
+ plan.measure && numericFields.has(plan.measure)
891
+ ? plan.measure
892
+ : (schema.find((c) => c.type === 'number')?.field ?? null)
893
+ plan.reduce = ['sum', 'avg', 'count'].includes(plan.reduce) ? plan.reduce : 'sum'
894
+ plan.stacked = plan.stacked === true
895
+ plan.logScale = plan.logScale === true
896
+ plan.timeAxis = plan.timeAxis === true
897
+ plan.valueFormat = ['number', 'currency', 'percent'].includes(plan.valueFormat) ? plan.valueFormat : 'number'
898
+ plan.rationale = plan.rationale ?? ''
899
+
900
+ if (opts.apply === true) {
901
+ const cfg = api as unknown as { configureChart?: (c: Record<string, unknown>) => void }
902
+ cfg.configureChart?.({
903
+ open: true,
904
+ type: plan.type,
905
+ dimension: plan.dimension,
906
+ series: plan.series,
907
+ measure: plan.measure,
908
+ reduce: plan.reduce,
909
+ stacked: plan.stacked,
910
+ logScale: plan.logScale,
911
+ timeAxis: plan.timeAxis,
912
+ valueFormat: plan.valueFormat,
913
+ })
914
+ }
915
+
916
+ return plan
917
+ }
918
+
919
+ /**
920
+ * Wire the grid's chart-panel AI button to the model: registers a handler (via
921
+ * `api.setChartAiHandler`) that runs `aiChart` and returns the plan for the
922
+ * panel to apply + explain. Call once after `onApiReady`. `installEnterprise`
923
+ * calls this for you.
924
+ */
925
+ export function enableAiCharting<
926
+ TFeatures extends TableFeatures,
927
+ TData extends RowData,
928
+ >(api: SvGridApi<TFeatures, TData>): void {
929
+ const hook = api as unknown as {
930
+ setChartAiHandler?: (
931
+ fn: ((prompt: string) => Promise<Record<string, unknown> | null>) | null,
932
+ ) => void
933
+ }
934
+ hook.setChartAiHandler?.(async (prompt: string) => {
935
+ const plan = await aiChart(api, prompt)
936
+ return {
937
+ type: plan.type,
938
+ dimension: plan.dimension,
939
+ series: plan.series,
940
+ measure: plan.measure,
941
+ reduce: plan.reduce,
942
+ stacked: plan.stacked,
943
+ logScale: plan.logScale,
944
+ timeAxis: plan.timeAxis,
945
+ valueFormat: plan.valueFormat,
946
+ rationale: plan.rationale,
947
+ }
948
+ })
949
+ }
950
+
951
+ export function disableAiCharting<
952
+ TFeatures extends TableFeatures,
953
+ TData extends RowData,
954
+ >(api: SvGridApi<TFeatures, TData>): void {
955
+ const hook = api as unknown as { setChartAiHandler?: (fn: null) => void }
956
+ hook.setChartAiHandler?.(null)
957
+ }
958
+
959
+ // ---------------------------------------------------------------------------
960
+ // 8. Mock provider for examples + tests
811
961
  // ---------------------------------------------------------------------------
812
962
 
813
963
  /**
@@ -838,9 +988,59 @@ export const mockAIProvider: AIProvider = async (req) => {
838
988
  if (req.task === 'anomaly') {
839
989
  return JSON.stringify(buildMockAnomaly(req.prompt))
840
990
  }
991
+ if (req.task === 'chart') {
992
+ return JSON.stringify(buildMockChart(req.prompt))
993
+ }
841
994
  return '{}'
842
995
  }
843
996
 
997
+ function buildMockChart(prompt: string): AIChartPlan {
998
+ const q = extractUserRequest(prompt)
999
+ const typed = extractFieldTypes(prompt)
1000
+ const textFields = typed.filter((f) => f.type === 'string' || f.type === 'date').map((f) => f.name)
1001
+ const numberFields = typed.filter((f) => f.type === 'number').map((f) => f.name)
1002
+
1003
+ const type: AIChartType =
1004
+ /\bline\b|\btrend\b|over time/.test(q) ? 'line'
1005
+ : /\barea\b/.test(q) ? 'area'
1006
+ : /\bpie\b|share|proportion|breakdown/.test(q) ? 'pie'
1007
+ : 'bar'
1008
+
1009
+ const byMatch = q.match(/\bby\s+([a-z0-9_ ]+?)(?:\s+(?:split|stacked|grouped|by|as|,|$))/)
1010
+ const pickText = (hint?: string): string | null => {
1011
+ if (hint) {
1012
+ const f = textFields.find((t) => t.toLowerCase() === hint.trim() || (hint.trim().length >= 4 && t.toLowerCase().includes(hint.trim())))
1013
+ if (f) return f
1014
+ }
1015
+ return null
1016
+ }
1017
+ const dimension = pickText(byMatch?.[1]) ?? textFields[0] ?? null
1018
+ const series =
1019
+ /\b(split|stack(?:ed)?|by product|by category|per)\b/.test(q)
1020
+ ? (textFields.find((t) => t !== dimension) ?? null)
1021
+ : null
1022
+ const measure = numberFields.find((n) => q.includes(n.toLowerCase())) ?? numberFields[0] ?? null
1023
+ const reduce: 'sum' | 'avg' | 'count' =
1024
+ /\b(average|avg|mean)\b/.test(q) ? 'avg' : /\b(count|number of|how many)\b/.test(q) ? 'count' : 'sum'
1025
+ const stacked = /\bstack(ed)?\b/.test(q)
1026
+ const logScale = /\b(log|logarithmic)\b/.test(q)
1027
+ const timeAxis = /\b(over time|by date|by day|by month|by week|time series|timeline|trend)\b/.test(q)
1028
+ const valueFormat: 'number' | 'currency' | 'percent' =
1029
+ /\b(currency|money|revenue|sales|price|cost|\$|dollar)\b/.test(q) || /(price|revenue|amount|cost|sales)/i.test(measure ?? '')
1030
+ ? 'currency'
1031
+ : /\b(percent|percentage|%|rate|share)\b/.test(q)
1032
+ ? 'percent'
1033
+ : 'number'
1034
+
1035
+ return {
1036
+ type, dimension, series, measure, reduce, stacked, logScale, timeAxis, valueFormat,
1037
+ rationale:
1038
+ `${type} chart of ${reduce}(${measure ?? 'value'}) by ${dimension ?? 'category'}` +
1039
+ `${series ? `, split by ${series}` : ''}${stacked ? ', stacked' : ''}` +
1040
+ `${logScale ? ', log scale' : ''}${timeAxis ? ', date axis' : ''} (mock - wire a real model for genuine parsing).`,
1041
+ }
1042
+ }
1043
+
844
1044
  function extractUserRequest(prompt: string): string {
845
1045
  const m = prompt.match(/User request:\s*([\s\S]+)$/)
846
1046
  return (m?.[1] ?? '').trim().toLowerCase()
package/src/export.ts CHANGED
@@ -1172,7 +1172,13 @@ async function exportXlsxNative<
1172
1172
  : cols.map((_, i) => i)
1173
1173
  for (const ci of targetIdxs) {
1174
1174
  if (f.type === 'dataBar') condFormats.push({ kind: 'dataBar', colIdx: ci, color: f.color })
1175
- else condFormats.push({ kind: 'colorScale', colIdx: ci, colors: f.mid ? [f.min, f.mid, f.max] : [f.min, f.max] })
1175
+ else {
1176
+ // min/mid/max are optional on the format; fall back to the same defaults
1177
+ // the on-screen renderer uses (#ffffff -> #000000) so the export matches.
1178
+ const min = f.min ?? '#ffffff'
1179
+ const max = f.max ?? '#000000'
1180
+ condFormats.push({ kind: 'colorScale', colIdx: ci, colors: f.mid ? [min, f.mid, max] : [min, max] })
1181
+ }
1176
1182
  }
1177
1183
  }
1178
1184