From 0676377dab769e42491f4675706a1c12226a781a Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Wed, 7 Jan 2026 12:32:55 +0530 Subject: [PATCH 1/2] init develop canvas tool --- runtime/ai/ai.go | 1 + runtime/ai/develop_canvas.go | 994 ++++++++++++++++++++++++++++++++++ runtime/ai/developer_agent.go | 18 +- 3 files changed, 1007 insertions(+), 6 deletions(-) create mode 100644 runtime/ai/develop_canvas.go diff --git a/runtime/ai/ai.go b/runtime/ai/ai.go index 71eb2713c33..89d55825df8 100644 --- a/runtime/ai/ai.go +++ b/runtime/ai/ai.go @@ -58,6 +58,7 @@ func NewRunner(rt *runtime.Runtime, activity *activity.Client) *Runner { RegisterTool(r, &DevelopModel{Runtime: rt}) RegisterTool(r, &DevelopMetricsView{Runtime: rt}) + RegisterTool(r, &DevelopCanvas{Runtime: rt}) RegisterTool(r, &ListFiles{Runtime: rt}) RegisterTool(r, &SearchFiles{Runtime: rt}) RegisterTool(r, &ReadFile{Runtime: rt}) diff --git a/runtime/ai/develop_canvas.go b/runtime/ai/develop_canvas.go new file mode 100644 index 00000000000..261a7cf34c5 --- /dev/null +++ b/runtime/ai/develop_canvas.go @@ -0,0 +1,994 @@ +package ai + +import ( + "bytes" + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/modelcontextprotocol/go-sdk/mcp" + aiv1 "github.com/rilldata/rill/proto/gen/rill/ai/v1" + runtimev1 "github.com/rilldata/rill/proto/gen/rill/runtime/v1" + "github.com/rilldata/rill/runtime" + "github.com/rilldata/rill/runtime/drivers" + "github.com/rilldata/rill/runtime/pkg/fileutil" + "gopkg.in/yaml.v3" +) + +const DevelopCanvasName = "develop_canvas" + +type DevelopCanvas struct { + Runtime *runtime.Runtime +} + +var _ Tool[*DevelopCanvasArgs, *DevelopCanvasResult] = (*DevelopCanvas)(nil) + +type DevelopCanvasArgs struct { + Path string `json:"path" jsonschema:"The path of a .yaml file in which to create or update a Rill canvas dashboard definition."` + Prompt string `json:"prompt" jsonschema:"Optional description of changes to make if editing an existing canvas, or guidance for creating a new one."` + MetricsViewName string `json:"metrics_view_name" jsonschema:"Optional Rill metrics view to base the canvas on if creating a new canvas."` +} + +type DevelopCanvasResult struct { + CanvasName string `json:"canvas_name" jsonschema:"The name of the developed Rill canvas dashboard."` +} + +func (t *DevelopCanvas) Spec() *mcp.Tool { + return &mcp.Tool{ + Name: DevelopCanvasName, + Title: "Develop Canvas", + Description: "Developer agent that creates or edits a single Rill canvas dashboard.", + Meta: map[string]any{ + "openai/toolInvocation/invoking": "Developing canvas dashboard...", + "openai/toolInvocation/invoked": "Developed canvas dashboard", + }, + } +} + +func (t *DevelopCanvas) CheckAccess(ctx context.Context) (bool, error) { + return checkDeveloperAgentAccess(ctx, t.Runtime) +} + +func (t *DevelopCanvas) Handler(ctx context.Context, args *DevelopCanvasArgs) (*DevelopCanvasResult, error) { + // Validate input + if !strings.HasPrefix(args.Path, "/") && args.Path == "" { + args.Path = "/" + args.Path + } + + // If a prompt is provided, use the agent-based editing flow + if args.Prompt != "" { + return t.handleWithAgent(ctx, args) + } + + // Otherwise, use the creation flow (similar to GenerateCanvasFile) + return t.handleCreation(ctx, args) +} + +func (t *DevelopCanvas) handleWithAgent(ctx context.Context, args *DevelopCanvasArgs) (*DevelopCanvasResult, error) { + // Pre-invoke file listing to get context about existing canvases + session := GetSession(ctx) + _, err := session.CallTool(ctx, RoleAssistant, ListFilesName, nil, &ListFilesArgs{}) + if err != nil { + return nil, err + } + + // Pre-invoke file read for the target file + _, _ = session.CallTool(ctx, RoleAssistant, ReadFileName, nil, &ReadFileArgs{ + Path: args.Path, + }) + if ctx.Err() != nil { // Ignore tool error since the file may not exist + return nil, ctx.Err() + } + + // Add the system prompt + systemPrompt, err := t.systemPrompt(ctx) + if err != nil { + return nil, err + } + + // Add the user prompt + userPrompt, err := t.userPrompt(ctx, args) + if err != nil { + return nil, err + } + + // Run an LLM tool call loop + var response string + err = session.Complete(ctx, "Canvas developer loop", &response, &CompleteOptions{ + Messages: []*aiv1.CompletionMessage{ + NewTextCompletionMessage(RoleSystem, systemPrompt), + NewTextCompletionMessage(RoleUser, userPrompt), + }, + Tools: []string{SearchFilesName, ReadFileName, WriteFileName}, + MaxIterations: 10, + UnwrapCall: true, + }) + if err != nil { + return nil, err + } + + return &DevelopCanvasResult{ + CanvasName: fileutil.Stem(args.Path), // Get name from input path + }, nil +} + +func (t *DevelopCanvas) handleCreation(ctx context.Context, args *DevelopCanvasArgs) (*DevelopCanvasResult, error) { + // Get session + s := GetSession(ctx) + + // Validate that a metrics view is provided + if args.MetricsViewName == "" { + return nil, fmt.Errorf("metrics_view_name is required when creating a new canvas") + } + + // Get the metrics view resource + ctrl, err := t.Runtime.Controller(ctx, s.InstanceID()) + if err != nil { + return nil, err + } + + metricsView, err := ctrl.Get(ctx, &runtimev1.ResourceName{Kind: runtime.ResourceKindMetricsView, Name: args.MetricsViewName}, false) + if err != nil { + if errors.Is(err, drivers.ErrResourceNotFound) { + return nil, fmt.Errorf("metrics view %q not found", args.MetricsViewName) + } + return nil, err + } + + // Get the metrics view spec + spec := metricsView.GetMetricsView().State.ValidSpec + if spec == nil { + return nil, fmt.Errorf("metrics view %q is not valid", args.MetricsViewName) + } + + // Extract measures, dimensions, and time dimension + var measures []string + for _, m := range spec.Measures { + measures = append(measures, m.Name) + } + + var dimensions []string + for _, d := range spec.Dimensions { + dimensions = append(dimensions, d.Name) + } + + timeDimension := spec.TimeDimension + + // Try to generate the YAML with AI + var data string + res, err := t.generateCanvasYAMLWithAI(ctx, s.InstanceID(), args.MetricsViewName, measures, dimensions, timeDimension, args.Prompt) + if err == nil { + data = res + } + + // If we didn't manage to generate the YAML using AI, we fall back to the simple generator + if data == "" { + data, err = t.generateCanvasYAMLSimple(args.MetricsViewName, measures, dimensions, timeDimension) + if err != nil { + return nil, err + } + } + + // Write the file to the repo + repo, release, err := t.Runtime.Repo(ctx, s.InstanceID()) + if err != nil { + return nil, err + } + defer release() + err = repo.Put(ctx, args.Path, strings.NewReader(data)) + if err != nil { + return nil, err + } + + // Wait for it to reconcile + err = ctrl.Reconcile(ctx, runtime.GlobalProjectParserName) + if err != nil { + return nil, err + } + select { + case <-time.After(time.Millisecond * 500): + case <-ctx.Done(): + return nil, ctx.Err() + } + err = ctrl.WaitUntilIdle(ctx, true) + if err != nil { + return nil, err + } + + return &DevelopCanvasResult{ + CanvasName: fileutil.Stem(args.Path), // Get name from input path + }, nil +} + +func (t *DevelopCanvas) systemPrompt(ctx context.Context) (string, error) { + // Prepare template data + session := GetSession(ctx) + data := map[string]any{ + "ai_instructions": session.ProjectInstructions(), + } + + // Generate the system prompt + return executeTemplate(`You are a data engineer agent specialized in developing canvas dashboards in the Rill business intelligence platform. + + +Rill is a "business intelligence as code" platform where all resources are defined using YAML files in a project directory. +For the purposes of your work, you will only deal with **canvas dashboard** resources, which are YAML files that define interactive dashboards with multiple visualization components. +A canvas dashboard in Rill contains multiple rows of components (charts, leaderboards, KPIs, etc.) that visualize data from metrics views. +In Rill, when you write a file, the platform discovers and "reconciles" it immediately. For a canvas, reconcile validates the canvas definition and makes it available for viewing. + + + +A canvas dashboard consists of: +- **type**: Always "canvas" +- **display_name**: Human-friendly name for the dashboard +- **defaults**: Default settings like time_range and comparison_mode +- **rows**: Array of row objects, each containing: + - **height**: Row height in pixels (e.g., "400px") + - **items**: Array of component items, each containing: + - **width**: Width in grid units (1-12, where 12 is full width) + - **component**: The visualization component (inline definition) + +Available component types: +- **markdown**: Rich text content for documentation +- **kpi_grid**: Grid of key performance indicators +- **leaderboard**: Ranked table of dimension values +- **bar_chart**: Bar chart for categorical comparisons +- **line_chart**: Line chart for time series trends +- **area_chart**: Area chart for cumulative trends +- **stacked_bar**: Stacked bar chart for multi-measure comparisons +- **stacked_bar_normalized**: Normalized stacked bar for proportions +- **donut_chart**: Donut chart for part-to-whole relationships +- **heatmap**: Heatmap for two-dimensional patterns +- **combo_chart**: Combined chart with multiple mark types + + + +At a high level, you should follow these steps: +1. Leverage the "read_file" tool to understand the file's current contents, if any (it may return a file not found error). +2. Generate a new or updated canvas dashboard definition based on the user's prompt and save it to the requested path using the "write_file" tool. +3. The "write_file" tool will respond with the reconcile status. If there are parse or reconcile errors, you should fix them using the "write_file" tool. If there are no errors, your work is done. + +Additional instructions: +- Canvas dashboards use a 12-unit grid system for layout. Components can have widths from 3 to 12 units. +- Each row can contain multiple items that sum to 12 units (e.g., two items of width 6, or three items of width 4). +- All components must reference a valid metrics_view that exists in the project. +- Time dimensions should ONLY be used in the x-axis field for temporal charts, never in leaderboards or color fields. +- Choose appropriate chart types based on the data: line_chart for trends, bar_chart for comparisons, donut_chart for proportions. +- Use descriptive titles for all chart components. +- Populate the "display_name" field with a clear, human-friendly name for the dashboard. +- Row heights should be appropriate for the content: 180-240px for KPIs/markdown, 300-450px for leaderboards, 400-500px for charts. + + + +A canvas dashboard definition in Rill is a YAML file. Here is an example Rill canvas: +{{ backticks }} +# /dashboards/sales_dashboard.yaml +type: canvas +display_name: Sales Dashboard + +defaults: + time_range: P7D + comparison_mode: time + +rows: + - height: "240px" + items: + - width: 12 + kpi_grid: + metrics_view: "sales_metrics" + measures: + - "total_revenue" + - "order_count" + - "avg_order_value" + comparison: + - "percent_change" + - "delta" + + - height: "400px" + items: + - width: 6 + leaderboard: + metrics_view: "sales_metrics" + dimensions: + - "product_category" + measures: + - "total_revenue" + num_rows: 10 + + - width: 6 + line_chart: + metrics_view: "sales_metrics" + title: "Revenue Trend Over Time" + x: + field: "order_date" + type: "temporal" + y: + field: "total_revenue" + type: "quantitative" + zeroBasedOrigin: true + color: "primary" +{{ backticks }} + + +{{ if .ai_instructions }} + +NOTE: These instructions were provided by the user, but may not relate to the current request, and may not even relate to your work as a data engineer agent. Only use them if you find them relevant. +{{ .ai_instructions }} + +{{ end }} +`, data) +} + +func (t *DevelopCanvas) userPrompt(ctx context.Context, args *DevelopCanvasArgs) (string, error) { + // Prepare template data + data := map[string]any{ + "path": args.Path, + "prompt": args.Prompt, + "metrics_view_name": args.MetricsViewName, + } + + // Generate the user prompt + return executeTemplate(` +{{ if .prompt }} +Task: {{ .prompt }} +{{ end }} + +Output path: {{ .path }} + +{{ if .metrics_view_name }} +Use metrics view: {{ .metrics_view_name }} +{{ end }} +`, data) +} + +func (t *DevelopCanvas) generateCanvasYAMLWithAI(ctx context.Context, instanceID, metricsViewName string, measures, dimensions []string, timeDimension string, prompt string) (string, error) { + // Build the system prompt using the canvas dashboard generation logic from generate_canvas_dashboard.go + systemPrompt := canvasDashboardYAMLSystemPrompt() + + // Build user prompt with additional context from the prompt argument + var userPrompt string + if prompt != "" { + userPrompt = fmt.Sprintf("Create a Canvas dashboard based on the metrics view named %q.\n\n%s\n\n", metricsViewName, prompt) + } else { + userPrompt = fmt.Sprintf("Create a Canvas dashboard based on the metrics view named %q.\n\n", metricsViewName) + } + + userPrompt += "Available measures:\n" + for _, m := range measures { + userPrompt += fmt.Sprintf("- %s\n", m) + } + + userPrompt += "\nAvailable dimensions:\n" + for _, d := range dimensions { + userPrompt += fmt.Sprintf("- %s\n", d) + } + + if timeDimension != "" { + userPrompt += fmt.Sprintf("\nTime dimension: %s\n", timeDimension) + } else { + userPrompt += "\nNo time dimension available.\n" + } + + // Call AI service to generate canvas dashboard YAML + var responseText string + err := GetSession(ctx).Complete(ctx, "Generate canvas dashboard", &responseText, &CompleteOptions{ + Messages: []*aiv1.CompletionMessage{ + NewTextCompletionMessage(RoleSystem, systemPrompt), + NewTextCompletionMessage(RoleUser, userPrompt), + }, + }) + if err != nil { + return "", err + } + + // The AI may produce Markdown output. Remove the code tags around the YAML. + responseText = strings.TrimPrefix(responseText, "```yaml") + responseText = strings.TrimPrefix(responseText, "```") + responseText = strings.TrimSuffix(responseText, "```") + responseText = strings.TrimSpace(responseText) + + return responseText, nil +} + +func (t *DevelopCanvas) generateCanvasYAMLSimple(metricsViewName string, measures, dimensions []string, timeDimension string) (string, error) { + doc := &canvasDashboardYAML{ + Type: "canvas", + DisplayName: identifierToDisplayName(metricsViewName) + " Dashboard", + Defaults: &canvasDefaults{ + TimeRange: "P7D", + ComparisonMode: "time", + }, + } + + // Filter out time dimension from regular dimensions (time dimension is special and can't be used in leaderboards or color fields) + var nonTimeDimensions []string + for _, d := range dimensions { + if timeDimension == "" || d != timeDimension { + nonTimeDimensions = append(nonTimeDimensions, d) + } + } + + // Row 1: KPI Grid with up to 4 measures + kpiMeasures := measures + if len(kpiMeasures) > 4 { + kpiMeasures = measures[:4] + } + + if len(kpiMeasures) > 0 { + row1 := &canvasRow{ + Height: "240px", + Items: []*canvasItem{ + { + Width: 12, + Component: map[string]interface{}{ + "kpi_grid": map[string]interface{}{ + "measures": kpiMeasures, + "metrics_view": metricsViewName, + "comparison": []string{"percent_change", "delta", "previous"}, + }, + }, + }, + }, + } + doc.Rows = append(doc.Rows, row1) + } + + // Row 2: Leaderboard (left) and Chart (right) + if len(measures) > 0 { + row2Items := []*canvasItem{} + + // Left: Leaderboard with first non-time dimension and first measure + if len(nonTimeDimensions) > 0 { + row2Items = append(row2Items, &canvasItem{ + Width: 6, + Component: map[string]interface{}{ + "leaderboard": map[string]interface{}{ + "measures": []string{measures[0]}, + "metrics_view": metricsViewName, + "num_rows": 7, + "dimensions": []string{nonTimeDimensions[0]}, + }, + }, + }) + } + + // Right: Chart with time dimension if available + if timeDimension != "" { + chartType := "stacked_bar" + chartComponent := map[string]interface{}{ + "metrics_view": metricsViewName, + "title": fmt.Sprintf("%s over time", identifierToDisplayName(measures[0])), + "x": map[string]interface{}{ + "field": timeDimension, + "type": "temporal", + "limit": 10, + "showNull": true, + }, + "y": map[string]interface{}{ + "field": measures[0], + "type": "quantitative", + "zeroBasedOrigin": true, + }, + } + + // Add color dimension if we have non-time dimensions (can't use time dimension in color field) + if len(nonTimeDimensions) > 0 { + chartComponent["color"] = map[string]interface{}{ + "field": nonTimeDimensions[0], + "type": "nominal", + "limit": 10, + } + } + + row2Items = append(row2Items, &canvasItem{ + Width: 6, + Component: map[string]interface{}{ + chartType: chartComponent, + }, + }) + } + + if len(row2Items) > 0 { + row2 := &canvasRow{ + Items: row2Items, + Height: "400px", + } + doc.Rows = append(doc.Rows, row2) + } + } + + return marshalCanvasDashboardYAML(doc, false) +} + +// canvasDashboardYAML is a struct for generating a canvas dashboard YAML file +type canvasDashboardYAML struct { + Type string `yaml:"type,omitempty"` + DisplayName string `yaml:"display_name,omitempty"` + Defaults *canvasDefaults `yaml:"defaults,omitempty"` + Rows []*canvasRow `yaml:"rows,omitempty"` +} + +type canvasDefaults struct { + TimeRange string `yaml:"time_range,omitempty"` + ComparisonMode string `yaml:"comparison_mode,omitempty"` +} + +type canvasRow struct { + Height string `yaml:"height,omitempty"` + Items []*canvasItem `yaml:"items,omitempty"` +} + +type canvasItem struct { + Width interface{} `yaml:"width,omitempty"` + Component map[string]interface{} `yaml:",inline"` +} + +func marshalCanvasDashboardYAML(doc *canvasDashboardYAML, aiPowered bool) (string, error) { + buf := new(bytes.Buffer) + + buf.WriteString("# Canvas Dashboard YAML\n") + buf.WriteString("# Reference documentation: https://docs.rilldata.com/reference/project-files/canvas-dashboards\n") + if aiPowered { + buf.WriteString("# This file was generated using AI.\n") + } + buf.WriteString("\n") + + yamlBytes, err := yaml.Marshal(doc) + if err != nil { + return "", err + } + + var rootNode yaml.Node + if err := yaml.Unmarshal(yamlBytes, &rootNode); err != nil { + return "", err + } + + insertEmptyLinesInCanvasYaml(&rootNode) + + enc := yaml.NewEncoder(buf) + enc.SetIndent(2) + if err := enc.Encode(&rootNode); err != nil { + return "", err + } + + if err := enc.Close(); err != nil { + return "", err + } + + return buf.String(), nil +} + +func insertEmptyLinesInCanvasYaml(node *yaml.Node) { + for i := 0; i < len(node.Content); i++ { + if node.Content[i].Kind == yaml.MappingNode { + for j := 0; j < len(node.Content[i].Content); j += 2 { + keyNode := node.Content[i].Content[j] + valueNode := node.Content[i].Content[j+1] + + if keyNode.Value == "rows" { + keyNode.HeadComment = "\n" + } + insertEmptyLinesInCanvasYaml(valueNode) + } + } else if node.Content[i].Kind == yaml.SequenceNode { + for j := 0; j < len(node.Content[i].Content); j++ { + if node.Content[i].Content[j].Kind == yaml.MappingNode { + node.Content[i].Content[j].HeadComment = "\n" + } + } + } + } +} + +func canvasDashboardYAMLSystemPrompt() string { + // Chart type examples + barChartExample := `bar_chart: + metrics_view: "" + title: "Top Advertisers by Total Bids" + color: "primary" + x: + field: "" + limit: 20 + showNull: true + type: "nominal" + sort: "-y" + y: + field: "" + type: "quantitative" + zeroBasedOrigin: true` + + lineChartExample := `line_chart: + metrics_view: "" + title: "Trends Over Time" + color: + field: "" + limit: 3 + type: "nominal" + x: + field: "" + type: "temporal" + y: + field: "" + type: "quantitative" + zeroBasedOrigin: true` + + areaChartExample := `area_chart: + metrics_view: "" + title: "Magnitude Over Time" + color: + field: "" + type: "nominal" + x: + field: "" + limit: 20 + showNull: true + type: "temporal" + y: + field: "" + type: "quantitative" + zeroBasedOrigin: true` + + stackedBarExample := `stacked_bar: + metrics_view: "" + title: "Stacked Metrics Over Time" + color: + field: "rill_measures" + legendOrientation: "top" + type: "value" + x: + field: "" + limit: 20 + type: "temporal" + y: + field: "" + fields: + - "" + - "" + - "" + type: "quantitative" + zeroBasedOrigin: true` + + stackedBarNormalizedExample := `stacked_bar_normalized: + metrics_view: "" + title: "Proportional Distribution" + color: + field: "" + limit: 3 + type: "nominal" + x: + field: "" + limit: 20 + type: "temporal" + y: + field: "" + type: "quantitative" + zeroBasedOrigin: true` + + donutChartExample := `donut_chart: + metrics_view: "" + title: "Distribution by Category" + color: + field: "" + limit: 20 + type: "nominal" + innerRadius: 50 + measure: + field: "" + type: "quantitative" + showTotal: true` + + heatmapExample := `heatmap: + metrics_view: "" + title: "Density Across Two Dimensions" + color: + field: "" + type: "quantitative" + x: + field: "" + limit: 10 + type: "nominal" + y: + field: "" + limit: 24 + type: "nominal" + sort: "-color"` + + comboChartExample := `combo_chart: + metrics_view: "" + title: "Combined Metrics Analysis" + color: + field: "measures" + legendOrientation: "top" + type: "value" + x: + field: "" + limit: 20 + type: "temporal" + y1: + field: "" + mark: "bar" + type: "quantitative" + zeroBasedOrigin: true + y2: + field: "" + mark: "line" + type: "quantitative" + zeroBasedOrigin: true` + + leaderboardDetailsPrompt := ` +## Leaderboard Component + +The leaderboard component displays a ranked table of dimension values based on one or more measures. Leaderboards show ranked data with the top performers highlighted. + +**When to use:** +- Showing top N entities by a specific measure (e.g., top customers by revenue, top products by sales) +- Ranking categories or groups +- Displaying tabular data with sorting capabilities + +**Key parameters:** +- ` + "`metrics_view`" + `: The metrics view name to query data from (required) +- ` + "`dimensions`" + `: Array of dimension fields to display as columns (required, typically 1-2 dimensions) +- ` + "`measures`" + `: Array of measure fields to display as columns (required, typically 1-3 measures) +- ` + "`num_rows`" + `: Number of rows to display (default: 7, typically 5-15) + +**Important notes:** +- NEVER use time dimensions in the leaderboard dimensions array - time dimensions are only for temporal charts +- Use non-time dimensions only (e.g., customer_id, product_name, category, region) +- The leaderboard automatically sorts by the first measure in descending order +- Best suited for categorical data analysis, not time-series data + +**Example:** +` + "```yaml" + ` +leaderboard: + metrics_view: "" + dimensions: + - "" + measures: + - "" + num_rows: 10 +` + "```" + ` +` + + markdownDetailsPrompt := ` +## Markdown Component + +The markdown component allows you to add rich text content, documentation, and context to your canvas dashboards. Use it to provide descriptions, insights, and guidance for dashboard users. + +**When to use:** +- Adding overview and context about the dashboard's purpose +- Documenting key questions the dashboard answers +- Providing insights or analysis notes +- Adding headers, sections, or explanatory text + +**Key parameters:** +- ` + "`content`" + `: The markdown text content (required, supports full markdown syntax) +- ` + "`alignment`" + `: Optional alignment settings + - ` + "`horizontal`" + `: left, center, or right (default: left) + - ` + "`vertical`" + `: top, middle, or bottom (default: top) + +**Supported markdown features:** +- Headers (# H1, ## H2, ### H3, etc.) +- Bold (**text**) and italic (*text*) +- Lists (bulleted and numbered) +- Links [text](url) +- Horizontal rules (---) + +**Best practices:** +- Place markdown components at the top of the dashboard to provide context +- Add empty new line between each markdown feature to render properly +- Use headers to organize content and create visual hierarchy +- Keep content concise and focused on key insights +- Use bullet points for easy scanning + +**Example:** + +Notice how empty new lines have been added in the content to render properly. +` + "```yaml" + ` +markdown: + content: | + ## Dashboard Overview + + This dashboard provides a comprehensive overview of bidding activity, spend, win rates across your advertising inventory. + + --- + alignment: + horizontal: left + vertical: top +` + "```" + ` +` + + chartGuidelinesPrompt := ` + +# Chart configuration guidelines + +### Data Types +- **nominal**: Categorical data (e.g., categories, names, labels), use for dimensions +- **temporal**: Time-based data (dates, timestamps), use for time dimensions and timestamps +- **quantitative**: Numerical data (counts, amounts, measurements), use for measures +- **value**: Special type for multiple measures (used in color field) + +### Special Fields +- **rill_measures**: Special field for multiple measures in stacked charts and area charts. The field name is only used in color field object. DO NOT USE it for other keys except for "color" key in the field object. + +### Common Field Properties +- **field**: The field name from the metrics view +- **type**: Data type (nominal, temporal, quantitative, value) +- **limit**: Maximum number of values to display for selected sort mode +- **showNull**: Include null values in the visualization (true/false) +- **sort**: Sorting order + - ` + "`\"x\"`" + ` or ` + "`\"-x\"`" + `: Sort by x-axis values (ascending/descending) + - ` + "`\"y\"`" + ` or ` + "`\"-y\"`" + `: Sort by y-axis values (ascending/descending) + - ` + "`\"color\"`" + ` or ` + "`\"-color\"`" + `: Sort by color field values (ascending/descending) Only used for heatmap charts + - ` + "`\"measure\"`" + ` or ` + "`\"-measure\"`" + `: Sort by measure field values (ascending/descending) Only used for donut charts + - Array of values for custom sort order (e.g., weekday names) +- **zeroBasedOrigin**: Start y-axis from zero (true/false) +- **showTotal**: Displays the measure total without any breakdown. Only used for donut chart to display totals in center + +## Color Configuration for Charts + +Colors can be specified in two ways depending on the chart type and requirements: + +### 1. Single Color String +For bar_chart, stacked_bar, line_chart, and area_chart types in single measure mode and only 1 dimensions is involved. That is, there is no color dimension, only the X-axis dimension is present: +- Named colors: "primary" or "secondary" +- CSS color values: "#FF5733", "rgb(255, 87, 51)", "hsl(12, 100%%, 60%%)" +- **Note**: If no color field object is provided, a color string MUST be included for the mentioned chart types + +### 2. Field-Based Color Object +For dynamic coloring based on data dimensions: +` + "```json" + ` +{ + "field": "dimension_name|rill_measures", // The data field to base colors on + "type": "nominal|value", // Data type, use value only when field in "rill_measures" + "limit": 10, // Limit denotes the maximum number of color categories + "legendOrientation": "top|bottom|left|right" // Legend position (optional) +} +` + "```" + ` + +## Visualization Best Practices & Usage Guidelines + +Choose the appropriate chart type based on your data and analysis goals: + +### Time Series Analysis +- **` + "`line_chart`" + `**: Best for showing trends over time, especially with continuous data or multiple series +- **` + "`area_chart`" + `**: Ideal for cumulative trends or showing magnitude of change over time +- **Temporal axis**: Always use temporal encoding for time-based x-axis + +### Categorical Comparisons +- **` + "`bar_chart`" + `**: Standard choice for comparing discrete categories or groups +- **` + "`stacked_bar`" + `**: Standard choice for comparing discrete categories or groups when split by dimension is involved +- **Nominal axis**: Use nominal encoding for categorical x-axis + +### Part-to-Whole Relationships +- **` + "`donut_chart`" + `**: Shows composition of a whole +- **` + "`stacked_bar_normalized`" + `**: Compares part-to-whole across multiple groups +- **Consideration**: Avoid when precise value comparison is needed + +### Multiple Dimensions +- **` + "`combo_chart`" + `**: Combines different chart types for metrics with different scales. Used when comparing 2 measures. +- **` + "`stacked_bar`" + `**: Shows cumulative values across categories (use for 2+ measures) +- **` + "`heatmap`" + `**: Reveals patterns across two categorical dimensions along with single measure +- **Color encoding**: Add a second dimension to bar, stacked bar, line and area charts through color mapping + +### Field Configuration +- **Y-axis with multiple measures**: Use the 'fields' array in y-axis configuration +- **Field names**: Must exactly match field names in the metrics view (case-sensitive) +- **Metrics view name**: Must exactly match available view names + +## Chart Type Examples + +### Bar Chart +` + "```yaml" + ` +%s +` + "```" + ` + +### Line Chart +` + "```yaml" + ` +%s +` + "```" + ` + +### Area Chart +` + "```yaml" + ` +%s +` + "```" + ` + +### Stacked Bar Chart +` + "```yaml" + ` +%s +` + "```" + ` + +### Normalized Stacked Bar Chart +` + "```yaml" + ` +%s +` + "```" + ` + +### Donut Chart +` + "```yaml" + ` +%s +` + "```" + ` + +### Heatmap +` + "```yaml" + ` +%s +` + "```" + ` + +### Combo Chart +` + "```yaml" + ` +%s +` + "```" + ` +` + + // Format the chart guidelines with all the examples + formattedChartGuidelines := fmt.Sprintf(chartGuidelinesPrompt, + barChartExample, + lineChartExample, + areaChartExample, + stackedBarExample, + stackedBarNormalizedExample, + donutChartExample, + heatmapExample, + comboChartExample, + ) + + return fmt.Sprintf(`You are an agent whose only task is to create a Canvas dashboard YAML configuration based on a metrics view. +The canvas should include business-relevant components and visualizations that help users understand their key metrics. + +Your output should only consist of valid YAML following the canvas dashboard specification. + +# Layout Guidelines: + +## Row and Item Structure +- Canvas dashboards contain multiple rows, each with an 'items' array containing widgets/components +- Each row has a total span of **12 units** +- Components can have widths from **3 (minimum)** to **12 (maximum)** units +- Maximum of **4 components** can fit in a single row (4 × 3 = 12) +- You can add multiple rows, but keep the dashboard balanced and not overwhelming + +## Width Best Practices +- Full width (12): Use for KPI grids, markdown overviews, or primary visualizations +- Half width (6): Use for side-by-side comparisons (leaderboard + chart, chart + chart) +- Third width (4): Use for three equal components in a row +- Quarter width (3): Use for four equal components in a row (e.g., small KPI cards, small charts) + +## Row Height Recommendations +- Markdown/Text: 120px-200px (depending on content) +- KPI Grid: 250px-240px +- Charts: 400px-500px (standard visualization height) +- Leaderboards: 300px-450px (depending on num_rows) + +# Content Guidelines: +1. Row 1 should have a markdown component at the top to provide dashboard context and overview +2. Row 2 should contain a KPI grid with 2-4 of the most business-relevant measures +3. Row 3 should contain: + - Left (width 6): A leaderboard with the most important NON-TIME dimension and a relevant measures + - Right (width 6): A stacked_bar or line_chart showing trends over time (if time dimension exists) +4. You may add Row 4 or Row 5 with additional relevant charts. Pick the right number of component per row as needed. +5. All components must reference the provided metrics_view name +6. Choose dimensions and measures that would provide the most business value +7. Only use valid and available measures and dimensions names provided by the user +8. For charts with time dimension, use the timestamp from the metrics view as the field name +9. Use descriptive titles for charts +10. IMPORTANT: The time dimension is SPECIAL - it can ONLY be used in the x-axis field for temporal charts. NEVER use the time dimension in: + - Leaderboard dimensions + - Color fields + - Any other dimension fields + +# Component types available: +- markdown: For adding text, context, and documentation +- kpi_grid: For key metrics summary +- leaderboard: For top rankings +- stacked_bar: For temporal or categorical comparisons +- line_chart: For time series trends +- bar_chart: For categorical comparisons +- donut_chart: For proportional breakdowns +- heatmap: For two-dimensional distribution + +%s + +%s + +%s +`, markdownDetailsPrompt, leaderboardDetailsPrompt, formattedChartGuidelines) +} diff --git a/runtime/ai/developer_agent.go b/runtime/ai/developer_agent.go index 9a96dc25c31..4fb986de13c 100644 --- a/runtime/ai/developer_agent.go +++ b/runtime/ai/developer_agent.go @@ -71,7 +71,7 @@ func (t *DeveloperAgent) Handler(ctx context.Context, args *DeveloperAgentArgs) var response string err = s.Complete(ctx, "Developer loop", &response, &CompleteOptions{ Messages: messages, - Tools: []string{ListFilesName, SearchFilesName, ReadFileName, DevelopModelName, DevelopMetricsViewName}, + Tools: []string{ListFilesName, SearchFilesName, ReadFileName, DevelopModelName, DevelopMetricsViewName, DevelopCanvasName}, MaxIterations: 10, UnwrapCall: true, }) @@ -92,7 +92,7 @@ func (t *DeveloperAgent) systemPrompt(ctx context.Context) (string, error) { } // Generate the system prompt - return executeTemplate(`You are a data engineer agent specialized in developing data models and metrics view definitions in the Rill business intelligence platform. + return executeTemplate(`You are a data engineer agent specialized in developing data models, metrics view definitions, and canvas dashboards in the Rill business intelligence platform. Rill is a "business intelligence as code" platform where all resources are defined using YAML files containing SQL snippets in a project directory. @@ -100,8 +100,11 @@ Rill supports many different resource types, such as connectors, models, metrics For the purposes of your work, you will only deal with: - **Models**: SQL statements and related metadata that produce a single table in the project's database (usually DuckDB or Clickhouse). - **Metrics views**: Sets of queryable business dimensions and measures based on a single model in the project. This is sometimes called the "semantic layer" or "metrics layer" in other tools. -Rill maintains a DAG of resources. In this DAG, metrics views are always derived from a single model. Multiple metrics views can derive from a single model, although usually it makes sense to have just one metrics view per model. -When users ask you to develop a "dashboard", that just means to develop a new metrics view (and possibly a new underlying model). Rill automatically creates visual dashboards for each metrics view. +- **Canvas dashboards**: Interactive dashboards with multiple visualization components (charts, leaderboards, KPIs) that query data from metrics views. Canvas dashboards provide rich, customizable layouts for data visualization. +Rill maintains a DAG of resources. In this DAG, metrics views are always derived from a single model, and canvas dashboards reference one or more metrics views. Multiple metrics views can derive from a single model, although usually it makes sense to have just one metrics view per model. +When users ask you to develop a "dashboard", they may mean either: +1. An explore dashboard (automatic) - develop a new metrics view (and possibly a new underlying model). Rill automatically creates visual dashboards for each metrics view. +2. A canvas dashboard (custom) - develop a new canvas dashboard using the "develop_canvas" tool, which references existing metrics views and allows full layout customization. @@ -128,8 +131,11 @@ At a high level, you should follow these steps: 4. Only if necessary, add a new metrics view or update an existing metrics view to reflect the user's request. The metrics view should use a model in the project, which may already exist or may have been added in step 2. - To *create* a new metrics view: Use "develop_metrics_view" with path and model (no prompt). - To *edit* an existing metrics view: Use "develop_metrics_view" with path, model, AND a prompt describing the changes. -5. If a user requests a new model/file, DO NOT overwrite existing file and instead use a unique name. -6. After successfully creating/updating the artifacts, provide a summary with links using the following format: +5. Only if necessary, add a new canvas dashboard or update an existing canvas dashboard to reflect the user's request. Canvas dashboards reference metrics views, which must already exist in the project. + - To *create* a new canvas: Use "develop_canvas" with path and metrics_view_name (and optional prompt for guidance). + - To *edit* an existing canvas: Use "develop_canvas" with path AND a prompt describing the changes. +6. If a user requests a new model/file, DO NOT overwrite existing file and instead use a unique name. +7. After successfully creating/updating the artifacts, provide a summary with links using the following format: {{ backticks }} ## Summary of Changes I've created the following files for you: From 735eea4706b7e4b47b562a5cc7d4fb8d445a06db Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Thu, 8 Jan 2026 16:00:03 +0530 Subject: [PATCH 2/2] add component schema to prompt --- runtime/ai/data/component-template-v1.json | 1344 ++++++++++++++++ runtime/ai/develop_canvas.go | 55 +- .../parser/data/component-template-v1.json | 1373 ++++++++++++++--- web-common/package.json | 2 +- .../src/features/canvas/components/types.ts | 57 +- .../src/features/components/charts/types.ts | 22 +- 6 files changed, 2582 insertions(+), 271 deletions(-) create mode 100644 runtime/ai/data/component-template-v1.json diff --git a/runtime/ai/data/component-template-v1.json b/runtime/ai/data/component-template-v1.json new file mode 100644 index 00000000000..8ab78631dc3 --- /dev/null +++ b/runtime/ai/data/component-template-v1.json @@ -0,0 +1,1344 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "line_chart": { + "$ref": "#/definitions/CartesianCanvasChartSpec" + } + }, + "required": [ + "line_chart" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "bar_chart": { + "$ref": "#/definitions/CartesianCanvasChartSpec" + } + }, + "required": [ + "bar_chart" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "area_chart": { + "$ref": "#/definitions/CartesianCanvasChartSpec" + } + }, + "required": [ + "area_chart" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "stacked_bar": { + "$ref": "#/definitions/CartesianCanvasChartSpec" + } + }, + "required": [ + "stacked_bar" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "stacked_bar_normalized": { + "$ref": "#/definitions/CartesianCanvasChartSpec" + } + }, + "required": [ + "stacked_bar_normalized" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "donut_chart": { + "$ref": "#/definitions/CircularCanvasChartSpec" + } + }, + "required": [ + "donut_chart" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "funnel_chart": { + "$ref": "#/definitions/FunnelChartSpec" + } + }, + "required": [ + "funnel_chart" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "heatmap": { + "$ref": "#/definitions/HeatmapChartSpec" + } + }, + "required": [ + "heatmap" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "combo_chart": { + "$ref": "#/definitions/ComboChartSpec" + } + }, + "required": [ + "combo_chart" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "pivot": { + "$ref": "#/definitions/PivotSpec" + } + }, + "required": [ + "pivot" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "table": { + "$ref": "#/definitions/TableSpec" + } + }, + "required": [ + "table" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kpi_grid": { + "$ref": "#/definitions/KPIGridSpec" + } + }, + "required": [ + "kpi_grid" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "markdown": { + "$ref": "#/definitions/MarkdownSpec" + } + }, + "required": [ + "markdown" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "image": { + "$ref": "#/definitions/ImageSpec" + } + }, + "required": [ + "image" + ], + "type": "object" + } + ], + "definitions": { + "CartesianCanvasChartSpec": { + "additionalProperties": false, + "properties": { + "color": { + "anyOf": [ + { + "$ref": "#/definitions/FieldConfig%3C%22nominal%22%3E" + }, + { + "type": "string" + } + ] + }, + "description": { + "type": "string" + }, + "dimension_filters": { + "type": "string" + }, + "metrics_view": { + "type": "string" + }, + "show_description_as_tooltip": { + "type": "boolean" + }, + "time_filters": { + "type": "string" + }, + "title": { + "type": "string" + }, + "tooltip": { + "$ref": "#/definitions/FieldConfig" + }, + "vl_config": { + "type": "string" + }, + "x": { + "$ref": "#/definitions/FieldConfig%3C(%22nominal%22%7C%22time%22)%3E" + }, + "y": { + "$ref": "#/definitions/FieldConfig%3C%22quantitative%22%3E" + } + }, + "required": [ + "metrics_view" + ], + "type": "object" + }, + "Categorical": { + "enum": [ + "accent", + "category10", + "category20", + "category20b", + "category20c", + "dark2", + "paired", + "pastel1", + "pastel2", + "set1", + "set2", + "set3", + "tableau10", + "tableau20", + "observable10" + ], + "type": "string" + }, + "ChartLegend": { + "enum": [ + "none", + "top", + "bottom", + "left", + "right" + ], + "type": "string" + }, + "ChartSortDirection": { + "anyOf": [ + { + "enum": [ + "x", + "y", + "-x", + "-y", + "color", + "-color", + "measure", + "-measure" + ], + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "CircularCanvasChartSpec": { + "additionalProperties": false, + "properties": { + "color": { + "$ref": "#/definitions/FieldConfig%3C%22nominal%22%3E" + }, + "description": { + "type": "string" + }, + "dimension_filters": { + "type": "string" + }, + "innerRadius": { + "type": "number" + }, + "measure": { + "$ref": "#/definitions/FieldConfig%3C%22quantitative%22%3E" + }, + "metrics_view": { + "type": "string" + }, + "show_description_as_tooltip": { + "type": "boolean" + }, + "time_filters": { + "type": "string" + }, + "title": { + "type": "string" + }, + "tooltip": { + "$ref": "#/definitions/FieldConfig" + }, + "vl_config": { + "type": "string" + } + }, + "required": [ + "metrics_view" + ], + "type": "object" + }, + "ColorMapping": { + "items": { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "value", + "color" + ], + "type": "object" + }, + "type": "array" + }, + "ColorRangeMapping": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "mode": { + "const": "scheme", + "type": "string" + }, + "scheme": { + "anyOf": [ + { + "$ref": "#/definitions/ColorScheme" + }, + { + "const": "sequential", + "type": "string" + }, + { + "const": "diverging", + "type": "string" + } + ] + } + }, + "required": [ + "mode", + "scheme" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "end": { + "type": "string" + }, + "mode": { + "const": "gradient", + "type": "string" + }, + "start": { + "type": "string" + } + }, + "required": [ + "mode", + "start", + "end" + ], + "type": "object" + } + ] + }, + "ColorScheme": { + "anyOf": [ + { + "$ref": "#/definitions/Categorical" + }, + { + "$ref": "#/definitions/SequentialSingleHue" + }, + { + "$ref": "#/definitions/SequentialMultiHue" + }, + { + "$ref": "#/definitions/Diverging" + }, + { + "$ref": "#/definitions/Cyclical" + } + ] + }, + "ComboChartSpec": { + "additionalProperties": false, + "properties": { + "color": { + "$ref": "#/definitions/FieldConfig%3C%22nominal%22%3E" + }, + "metrics_view": { + "type": "string" + }, + "x": { + "$ref": "#/definitions/FieldConfig%3C(%22nominal%22%7C%22time%22)%3E" + }, + "y1": { + "$ref": "#/definitions/FieldConfig%3C(%22quantitative%22%7C%22mark%22)%3E" + }, + "y2": { + "$ref": "#/definitions/FieldConfig%3C(%22quantitative%22%7C%22mark%22)%3E" + } + }, + "required": [ + "metrics_view" + ], + "type": "object" + }, + "ComponentAlignment": { + "additionalProperties": false, + "properties": { + "horizontal": { + "$ref": "#/definitions/HoritzontalAlignment" + }, + "vertical": { + "$ref": "#/definitions/VeriticalAlignment" + } + }, + "required": [ + "vertical", + "horizontal" + ], + "type": "object" + }, + "ComponentComparisonOptions": { + "enum": [ + "previous", + "delta", + "percent_change" + ], + "type": "string" + }, + "Cyclical": { + "enum": [ + "rainbow", + "sinebow" + ], + "type": "string" + }, + "Diverging": { + "enum": [ + "blueorange", + "blueorange-3", + "blueorange-4", + "blueorange-5", + "blueorange-6", + "blueorange-7", + "blueorange-8", + "blueorange-9", + "blueorange-10", + "blueorange-11", + "brownbluegreen", + "brownbluegreen-3", + "brownbluegreen-4", + "brownbluegreen-5", + "brownbluegreen-6", + "brownbluegreen-7", + "brownbluegreen-8", + "brownbluegreen-9", + "brownbluegreen-10", + "brownbluegreen-11", + "purplegreen", + "purplegreen-3", + "purplegreen-4", + "purplegreen-5", + "purplegreen-6", + "purplegreen-7", + "purplegreen-8", + "purplegreen-9", + "purplegreen-10", + "purplegreen-11", + "pinkyellowgreen", + "pinkyellowgreen-3", + "pinkyellowgreen-4", + "pinkyellowgreen-5", + "pinkyellowgreen-6", + "pinkyellowgreen-7", + "pinkyellowgreen-8", + "pinkyellowgreen-9", + "pinkyellowgreen-10", + "pinkyellowgreen-11", + "purpleorange", + "purpleorange-3", + "purpleorange-4", + "purpleorange-5", + "purpleorange-6", + "purpleorange-7", + "purpleorange-8", + "purpleorange-9", + "purpleorange-10", + "purpleorange-11", + "redblue", + "redblue-3", + "redblue-4", + "redblue-5", + "redblue-6", + "redblue-7", + "redblue-8", + "redblue-9", + "redblue-10", + "redblue-11", + "redgrey", + "redgrey-3", + "redgrey-4", + "redgrey-5", + "redgrey-6", + "redgrey-7", + "redgrey-8", + "redgrey-9", + "redgrey-10", + "redgrey-11", + "redyellowblue", + "redyellowblue-3", + "redyellowblue-4", + "redyellowblue-5", + "redyellowblue-6", + "redyellowblue-7", + "redyellowblue-8", + "redyellowblue-9", + "redyellowblue-10", + "redyellowblue-11", + "redyellowgreen", + "redyellowgreen-3", + "redyellowgreen-4", + "redyellowgreen-5", + "redyellowgreen-6", + "redyellowgreen-7", + "redyellowgreen-8", + "redyellowgreen-9", + "redyellowgreen-10", + "redyellowgreen-11", + "spectral", + "spectral-3", + "spectral-4", + "spectral-5", + "spectral-6", + "spectral-7", + "spectral-8", + "spectral-9", + "spectral-10", + "spectral-11" + ], + "type": "string" + }, + "FieldConfig": { + "additionalProperties": false, + "properties": { + "colorMapping": { + "$ref": "#/definitions/ColorMapping" + }, + "colorRange": { + "$ref": "#/definitions/ColorRangeMapping" + }, + "field": { + "type": "string" + }, + "fields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "labelAngle": { + "type": "number" + }, + "legendOrientation": { + "$ref": "#/definitions/ChartLegend" + }, + "limit": { + "type": "number" + }, + "mark": { + "enum": [ + "bar", + "line" + ], + "type": "string" + }, + "max": { + "type": "number" + }, + "min": { + "type": "number" + }, + "showAxisTitle": { + "type": "boolean" + }, + "showNull": { + "type": "boolean" + }, + "showTotal": { + "type": "boolean" + }, + "sort": { + "$ref": "#/definitions/ChartSortDirection" + }, + "timeUnit": { + "type": "string" + }, + "type": { + "enum": [ + "quantitative", + "ordinal", + "nominal", + "temporal", + "value" + ], + "type": "string" + }, + "zeroBasedOrigin": { + "type": "boolean" + } + }, + "required": [ + "field", + "type" + ], + "type": "object" + }, + "FieldConfig<\"nominal\">": { + "properties": { + "colorMapping": { + "$ref": "#/definitions/ColorMapping" + }, + "field": { + "type": "string" + }, + "fields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "labelAngle": { + "type": "number" + }, + "legendOrientation": { + "$ref": "#/definitions/ChartLegend" + }, + "limit": { + "type": "number" + }, + "showAxisTitle": { + "type": "boolean" + }, + "showNull": { + "type": "boolean" + }, + "sort": { + "$ref": "#/definitions/ChartSortDirection" + }, + "type": { + "enum": [ + "quantitative", + "ordinal", + "nominal", + "temporal", + "value" + ], + "type": "string" + } + }, + "required": [ + "field", + "type" + ], + "type": "object" + }, + "FieldConfig<\"quantitative\">": { + "properties": { + "colorRange": { + "$ref": "#/definitions/ColorRangeMapping" + }, + "field": { + "type": "string" + }, + "fields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "max": { + "type": "number" + }, + "min": { + "type": "number" + }, + "showAxisTitle": { + "type": "boolean" + }, + "showTotal": { + "type": "boolean" + }, + "type": { + "enum": [ + "quantitative", + "ordinal", + "nominal", + "temporal", + "value" + ], + "type": "string" + }, + "zeroBasedOrigin": { + "type": "boolean" + } + }, + "required": [ + "field", + "type" + ], + "type": "object" + }, + "FieldConfig<(\"nominal\"|\"time\")>": { + "properties": { + "colorMapping": { + "$ref": "#/definitions/ColorMapping" + }, + "field": { + "type": "string" + }, + "fields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "labelAngle": { + "type": "number" + }, + "legendOrientation": { + "$ref": "#/definitions/ChartLegend" + }, + "limit": { + "type": "number" + }, + "showAxisTitle": { + "type": "boolean" + }, + "showNull": { + "type": "boolean" + }, + "sort": { + "$ref": "#/definitions/ChartSortDirection" + }, + "timeUnit": { + "type": "string" + }, + "type": { + "enum": [ + "quantitative", + "ordinal", + "nominal", + "temporal", + "value" + ], + "type": "string" + } + }, + "required": [ + "field", + "type" + ], + "type": "object" + }, + "FieldConfig<(\"quantitative\"|\"mark\")>": { + "properties": { + "colorRange": { + "$ref": "#/definitions/ColorRangeMapping" + }, + "field": { + "type": "string" + }, + "fields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "mark": { + "enum": [ + "bar", + "line" + ], + "type": "string" + }, + "max": { + "type": "number" + }, + "min": { + "type": "number" + }, + "showAxisTitle": { + "type": "boolean" + }, + "showTotal": { + "type": "boolean" + }, + "type": { + "enum": [ + "quantitative", + "ordinal", + "nominal", + "temporal", + "value" + ], + "type": "string" + }, + "zeroBasedOrigin": { + "type": "boolean" + } + }, + "required": [ + "field", + "type" + ], + "type": "object" + }, + "FunnelBreakdownMode": { + "enum": [ + "dimension", + "measures" + ], + "type": "string" + }, + "FunnelChartSpec": { + "additionalProperties": false, + "properties": { + "breakdownMode": { + "$ref": "#/definitions/FunnelBreakdownMode" + }, + "color": { + "$ref": "#/definitions/FunnelColorMode" + }, + "measure": { + "$ref": "#/definitions/FieldConfig%3C%22quantitative%22%3E" + }, + "metrics_view": { + "type": "string" + }, + "mode": { + "$ref": "#/definitions/FunnelMode" + }, + "stage": { + "$ref": "#/definitions/FieldConfig%3C%22nominal%22%3E" + } + }, + "required": [ + "metrics_view" + ], + "type": "object" + }, + "FunnelColorMode": { + "enum": [ + "stage", + "measure", + "name", + "value" + ], + "type": "string" + }, + "FunnelMode": { + "enum": [ + "width", + "order" + ], + "type": "string" + }, + "HeatmapChartSpec": { + "additionalProperties": false, + "properties": { + "color": { + "$ref": "#/definitions/FieldConfig%3C%22quantitative%22%3E" + }, + "metrics_view": { + "type": "string" + }, + "show_data_labels": { + "type": "boolean" + }, + "x": { + "$ref": "#/definitions/FieldConfig%3C(%22nominal%22%7C%22time%22)%3E" + }, + "y": { + "$ref": "#/definitions/FieldConfig%3C(%22nominal%22%7C%22time%22)%3E" + } + }, + "required": [ + "metrics_view" + ], + "type": "object" + }, + "HoritzontalAlignment": { + "enum": [ + "left", + "center", + "right" + ], + "type": "string" + }, + "ImageSpec": { + "additionalProperties": false, + "properties": { + "alignment": { + "$ref": "#/definitions/ComponentAlignment" + }, + "description": { + "type": "string" + }, + "show_description_as_tooltip": { + "type": "boolean" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + }, + "KPIGridSpec": { + "additionalProperties": false, + "properties": { + "comparison": { + "items": { + "$ref": "#/definitions/ComponentComparisonOptions" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "dimension_filters": { + "type": "string" + }, + "hide_time_range": { + "type": "boolean" + }, + "measures": { + "items": { + "type": "string" + }, + "type": "array" + }, + "metrics_view": { + "type": "string" + }, + "show_description_as_tooltip": { + "type": "boolean" + }, + "sparkline": { + "enum": [ + "none", + "bottom", + "right" + ], + "type": "string" + }, + "time_filters": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "metrics_view", + "measures" + ], + "type": "object" + }, + "MarkdownSpec": { + "additionalProperties": false, + "properties": { + "alignment": { + "$ref": "#/definitions/ComponentAlignment" + }, + "apply_formatting": { + "type": "boolean" + }, + "content": { + "type": "string" + }, + "description": { + "type": "string" + }, + "show_description_as_tooltip": { + "type": "boolean" + }, + "title": { + "type": "string" + } + }, + "required": [ + "content" + ], + "type": "object" + }, + "PivotSpec": { + "additionalProperties": false, + "properties": { + "col_dimensions": { + "items": { + "type": "string" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "dimension_filters": { + "type": "string" + }, + "measures": { + "items": { + "type": "string" + }, + "type": "array" + }, + "metrics_view": { + "type": "string" + }, + "row_dimensions": { + "items": { + "type": "string" + }, + "type": "array" + }, + "show_description_as_tooltip": { + "type": "boolean" + }, + "time_filters": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "metrics_view", + "measures" + ], + "type": "object" + }, + "SequentialMultiHue": { + "enum": [ + "turbo", + "viridis", + "inferno", + "magma", + "plasma", + "cividis", + "bluegreen", + "bluegreen-3", + "bluegreen-4", + "bluegreen-5", + "bluegreen-6", + "bluegreen-7", + "bluegreen-8", + "bluegreen-9", + "bluepurple", + "bluepurple-3", + "bluepurple-4", + "bluepurple-5", + "bluepurple-6", + "bluepurple-7", + "bluepurple-8", + "bluepurple-9", + "goldgreen", + "goldgreen-3", + "goldgreen-4", + "goldgreen-5", + "goldgreen-6", + "goldgreen-7", + "goldgreen-8", + "goldgreen-9", + "goldorange", + "goldorange-3", + "goldorange-4", + "goldorange-5", + "goldorange-6", + "goldorange-7", + "goldorange-8", + "goldorange-9", + "goldred", + "goldred-3", + "goldred-4", + "goldred-5", + "goldred-6", + "goldred-7", + "goldred-8", + "goldred-9", + "greenblue", + "greenblue-3", + "greenblue-4", + "greenblue-5", + "greenblue-6", + "greenblue-7", + "greenblue-8", + "greenblue-9", + "orangered", + "orangered-3", + "orangered-4", + "orangered-5", + "orangered-6", + "orangered-7", + "orangered-8", + "orangered-9", + "purplebluegreen", + "purplebluegreen-3", + "purplebluegreen-4", + "purplebluegreen-5", + "purplebluegreen-6", + "purplebluegreen-7", + "purplebluegreen-8", + "purplebluegreen-9", + "purpleblue", + "purpleblue-3", + "purpleblue-4", + "purpleblue-5", + "purpleblue-6", + "purpleblue-7", + "purpleblue-8", + "purpleblue-9", + "purplered", + "purplered-3", + "purplered-4", + "purplered-5", + "purplered-6", + "purplered-7", + "purplered-8", + "purplered-9", + "redpurple", + "redpurple-3", + "redpurple-4", + "redpurple-5", + "redpurple-6", + "redpurple-7", + "redpurple-8", + "redpurple-9", + "yellowgreenblue", + "yellowgreenblue-3", + "yellowgreenblue-4", + "yellowgreenblue-5", + "yellowgreenblue-6", + "yellowgreenblue-7", + "yellowgreenblue-8", + "yellowgreenblue-9", + "yellowgreen", + "yellowgreen-3", + "yellowgreen-4", + "yellowgreen-5", + "yellowgreen-6", + "yellowgreen-7", + "yellowgreen-8", + "yellowgreen-9", + "yelloworangebrown", + "yelloworangebrown-3", + "yelloworangebrown-4", + "yelloworangebrown-5", + "yelloworangebrown-6", + "yelloworangebrown-7", + "yelloworangebrown-8", + "yelloworangebrown-9", + "yelloworangered", + "yelloworangered-3", + "yelloworangered-4", + "yelloworangered-5", + "yelloworangered-6", + "yelloworangered-7", + "yelloworangered-8", + "yelloworangered-9", + "darkblue", + "darkblue-3", + "darkblue-4", + "darkblue-5", + "darkblue-6", + "darkblue-7", + "darkblue-8", + "darkblue-9", + "darkgold", + "darkgold-3", + "darkgold-4", + "darkgold-5", + "darkgold-6", + "darkgold-7", + "darkgold-8", + "darkgold-9", + "darkgreen", + "darkgreen-3", + "darkgreen-4", + "darkgreen-5", + "darkgreen-6", + "darkgreen-7", + "darkgreen-8", + "darkgreen-9", + "darkmulti", + "darkmulti-3", + "darkmulti-4", + "darkmulti-5", + "darkmulti-6", + "darkmulti-7", + "darkmulti-8", + "darkmulti-9", + "darkred", + "darkred-3", + "darkred-4", + "darkred-5", + "darkred-6", + "darkred-7", + "darkred-8", + "darkred-9", + "lightgreyred", + "lightgreyred-3", + "lightgreyred-4", + "lightgreyred-5", + "lightgreyred-6", + "lightgreyred-7", + "lightgreyred-8", + "lightgreyred-9", + "lightgreyteal", + "lightgreyteal-3", + "lightgreyteal-4", + "lightgreyteal-5", + "lightgreyteal-6", + "lightgreyteal-7", + "lightgreyteal-8", + "lightgreyteal-9", + "lightmulti", + "lightmulti-3", + "lightmulti-4", + "lightmulti-5", + "lightmulti-6", + "lightmulti-7", + "lightmulti-8", + "lightmulti-9", + "lightorange", + "lightorange-3", + "lightorange-4", + "lightorange-5", + "lightorange-6", + "lightorange-7", + "lightorange-8", + "lightorange-9", + "lighttealblue", + "lighttealblue-3", + "lighttealblue-4", + "lighttealblue-5", + "lighttealblue-6", + "lighttealblue-7", + "lighttealblue-8", + "lighttealblue-9" + ], + "type": "string" + }, + "SequentialSingleHue": { + "enum": [ + "blues", + "tealblues", + "teals", + "greens", + "browns", + "greys", + "purples", + "warmgreys", + "reds", + "oranges" + ], + "type": "string" + }, + "TableSpec": { + "additionalProperties": false, + "properties": { + "columns": { + "items": { + "type": "string" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "dimension_filters": { + "type": "string" + }, + "metrics_view": { + "type": "string" + }, + "show_description_as_tooltip": { + "type": "boolean" + }, + "time_filters": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "metrics_view", + "columns" + ], + "type": "object" + }, + "VeriticalAlignment": { + "enum": [ + "top", + "middle", + "bottom" + ], + "type": "string" + } + } +} \ No newline at end of file diff --git a/runtime/ai/develop_canvas.go b/runtime/ai/develop_canvas.go index 261a7cf34c5..1a395cf2753 100644 --- a/runtime/ai/develop_canvas.go +++ b/runtime/ai/develop_canvas.go @@ -3,6 +3,7 @@ package ai import ( "bytes" "context" + _ "embed" "errors" "fmt" "strings" @@ -17,6 +18,9 @@ import ( "gopkg.in/yaml.v3" ) +//go:embed data/component-template-v1.json +var componentSchemaJSON string + const DevelopCanvasName = "develop_canvas" type DevelopCanvas struct { @@ -206,7 +210,8 @@ func (t *DevelopCanvas) systemPrompt(ctx context.Context) (string, error) { // Prepare template data session := GetSession(ctx) data := map[string]any{ - "ai_instructions": session.ProjectInstructions(), + "ai_instructions": session.ProjectInstructions(), + "component_schema": componentSchemaJSON, } // Generate the system prompt @@ -233,22 +238,35 @@ A canvas dashboard consists of: Available component types: - **markdown**: Rich text content for documentation - **kpi_grid**: Grid of key performance indicators -- **leaderboard**: Ranked table of dimension values +- **leaderboard**: Ranked table of dimension values (NOT a chart component) - **bar_chart**: Bar chart for categorical comparisons - **line_chart**: Line chart for time series trends - **area_chart**: Area chart for cumulative trends - **stacked_bar**: Stacked bar chart for multi-measure comparisons - **stacked_bar_normalized**: Normalized stacked bar for proportions - **donut_chart**: Donut chart for part-to-whole relationships +- **funnel_chart**: Funnel chart for conversion analysis - **heatmap**: Heatmap for two-dimensional patterns - **combo_chart**: Combined chart with multiple mark types +- **pivot**: Pivot table for multi-dimensional analysis +- **table**: Table for displaying raw data +- **image**: Image component for displaying images + +**CRITICAL: The following JSON Schema is the AUTHORITATIVE source of truth for all component field names and structures. You MUST strictly follow this schema when generating YAML.** + +{{ .component_schema }} + +**NEVER invent or guess field names** - only use fields defined in the schema above + + At a high level, you should follow these steps: 1. Leverage the "read_file" tool to understand the file's current contents, if any (it may return a file not found error). 2. Generate a new or updated canvas dashboard definition based on the user's prompt and save it to the requested path using the "write_file" tool. -3. The "write_file" tool will respond with the reconcile status. If there are parse or reconcile errors, you should fix them using the "write_file" tool. If there are no errors, your work is done. +3. Before writing, validate that ALL field names match the JSON Schema above exactly. +4. The "write_file" tool will respond with the reconcile status. If there are parse or reconcile errors, you should fix them using the "write_file" tool. If there are no errors, your work is done. Additional instructions: - Canvas dashboards use a 12-unit grid system for layout. Components can have widths from 3 to 12 units. @@ -309,6 +327,37 @@ rows: type: "quantitative" zeroBasedOrigin: true color: "primary" + + - height: "400px" + items: + - width: 6 + donut_chart: + metrics_view: "sales_metrics" + title: "Revenue Distribution by Category" + color: + field: "product_category" + type: "nominal" + limit: 10 + measure: + field: "total_revenue" + type: "quantitative" + showTotal: true + innerRadius: 50 + + - width: 6 + bar_chart: + metrics_view: "sales_metrics" + title: "Top Products by Revenue" + color: "secondary" + x: + field: "product_name" + type: "nominal" + limit: 15 + sort: "-y" + y: + field: "total_revenue" + type: "quantitative" + zeroBasedOrigin: true {{ backticks }} diff --git a/runtime/parser/data/component-template-v1.json b/runtime/parser/data/component-template-v1.json index 2c865bfec44..8ab78631dc3 100644 --- a/runtime/parser/data/component-template-v1.json +++ b/runtime/parser/data/component-template-v1.json @@ -1,74 +1,786 @@ { - "$ref": "#/definitions/TemplateSpec", "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "line_chart": { + "$ref": "#/definitions/CartesianCanvasChartSpec" + } + }, + "required": [ + "line_chart" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "bar_chart": { + "$ref": "#/definitions/CartesianCanvasChartSpec" + } + }, + "required": [ + "bar_chart" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "area_chart": { + "$ref": "#/definitions/CartesianCanvasChartSpec" + } + }, + "required": [ + "area_chart" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "stacked_bar": { + "$ref": "#/definitions/CartesianCanvasChartSpec" + } + }, + "required": [ + "stacked_bar" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "stacked_bar_normalized": { + "$ref": "#/definitions/CartesianCanvasChartSpec" + } + }, + "required": [ + "stacked_bar_normalized" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "donut_chart": { + "$ref": "#/definitions/CircularCanvasChartSpec" + } + }, + "required": [ + "donut_chart" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "funnel_chart": { + "$ref": "#/definitions/FunnelChartSpec" + } + }, + "required": [ + "funnel_chart" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "heatmap": { + "$ref": "#/definitions/HeatmapChartSpec" + } + }, + "required": [ + "heatmap" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "combo_chart": { + "$ref": "#/definitions/ComboChartSpec" + } + }, + "required": [ + "combo_chart" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "pivot": { + "$ref": "#/definitions/PivotSpec" + } + }, + "required": [ + "pivot" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "table": { + "$ref": "#/definitions/TableSpec" + } + }, + "required": [ + "table" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kpi_grid": { + "$ref": "#/definitions/KPIGridSpec" + } + }, + "required": [ + "kpi_grid" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "markdown": { + "$ref": "#/definitions/MarkdownSpec" + } + }, + "required": [ + "markdown" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "image": { + "$ref": "#/definitions/ImageSpec" + } + }, + "required": [ + "image" + ], + "type": "object" + } + ], "definitions": { - "ChartProperties": { + "CartesianCanvasChartSpec": { + "additionalProperties": false, + "properties": { + "color": { + "anyOf": [ + { + "$ref": "#/definitions/FieldConfig%3C%22nominal%22%3E" + }, + { + "type": "string" + } + ] + }, + "description": { + "type": "string" + }, + "dimension_filters": { + "type": "string" + }, + "metrics_view": { + "type": "string" + }, + "show_description_as_tooltip": { + "type": "boolean" + }, + "time_filters": { + "type": "string" + }, + "title": { + "type": "string" + }, + "tooltip": { + "$ref": "#/definitions/FieldConfig" + }, + "vl_config": { + "type": "string" + }, + "x": { + "$ref": "#/definitions/FieldConfig%3C(%22nominal%22%7C%22time%22)%3E" + }, + "y": { + "$ref": "#/definitions/FieldConfig%3C%22quantitative%22%3E" + } + }, + "required": [ + "metrics_view" + ], + "type": "object" + }, + "Categorical": { + "enum": [ + "accent", + "category10", + "category20", + "category20b", + "category20c", + "dark2", + "paired", + "pastel1", + "pastel2", + "set1", + "set2", + "set3", + "tableau10", + "tableau20", + "observable10" + ], + "type": "string" + }, + "ChartLegend": { + "enum": [ + "none", + "top", + "bottom", + "left", + "right" + ], + "type": "string" + }, + "ChartSortDirection": { + "anyOf": [ + { + "enum": [ + "x", + "y", + "-x", + "-y", + "color", + "-color", + "measure", + "-measure" + ], + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "CircularCanvasChartSpec": { "additionalProperties": false, "properties": { - "config": { - "$ref": "#/definitions/ChartConfig" + "color": { + "$ref": "#/definitions/FieldConfig%3C%22nominal%22%3E" + }, + "description": { + "type": "string" + }, + "dimension_filters": { + "type": "string" + }, + "innerRadius": { + "type": "number" + }, + "measure": { + "$ref": "#/definitions/FieldConfig%3C%22quantitative%22%3E" + }, + "metrics_view": { + "type": "string" + }, + "show_description_as_tooltip": { + "type": "boolean" + }, + "time_filters": { + "type": "string" }, "title": { "type": "string" }, - "description": { + "tooltip": { + "$ref": "#/definitions/FieldConfig" + }, + "vl_config": { + "type": "string" + } + }, + "required": [ + "metrics_view" + ], + "type": "object" + }, + "ColorMapping": { + "items": { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "value", + "color" + ], + "type": "object" + }, + "type": "array" + }, + "ColorRangeMapping": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "mode": { + "const": "scheme", + "type": "string" + }, + "scheme": { + "anyOf": [ + { + "$ref": "#/definitions/ColorScheme" + }, + { + "const": "sequential", + "type": "string" + }, + { + "const": "diverging", + "type": "string" + } + ] + } + }, + "required": [ + "mode", + "scheme" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "end": { + "type": "string" + }, + "mode": { + "const": "gradient", + "type": "string" + }, + "start": { + "type": "string" + } + }, + "required": [ + "mode", + "start", + "end" + ], + "type": "object" + } + ] + }, + "ColorScheme": { + "anyOf": [ + { + "$ref": "#/definitions/Categorical" + }, + { + "$ref": "#/definitions/SequentialSingleHue" + }, + { + "$ref": "#/definitions/SequentialMultiHue" + }, + { + "$ref": "#/definitions/Diverging" + }, + { + "$ref": "#/definitions/Cyclical" + } + ] + }, + "ComboChartSpec": { + "additionalProperties": false, + "properties": { + "color": { + "$ref": "#/definitions/FieldConfig%3C%22nominal%22%3E" + }, + "metrics_view": { + "type": "string" + }, + "x": { + "$ref": "#/definitions/FieldConfig%3C(%22nominal%22%7C%22time%22)%3E" + }, + "y1": { + "$ref": "#/definitions/FieldConfig%3C(%22quantitative%22%7C%22mark%22)%3E" + }, + "y2": { + "$ref": "#/definitions/FieldConfig%3C(%22quantitative%22%7C%22mark%22)%3E" + } + }, + "required": [ + "metrics_view" + ], + "type": "object" + }, + "ComponentAlignment": { + "additionalProperties": false, + "properties": { + "horizontal": { + "$ref": "#/definitions/HoritzontalAlignment" + }, + "vertical": { + "$ref": "#/definitions/VeriticalAlignment" + } + }, + "required": [ + "vertical", + "horizontal" + ], + "type": "object" + }, + "ComponentComparisonOptions": { + "enum": [ + "previous", + "delta", + "percent_change" + ], + "type": "string" + }, + "Cyclical": { + "enum": [ + "rainbow", + "sinebow" + ], + "type": "string" + }, + "Diverging": { + "enum": [ + "blueorange", + "blueorange-3", + "blueorange-4", + "blueorange-5", + "blueorange-6", + "blueorange-7", + "blueorange-8", + "blueorange-9", + "blueorange-10", + "blueorange-11", + "brownbluegreen", + "brownbluegreen-3", + "brownbluegreen-4", + "brownbluegreen-5", + "brownbluegreen-6", + "brownbluegreen-7", + "brownbluegreen-8", + "brownbluegreen-9", + "brownbluegreen-10", + "brownbluegreen-11", + "purplegreen", + "purplegreen-3", + "purplegreen-4", + "purplegreen-5", + "purplegreen-6", + "purplegreen-7", + "purplegreen-8", + "purplegreen-9", + "purplegreen-10", + "purplegreen-11", + "pinkyellowgreen", + "pinkyellowgreen-3", + "pinkyellowgreen-4", + "pinkyellowgreen-5", + "pinkyellowgreen-6", + "pinkyellowgreen-7", + "pinkyellowgreen-8", + "pinkyellowgreen-9", + "pinkyellowgreen-10", + "pinkyellowgreen-11", + "purpleorange", + "purpleorange-3", + "purpleorange-4", + "purpleorange-5", + "purpleorange-6", + "purpleorange-7", + "purpleorange-8", + "purpleorange-9", + "purpleorange-10", + "purpleorange-11", + "redblue", + "redblue-3", + "redblue-4", + "redblue-5", + "redblue-6", + "redblue-7", + "redblue-8", + "redblue-9", + "redblue-10", + "redblue-11", + "redgrey", + "redgrey-3", + "redgrey-4", + "redgrey-5", + "redgrey-6", + "redgrey-7", + "redgrey-8", + "redgrey-9", + "redgrey-10", + "redgrey-11", + "redyellowblue", + "redyellowblue-3", + "redyellowblue-4", + "redyellowblue-5", + "redyellowblue-6", + "redyellowblue-7", + "redyellowblue-8", + "redyellowblue-9", + "redyellowblue-10", + "redyellowblue-11", + "redyellowgreen", + "redyellowgreen-3", + "redyellowgreen-4", + "redyellowgreen-5", + "redyellowgreen-6", + "redyellowgreen-7", + "redyellowgreen-8", + "redyellowgreen-9", + "redyellowgreen-10", + "redyellowgreen-11", + "spectral", + "spectral-3", + "spectral-4", + "spectral-5", + "spectral-6", + "spectral-7", + "spectral-8", + "spectral-9", + "spectral-10", + "spectral-11" + ], + "type": "string" + }, + "FieldConfig": { + "additionalProperties": false, + "properties": { + "colorMapping": { + "$ref": "#/definitions/ColorMapping" + }, + "colorRange": { + "$ref": "#/definitions/ColorRangeMapping" + }, + "field": { + "type": "string" + }, + "fields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "labelAngle": { + "type": "number" + }, + "legendOrientation": { + "$ref": "#/definitions/ChartLegend" + }, + "limit": { + "type": "number" + }, + "mark": { + "enum": [ + "bar", + "line" + ], + "type": "string" + }, + "max": { + "type": "number" + }, + "min": { + "type": "number" + }, + "showAxisTitle": { + "type": "boolean" + }, + "showNull": { + "type": "boolean" + }, + "showTotal": { + "type": "boolean" + }, + "sort": { + "$ref": "#/definitions/ChartSortDirection" + }, + "timeUnit": { + "type": "string" + }, + "type": { + "enum": [ + "quantitative", + "ordinal", + "nominal", + "temporal", + "value" + ], + "type": "string" + }, + "zeroBasedOrigin": { + "type": "boolean" + } + }, + "required": [ + "field", + "type" + ], + "type": "object" + }, + "FieldConfig<\"nominal\">": { + "properties": { + "colorMapping": { + "$ref": "#/definitions/ColorMapping" + }, + "field": { + "type": "string" + }, + "fields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "labelAngle": { + "type": "number" + }, + "legendOrientation": { + "$ref": "#/definitions/ChartLegend" + }, + "limit": { + "type": "number" + }, + "showAxisTitle": { + "type": "boolean" + }, + "showNull": { + "type": "boolean" + }, + "sort": { + "$ref": "#/definitions/ChartSortDirection" + }, + "type": { + "enum": [ + "quantitative", + "ordinal", + "nominal", + "temporal", + "value" + ], "type": "string" } }, "required": [ - "config" + "field", + "type" ], "type": "object" }, - "ChartConfig": { - "additionalProperties": false, + "FieldConfig<\"quantitative\">": { "properties": { - "metrics_view": { + "colorRange": { + "$ref": "#/definitions/ColorRangeMapping" + }, + "field": { "type": "string" }, - "x": { - "$ref": "#/definitions/FieldConfig" + "fields": { + "items": { + "type": "string" + }, + "type": "array" }, - "y": { - "$ref": "#/definitions/FieldConfig" + "max": { + "type": "number" }, - "color": { - "oneOf": [ - { - "$ref": "#/definitions/FieldConfig" - }, - { - "type": "string" - } - ] + "min": { + "type": "number" }, - "tooltip": { - "$ref": "#/definitions/FieldConfig" + "showAxisTitle": { + "type": "boolean" + }, + "showTotal": { + "type": "boolean" + }, + "type": { + "enum": [ + "quantitative", + "ordinal", + "nominal", + "temporal", + "value" + ], + "type": "string" + }, + "zeroBasedOrigin": { + "type": "boolean" } }, "required": [ - "metrics_view" + "field", + "type" ], "type": "object" }, - "FieldConfig": { - "additionalProperties": false, + "FieldConfig<(\"nominal\"|\"time\")>": { "properties": { + "colorMapping": { + "$ref": "#/definitions/ColorMapping" + }, "field": { "type": "string" }, - "title": { - "type": "string" + "fields": { + "items": { + "type": "string" + }, + "type": "array" }, - "format": { - "type": "string" + "labelAngle": { + "type": "number" }, - "type": { - "type": "string", - "enum": ["quantitative", "ordinal", "nominal", "temporal", "geojson"] + "legendOrientation": { + "$ref": "#/definitions/ChartLegend" + }, + "limit": { + "type": "number" + }, + "showAxisTitle": { + "type": "boolean" + }, + "showNull": { + "type": "boolean" + }, + "sort": { + "$ref": "#/definitions/ChartSortDirection" }, "timeUnit": { "type": "string" + }, + "type": { + "enum": [ + "quantitative", + "ordinal", + "nominal", + "temporal", + "value" + ], + "type": "string" } }, "required": [ @@ -77,184 +789,244 @@ ], "type": "object" }, - "ImageProperties": { - "additionalProperties": false, + "FieldConfig<(\"quantitative\"|\"mark\")>": { "properties": { - "css": { - "type": "object" + "colorRange": { + "$ref": "#/definitions/ColorRangeMapping" }, - "url": { + "field": { "type": "string" }, - "title": { + "fields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "mark": { + "enum": [ + "bar", + "line" + ], "type": "string" }, - "description": { + "max": { + "type": "number" + }, + "min": { + "type": "number" + }, + "showAxisTitle": { + "type": "boolean" + }, + "showTotal": { + "type": "boolean" + }, + "type": { + "enum": [ + "quantitative", + "ordinal", + "nominal", + "temporal", + "value" + ], "type": "string" + }, + "zeroBasedOrigin": { + "type": "boolean" } }, "required": [ - "url" + "field", + "type" ], "type": "object" }, - "ImageTemplateT": { - "additionalProperties": false, - "properties": { - "image": { - "$ref": "#/definitions/ImageProperties" - } - }, - "required": [ - "image" + "FunnelBreakdownMode": { + "enum": [ + "dimension", + "measures" ], - "type": "object" + "type": "string" }, - "KPIProperties": { + "FunnelChartSpec": { "additionalProperties": false, "properties": { - "comparison_range": { - "type": "string" + "breakdownMode": { + "$ref": "#/definitions/FunnelBreakdownMode" }, - "filter": { - "type": "string" + "color": { + "$ref": "#/definitions/FunnelColorMode" }, "measure": { - "type": "string" + "$ref": "#/definitions/FieldConfig%3C%22quantitative%22%3E" }, "metrics_view": { "type": "string" }, - "time_range": { - "type": "string" - }, - "title": { - "type": "string" + "mode": { + "$ref": "#/definitions/FunnelMode" }, - "description": { - "type": "string" + "stage": { + "$ref": "#/definitions/FieldConfig%3C%22nominal%22%3E" } }, "required": [ - "measure", - "metrics_view", - "time_range" + "metrics_view" ], "type": "object" }, - "KPITemplateT": { + "FunnelColorMode": { + "enum": [ + "stage", + "measure", + "name", + "value" + ], + "type": "string" + }, + "FunnelMode": { + "enum": [ + "width", + "order" + ], + "type": "string" + }, + "HeatmapChartSpec": { "additionalProperties": false, "properties": { - "kpi": { - "$ref": "#/definitions/KPIProperties" + "color": { + "$ref": "#/definitions/FieldConfig%3C%22quantitative%22%3E" + }, + "metrics_view": { + "type": "string" + }, + "show_data_labels": { + "type": "boolean" + }, + "x": { + "$ref": "#/definitions/FieldConfig%3C(%22nominal%22%7C%22time%22)%3E" + }, + "y": { + "$ref": "#/definitions/FieldConfig%3C(%22nominal%22%7C%22time%22)%3E" } }, "required": [ - "kpi" + "metrics_view" ], "type": "object" }, - "MarkdownProperties": { + "HoritzontalAlignment": { + "enum": [ + "left", + "center", + "right" + ], + "type": "string" + }, + "ImageSpec": { "additionalProperties": false, "properties": { - "content": { + "alignment": { + "$ref": "#/definitions/ComponentAlignment" + }, + "description": { "type": "string" }, - "css": { - "type": "object" + "show_description_as_tooltip": { + "type": "boolean" }, "title": { "type": "string" }, - "description": { + "url": { "type": "string" } }, "required": [ - "content" - ], - "type": "object" - }, - "MarkdownTemplateT": { - "additionalProperties": false, - "properties": { - "markdown": { - "$ref": "#/definitions/MarkdownProperties" - } - }, - "required": [ - "markdown" + "url" ], "type": "object" }, - "SelectProperties": { + "KPIGridSpec": { "additionalProperties": false, "properties": { - "label": { + "comparison": { + "items": { + "$ref": "#/definitions/ComponentComparisonOptions" + }, + "type": "array" + }, + "description": { "type": "string" }, - "labelField": { + "dimension_filters": { "type": "string" }, - "placeholder": { + "hide_time_range": { + "type": "boolean" + }, + "measures": { + "items": { + "type": "string" + }, + "type": "array" + }, + "metrics_view": { "type": "string" }, - "tooltip": { + "show_description_as_tooltip": { + "type": "boolean" + }, + "sparkline": { + "enum": [ + "none", + "bottom", + "right" + ], "type": "string" }, - "valueField": { + "time_filters": { + "type": "string" + }, + "title": { "type": "string" } }, "required": [ - "valueField" - ], - "type": "object" - }, - "SelectPropertiesT": { - "additionalProperties": false, - "properties": { - "select": { - "$ref": "#/definitions/SelectProperties" - } - }, - "required": [ - "select" + "metrics_view", + "measures" ], "type": "object" }, - "SwitchProperties": { + "MarkdownSpec": { "additionalProperties": false, "properties": { - "label": { + "alignment": { + "$ref": "#/definitions/ComponentAlignment" + }, + "apply_formatting": { + "type": "boolean" + }, + "content": { "type": "string" }, - "tooltip": { + "description": { "type": "string" }, - "value": { + "show_description_as_tooltip": { + "type": "boolean" + }, + "title": { "type": "string" } }, "required": [ - "label", - "value" - ], - "type": "object" - }, - "SwitchPropertiesT": { - "additionalProperties": false, - "properties": { - "switch": { - "$ref": "#/definitions/SwitchProperties" - } - }, - "required": [ - "switch" + "content" ], "type": "object" }, - "TableProperties": { + "PivotSpec": { "additionalProperties": false, "properties": { "col_dimensions": { @@ -263,10 +1035,10 @@ }, "type": "array" }, - "comparison_range": { + "description": { "type": "string" }, - "filter": { + "dimension_filters": { "type": "string" }, "measures": { @@ -284,106 +1056,289 @@ }, "type": "array" }, - "time_range": { - "type": "string" + "show_description_as_tooltip": { + "type": "boolean" }, - "title": { + "time_filters": { "type": "string" }, - "description": { + "title": { "type": "string" } }, "required": [ - "measures", "metrics_view", - "time_range" + "measures" ], "type": "object" }, - "TableTemplateT": { - "additionalProperties": false, - "properties": { - "table": { - "$ref": "#/definitions/TableProperties" - } - }, - "required": [ - "table" + "SequentialMultiHue": { + "enum": [ + "turbo", + "viridis", + "inferno", + "magma", + "plasma", + "cividis", + "bluegreen", + "bluegreen-3", + "bluegreen-4", + "bluegreen-5", + "bluegreen-6", + "bluegreen-7", + "bluegreen-8", + "bluegreen-9", + "bluepurple", + "bluepurple-3", + "bluepurple-4", + "bluepurple-5", + "bluepurple-6", + "bluepurple-7", + "bluepurple-8", + "bluepurple-9", + "goldgreen", + "goldgreen-3", + "goldgreen-4", + "goldgreen-5", + "goldgreen-6", + "goldgreen-7", + "goldgreen-8", + "goldgreen-9", + "goldorange", + "goldorange-3", + "goldorange-4", + "goldorange-5", + "goldorange-6", + "goldorange-7", + "goldorange-8", + "goldorange-9", + "goldred", + "goldred-3", + "goldred-4", + "goldred-5", + "goldred-6", + "goldred-7", + "goldred-8", + "goldred-9", + "greenblue", + "greenblue-3", + "greenblue-4", + "greenblue-5", + "greenblue-6", + "greenblue-7", + "greenblue-8", + "greenblue-9", + "orangered", + "orangered-3", + "orangered-4", + "orangered-5", + "orangered-6", + "orangered-7", + "orangered-8", + "orangered-9", + "purplebluegreen", + "purplebluegreen-3", + "purplebluegreen-4", + "purplebluegreen-5", + "purplebluegreen-6", + "purplebluegreen-7", + "purplebluegreen-8", + "purplebluegreen-9", + "purpleblue", + "purpleblue-3", + "purpleblue-4", + "purpleblue-5", + "purpleblue-6", + "purpleblue-7", + "purpleblue-8", + "purpleblue-9", + "purplered", + "purplered-3", + "purplered-4", + "purplered-5", + "purplered-6", + "purplered-7", + "purplered-8", + "purplered-9", + "redpurple", + "redpurple-3", + "redpurple-4", + "redpurple-5", + "redpurple-6", + "redpurple-7", + "redpurple-8", + "redpurple-9", + "yellowgreenblue", + "yellowgreenblue-3", + "yellowgreenblue-4", + "yellowgreenblue-5", + "yellowgreenblue-6", + "yellowgreenblue-7", + "yellowgreenblue-8", + "yellowgreenblue-9", + "yellowgreen", + "yellowgreen-3", + "yellowgreen-4", + "yellowgreen-5", + "yellowgreen-6", + "yellowgreen-7", + "yellowgreen-8", + "yellowgreen-9", + "yelloworangebrown", + "yelloworangebrown-3", + "yelloworangebrown-4", + "yelloworangebrown-5", + "yelloworangebrown-6", + "yelloworangebrown-7", + "yelloworangebrown-8", + "yelloworangebrown-9", + "yelloworangered", + "yelloworangered-3", + "yelloworangered-4", + "yelloworangered-5", + "yelloworangered-6", + "yelloworangered-7", + "yelloworangered-8", + "yelloworangered-9", + "darkblue", + "darkblue-3", + "darkblue-4", + "darkblue-5", + "darkblue-6", + "darkblue-7", + "darkblue-8", + "darkblue-9", + "darkgold", + "darkgold-3", + "darkgold-4", + "darkgold-5", + "darkgold-6", + "darkgold-7", + "darkgold-8", + "darkgold-9", + "darkgreen", + "darkgreen-3", + "darkgreen-4", + "darkgreen-5", + "darkgreen-6", + "darkgreen-7", + "darkgreen-8", + "darkgreen-9", + "darkmulti", + "darkmulti-3", + "darkmulti-4", + "darkmulti-5", + "darkmulti-6", + "darkmulti-7", + "darkmulti-8", + "darkmulti-9", + "darkred", + "darkred-3", + "darkred-4", + "darkred-5", + "darkred-6", + "darkred-7", + "darkred-8", + "darkred-9", + "lightgreyred", + "lightgreyred-3", + "lightgreyred-4", + "lightgreyred-5", + "lightgreyred-6", + "lightgreyred-7", + "lightgreyred-8", + "lightgreyred-9", + "lightgreyteal", + "lightgreyteal-3", + "lightgreyteal-4", + "lightgreyteal-5", + "lightgreyteal-6", + "lightgreyteal-7", + "lightgreyteal-8", + "lightgreyteal-9", + "lightmulti", + "lightmulti-3", + "lightmulti-4", + "lightmulti-5", + "lightmulti-6", + "lightmulti-7", + "lightmulti-8", + "lightmulti-9", + "lightorange", + "lightorange-3", + "lightorange-4", + "lightorange-5", + "lightorange-6", + "lightorange-7", + "lightorange-8", + "lightorange-9", + "lighttealblue", + "lighttealblue-3", + "lighttealblue-4", + "lighttealblue-5", + "lighttealblue-6", + "lighttealblue-7", + "lighttealblue-8", + "lighttealblue-9" ], - "type": "object" + "type": "string" }, - "TemplateSpec": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "line_chart": { - "$ref": "#/definitions/ChartProperties" - } - }, - "required": [ - "line_chart" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "bar_chart": { - "$ref": "#/definitions/ChartProperties" - } - }, - "required": [ - "bar_chart" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "vega_lite": { - "spec": { - "type": "string" - } - } - }, - "required": [ - "vega_lite" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "stacked_bar_chart": { - "$ref": "#/definitions/ChartProperties" - } + "SequentialSingleHue": { + "enum": [ + "blues", + "tealblues", + "teals", + "greens", + "browns", + "greys", + "purples", + "warmgreys", + "reds", + "oranges" + ], + "type": "string" + }, + "TableSpec": { + "additionalProperties": false, + "properties": { + "columns": { + "items": { + "type": "string" }, - "required": [ - "stacked_bar_chart" - ], - "type": "object" + "type": "array" }, - { - "$ref": "#/definitions/KPITemplateT" + "description": { + "type": "string" }, - { - "$ref": "#/definitions/TableTemplateT" + "dimension_filters": { + "type": "string" }, - { - "$ref": "#/definitions/MarkdownTemplateT" + "metrics_view": { + "type": "string" }, - { - "$ref": "#/definitions/ImageTemplateT" + "show_description_as_tooltip": { + "type": "boolean" }, - { - "$ref": "#/definitions/SelectPropertiesT" + "time_filters": { + "type": "string" }, - { - "$ref": "#/definitions/SwitchPropertiesT" + "title": { + "type": "string" } - ] + }, + "required": [ + "metrics_view", + "columns" + ], + "type": "object" + }, + "VeriticalAlignment": { + "enum": [ + "top", + "middle", + "bottom" + ], + "type": "string" } } } \ No newline at end of file diff --git a/web-common/package.json b/web-common/package.json index 9acd75bfccb..41685ff22b1 100644 --- a/web-common/package.json +++ b/web-common/package.json @@ -7,7 +7,7 @@ "format": "prettier --write .", "generate:runtime-client": "svelte-kit sync && orval --config ./orval.config.ts", "generate:sveltekit": "svelte-kit sync", - "generate:template-schema": "ts-json-schema-generator --path 'src/features/canvas/components/types.ts' --type TemplateSpec -o ../runtime/parser/data/component-template-v1.json", + "generate:template-schema": "ts-json-schema-generator --path 'src/features/canvas/components/types.ts' --type ComponentWithTypeSpec --no-type-check -o ../runtime/ai/data/component-template-v1.json --no-top-ref", "test": "vitest run", "coverage": "vitest run --coverage", "test:watch": "vitest", diff --git a/web-common/src/features/canvas/components/types.ts b/web-common/src/features/canvas/components/types.ts index eb908cd5383..270474a6d3d 100644 --- a/web-common/src/features/canvas/components/types.ts +++ b/web-common/src/features/canvas/components/types.ts @@ -1,6 +1,11 @@ import type { CartesianCanvasChartSpec } from "@rilldata/web-common/features/canvas/components/charts/variants/CartesianChart"; import type { CircularCanvasChartSpec } from "@rilldata/web-common/features/canvas/components/charts/variants/CircularChart"; import type { KPIGridSpec } from "@rilldata/web-common/features/canvas/components/kpi-grid"; +import type { + ComboChartSpec, + FunnelChartSpec, + HeatmapChartSpec, +} from "@rilldata/web-common/features/components/charts"; import type { ChartType } from "../../components/charts/types"; import type { ImageSpec } from "./image"; import type { KPISpec } from "./kpi"; @@ -56,40 +61,18 @@ export type CanvasComponentType = | "table" | "leaderboard"; -interface LineChart { - line_chart: CartesianCanvasChartSpec; -} - -interface AreaChart { - area_chart: CartesianCanvasChartSpec; -} - -interface BarChart { - bar_chart: CartesianCanvasChartSpec; -} - -export type ChartTemplates = LineChart | BarChart | AreaChart; -export interface KPITemplateT { - kpi: KPISpec; -} -export interface MarkdownTemplateT { - markdown: MarkdownSpec; -} -export interface ImageTemplateT { - image: ImageSpec; -} - -export interface PivotTemplateT { - pivot: PivotSpec; -} -export interface TableTemplateT { - table: TableSpec; -} - -export type TemplateSpec = - | ChartTemplates - | KPITemplateT - | PivotTemplateT - | MarkdownTemplateT - | ImageTemplateT - | TableTemplateT; +export type ComponentWithTypeSpec = + | { line_chart: CartesianCanvasChartSpec } + | { bar_chart: CartesianCanvasChartSpec } + | { area_chart: CartesianCanvasChartSpec } + | { stacked_bar: CartesianCanvasChartSpec } + | { stacked_bar_normalized: CartesianCanvasChartSpec } + | { donut_chart: CircularCanvasChartSpec } + | { funnel_chart: FunnelChartSpec } + | { heatmap: HeatmapChartSpec } + | { combo_chart: ComboChartSpec } + | { pivot: PivotSpec } + | { table: TableSpec } + | { kpi_grid: KPIGridSpec } + | { markdown: MarkdownSpec } + | { image: ImageSpec }; diff --git a/web-common/src/features/components/charts/types.ts b/web-common/src/features/components/charts/types.ts index cb5f2a0fcdb..9c33a5b77c6 100644 --- a/web-common/src/features/components/charts/types.ts +++ b/web-common/src/features/components/charts/types.ts @@ -46,27 +46,7 @@ export type ChartSpec = ChartSpecBase & { vl_config?: string; }; -interface TimeRange { - time_range: { - start: string; - end: string; - }; -} - -export type ChartSpecAI = - | { chart_type: "bar_chart"; spec: CartesianChartSpec & TimeRange } - | { chart_type: "line_chart"; spec: CartesianChartSpec & TimeRange } - | { chart_type: "area_chart"; spec: CartesianChartSpec & TimeRange } - | { chart_type: "stacked_bar"; spec: CartesianChartSpec & TimeRange } - | { - chart_type: "stacked_bar_normalized"; - spec: CartesianChartSpec & TimeRange; - } - | { chart_type: "donut_chart"; spec: CircularChartSpec & TimeRange } - | { chart_type: "pie_chart"; spec: CircularChartSpec & TimeRange } - | { chart_type: "funnel_chart"; spec: FunnelChartSpec & TimeRange } - | { chart_type: "heatmap"; spec: HeatmapChartSpec & TimeRange } - | { chart_type: "combo_chart"; spec: ComboChartSpec & TimeRange }; +// export type ComponentChartSpec = export type ChartType = | "bar_chart"