feat: Go backend, enhanced search, new widgets, Docker deploy
Major changes: - Add Go backend (backend/) with microservices architecture - Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler, proxy-manager, media-search, fastClassifier, language detection - New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard, UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions - Improved discover-svc with discover-db integration - Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md) - Library-svc: project_id schema migration - Remove deprecated finance-svc and travel-svc - Localization improvements across services Made-with: Cursor
This commit is contained in:
759
backend/internal/labs/generator.go
Normal file
759
backend/internal/labs/generator.go
Normal file
@@ -0,0 +1,759 @@
|
||||
package labs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gooseek/backend/internal/llm"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Generator struct {
|
||||
llm llm.Client
|
||||
}
|
||||
|
||||
func NewGenerator(llmClient llm.Client) *Generator {
|
||||
return &Generator{llm: llmClient}
|
||||
}
|
||||
|
||||
type GenerateOptions struct {
|
||||
Query string
|
||||
Data interface{}
|
||||
PreferredTypes []VisualizationType
|
||||
Theme string
|
||||
Locale string
|
||||
MaxVisualizations int
|
||||
}
|
||||
|
||||
func (g *Generator) GenerateReport(ctx context.Context, opts GenerateOptions) (*Report, error) {
|
||||
analysisPrompt := fmt.Sprintf(`Analyze this data and query to determine the best visualizations.
|
||||
|
||||
Query: %s
|
||||
|
||||
Data: %v
|
||||
|
||||
Determine:
|
||||
1. What visualizations would best represent this data?
|
||||
2. How should the data be structured for each visualization?
|
||||
3. What insights can be highlighted?
|
||||
|
||||
Respond in JSON format:
|
||||
{
|
||||
"title": "Report title",
|
||||
"sections": [
|
||||
{
|
||||
"title": "Section title",
|
||||
"visualizations": [
|
||||
{
|
||||
"type": "chart_type",
|
||||
"title": "Viz title",
|
||||
"dataMapping": { "how to map the data" },
|
||||
"insight": "Key insight"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Available visualization types: bar_chart, line_chart, pie_chart, donut_chart, table, stat_cards, kpi, comparison, timeline, progress, heatmap, code_block, markdown, collapsible, tabs, accordion`, opts.Query, opts.Data)
|
||||
|
||||
result, err := g.llm.GenerateText(ctx, llm.StreamRequest{
|
||||
Messages: []llm.Message{{Role: "user", Content: analysisPrompt}},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var analysis struct {
|
||||
Title string `json:"title"`
|
||||
Sections []struct {
|
||||
Title string `json:"title"`
|
||||
Visualizations []struct {
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
DataMapping map[string]interface{} `json:"dataMapping"`
|
||||
Insight string `json:"insight"`
|
||||
} `json:"visualizations"`
|
||||
} `json:"sections"`
|
||||
}
|
||||
|
||||
jsonStr := extractJSON(result)
|
||||
if err := json.Unmarshal([]byte(jsonStr), &analysis); err != nil {
|
||||
return g.createDefaultReport(opts)
|
||||
}
|
||||
|
||||
report := &Report{
|
||||
ID: uuid.New().String(),
|
||||
Title: analysis.Title,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Theme: opts.Theme,
|
||||
Sections: make([]ReportSection, 0),
|
||||
}
|
||||
|
||||
for _, sec := range analysis.Sections {
|
||||
section := ReportSection{
|
||||
ID: uuid.New().String(),
|
||||
Title: sec.Title,
|
||||
Visualizations: make([]Visualization, 0),
|
||||
}
|
||||
|
||||
for _, viz := range sec.Visualizations {
|
||||
visualization := g.createVisualization(VisualizationType(viz.Type), viz.Title, opts.Data, viz.DataMapping)
|
||||
if visualization != nil {
|
||||
section.Visualizations = append(section.Visualizations, *visualization)
|
||||
}
|
||||
}
|
||||
|
||||
if len(section.Visualizations) > 0 {
|
||||
report.Sections = append(report.Sections, section)
|
||||
}
|
||||
}
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func (g *Generator) createDefaultReport(opts GenerateOptions) (*Report, error) {
|
||||
report := &Report{
|
||||
ID: uuid.New().String(),
|
||||
Title: "Анализ данных",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Sections: []ReportSection{
|
||||
{
|
||||
ID: uuid.New().String(),
|
||||
Title: "Обзор",
|
||||
Visualizations: []Visualization{
|
||||
g.CreateMarkdown("", formatDataAsMarkdown(opts.Data)),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func (g *Generator) createVisualization(vizType VisualizationType, title string, data interface{}, mapping map[string]interface{}) *Visualization {
|
||||
switch vizType {
|
||||
case VizBarChart, VizLineChart, VizAreaChart:
|
||||
return g.createChartVisualization(vizType, title, data, mapping)
|
||||
case VizPieChart, VizDonutChart:
|
||||
return g.createPieVisualization(vizType, title, data, mapping)
|
||||
case VizTable:
|
||||
return g.createTableVisualization(title, data, mapping)
|
||||
case VizStatCards:
|
||||
return g.createStatCardsVisualization(title, data, mapping)
|
||||
case VizKPI:
|
||||
return g.createKPIVisualization(title, data, mapping)
|
||||
case VizTimeline:
|
||||
return g.createTimelineVisualization(title, data, mapping)
|
||||
case VizComparison:
|
||||
return g.createComparisonVisualization(title, data, mapping)
|
||||
case VizProgress:
|
||||
return g.createProgressVisualization(title, data, mapping)
|
||||
case VizMarkdown:
|
||||
content := extractStringFromData(data, mapping, "content")
|
||||
viz := g.CreateMarkdown(title, content)
|
||||
return &viz
|
||||
default:
|
||||
viz := g.CreateMarkdown(title, formatDataAsMarkdown(data))
|
||||
return &viz
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) createChartVisualization(vizType VisualizationType, title string, data interface{}, mapping map[string]interface{}) *Visualization {
|
||||
chartData := &ChartData{
|
||||
Labels: make([]string, 0),
|
||||
Datasets: make([]ChartDataset, 0),
|
||||
}
|
||||
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
labels := make([]string, 0)
|
||||
values := make([]float64, 0)
|
||||
|
||||
for k, v := range dataMap {
|
||||
labels = append(labels, k)
|
||||
values = append(values, toFloat64(v))
|
||||
}
|
||||
|
||||
chartData.Labels = labels
|
||||
chartData.Datasets = append(chartData.Datasets, ChartDataset{
|
||||
Label: title,
|
||||
Data: values,
|
||||
})
|
||||
}
|
||||
|
||||
if dataSlice, ok := data.([]interface{}); ok {
|
||||
for _, item := range dataSlice {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
if label, ok := itemMap["label"].(string); ok {
|
||||
chartData.Labels = append(chartData.Labels, label)
|
||||
}
|
||||
if value, ok := itemMap["value"]; ok {
|
||||
if len(chartData.Datasets) == 0 {
|
||||
chartData.Datasets = append(chartData.Datasets, ChartDataset{Label: title, Data: []float64{}})
|
||||
}
|
||||
chartData.Datasets[0].Data = append(chartData.Datasets[0].Data, toFloat64(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: vizType,
|
||||
Title: title,
|
||||
Data: chartData,
|
||||
Config: VisualizationConfig{
|
||||
ShowLegend: true,
|
||||
ShowTooltip: true,
|
||||
ShowGrid: true,
|
||||
Animated: true,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) createPieVisualization(vizType VisualizationType, title string, data interface{}, mapping map[string]interface{}) *Visualization {
|
||||
chartData := &ChartData{
|
||||
Labels: make([]string, 0),
|
||||
Datasets: make([]ChartDataset, 0),
|
||||
}
|
||||
|
||||
dataset := ChartDataset{Label: title, Data: []float64{}}
|
||||
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
for k, v := range dataMap {
|
||||
chartData.Labels = append(chartData.Labels, k)
|
||||
dataset.Data = append(dataset.Data, toFloat64(v))
|
||||
}
|
||||
}
|
||||
|
||||
chartData.Datasets = append(chartData.Datasets, dataset)
|
||||
|
||||
return &Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: vizType,
|
||||
Title: title,
|
||||
Data: chartData,
|
||||
Config: VisualizationConfig{
|
||||
ShowLegend: true,
|
||||
ShowTooltip: true,
|
||||
ShowValues: true,
|
||||
Animated: true,
|
||||
},
|
||||
Style: VisualizationStyle{
|
||||
Height: "300px",
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) createTableVisualization(title string, data interface{}, mapping map[string]interface{}) *Visualization {
|
||||
tableData := &TableData{
|
||||
Columns: make([]TableColumn, 0),
|
||||
Rows: make([]TableRow, 0),
|
||||
}
|
||||
|
||||
if dataSlice, ok := data.([]interface{}); ok && len(dataSlice) > 0 {
|
||||
if firstRow, ok := dataSlice[0].(map[string]interface{}); ok {
|
||||
for key := range firstRow {
|
||||
tableData.Columns = append(tableData.Columns, TableColumn{
|
||||
Key: key,
|
||||
Label: formatColumnLabel(key),
|
||||
Sortable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range dataSlice {
|
||||
if rowMap, ok := item.(map[string]interface{}); ok {
|
||||
tableData.Rows = append(tableData.Rows, TableRow(rowMap))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
tableData.Columns = []TableColumn{
|
||||
{Key: "key", Label: "Параметр", Sortable: true},
|
||||
{Key: "value", Label: "Значение", Sortable: true},
|
||||
}
|
||||
|
||||
for k, v := range dataMap {
|
||||
tableData.Rows = append(tableData.Rows, TableRow{
|
||||
"key": k,
|
||||
"value": v,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
tableData.Summary = &TableSummary{
|
||||
TotalRows: len(tableData.Rows),
|
||||
}
|
||||
|
||||
return &Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizTable,
|
||||
Title: title,
|
||||
Data: tableData,
|
||||
Config: VisualizationConfig{
|
||||
Sortable: true,
|
||||
Searchable: true,
|
||||
Paginated: len(tableData.Rows) > 10,
|
||||
PageSize: 10,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) createStatCardsVisualization(title string, data interface{}, mapping map[string]interface{}) *Visualization {
|
||||
cardsData := &StatCardsData{
|
||||
Cards: make([]StatCard, 0),
|
||||
}
|
||||
|
||||
colors := []string{"#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#EC4899"}
|
||||
colorIdx := 0
|
||||
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
for k, v := range dataMap {
|
||||
card := StatCard{
|
||||
ID: uuid.New().String(),
|
||||
Title: formatColumnLabel(k),
|
||||
Value: v,
|
||||
Color: colors[colorIdx%len(colors)],
|
||||
}
|
||||
cardsData.Cards = append(cardsData.Cards, card)
|
||||
colorIdx++
|
||||
}
|
||||
}
|
||||
|
||||
return &Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizStatCards,
|
||||
Title: title,
|
||||
Data: cardsData,
|
||||
Config: VisualizationConfig{
|
||||
Animated: true,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) createKPIVisualization(title string, data interface{}, mapping map[string]interface{}) *Visualization {
|
||||
kpiData := &KPIData{
|
||||
Value: data,
|
||||
}
|
||||
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
if v, ok := dataMap["value"]; ok {
|
||||
kpiData.Value = v
|
||||
}
|
||||
if v, ok := dataMap["change"].(float64); ok {
|
||||
kpiData.Change = v
|
||||
if v >= 0 {
|
||||
kpiData.ChangeType = "increase"
|
||||
} else {
|
||||
kpiData.ChangeType = "decrease"
|
||||
}
|
||||
}
|
||||
if v, ok := dataMap["target"]; ok {
|
||||
kpiData.Target = v
|
||||
}
|
||||
if v, ok := dataMap["unit"].(string); ok {
|
||||
kpiData.Unit = v
|
||||
}
|
||||
}
|
||||
|
||||
return &Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizKPI,
|
||||
Title: title,
|
||||
Data: kpiData,
|
||||
Config: VisualizationConfig{
|
||||
Animated: true,
|
||||
ShowValues: true,
|
||||
},
|
||||
Style: VisualizationStyle{
|
||||
MinHeight: "150px",
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) createTimelineVisualization(title string, data interface{}, mapping map[string]interface{}) *Visualization {
|
||||
timelineData := &TimelineData{
|
||||
Events: make([]TimelineEvent, 0),
|
||||
}
|
||||
|
||||
if dataSlice, ok := data.([]interface{}); ok {
|
||||
for _, item := range dataSlice {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
event := TimelineEvent{
|
||||
ID: uuid.New().String(),
|
||||
}
|
||||
|
||||
if v, ok := itemMap["date"].(string); ok {
|
||||
event.Date, _ = time.Parse(time.RFC3339, v)
|
||||
}
|
||||
if v, ok := itemMap["title"].(string); ok {
|
||||
event.Title = v
|
||||
}
|
||||
if v, ok := itemMap["description"].(string); ok {
|
||||
event.Description = v
|
||||
}
|
||||
|
||||
timelineData.Events = append(timelineData.Events, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(timelineData.Events, func(i, j int) bool {
|
||||
return timelineData.Events[i].Date.Before(timelineData.Events[j].Date)
|
||||
})
|
||||
|
||||
return &Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizTimeline,
|
||||
Title: title,
|
||||
Data: timelineData,
|
||||
Config: VisualizationConfig{
|
||||
Animated: true,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) createComparisonVisualization(title string, data interface{}, mapping map[string]interface{}) *Visualization {
|
||||
compData := &ComparisonData{
|
||||
Items: make([]ComparisonItem, 0),
|
||||
Categories: make([]string, 0),
|
||||
}
|
||||
|
||||
if dataSlice, ok := data.([]interface{}); ok && len(dataSlice) > 0 {
|
||||
if firstItem, ok := dataSlice[0].(map[string]interface{}); ok {
|
||||
for k := range firstItem {
|
||||
if k != "name" && k != "id" && k != "image" {
|
||||
compData.Categories = append(compData.Categories, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range dataSlice {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
compItem := ComparisonItem{
|
||||
ID: uuid.New().String(),
|
||||
Values: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
if v, ok := itemMap["name"].(string); ok {
|
||||
compItem.Name = v
|
||||
}
|
||||
if v, ok := itemMap["image"].(string); ok {
|
||||
compItem.Image = v
|
||||
}
|
||||
|
||||
for _, cat := range compData.Categories {
|
||||
if v, ok := itemMap[cat]; ok {
|
||||
compItem.Values[cat] = v
|
||||
}
|
||||
}
|
||||
|
||||
compData.Items = append(compData.Items, compItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizComparison,
|
||||
Title: title,
|
||||
Data: compData,
|
||||
Config: VisualizationConfig{
|
||||
ShowLabels: true,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) createProgressVisualization(title string, data interface{}, mapping map[string]interface{}) *Visualization {
|
||||
progressData := &ProgressData{
|
||||
Current: 0,
|
||||
Total: 100,
|
||||
ShowValue: true,
|
||||
Animated: true,
|
||||
}
|
||||
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
if v, ok := dataMap["current"]; ok {
|
||||
progressData.Current = toFloat64(v)
|
||||
}
|
||||
if v, ok := dataMap["total"]; ok {
|
||||
progressData.Total = toFloat64(v)
|
||||
}
|
||||
if v, ok := dataMap["label"].(string); ok {
|
||||
progressData.Label = v
|
||||
}
|
||||
}
|
||||
|
||||
if v, ok := data.(float64); ok {
|
||||
progressData.Current = v
|
||||
}
|
||||
|
||||
return &Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizProgress,
|
||||
Title: title,
|
||||
Data: progressData,
|
||||
Config: VisualizationConfig{
|
||||
Animated: true,
|
||||
ShowValues: true,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) CreateBarChart(title string, labels []string, values []float64) Visualization {
|
||||
return Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizBarChart,
|
||||
Title: title,
|
||||
Data: &ChartData{
|
||||
Labels: labels,
|
||||
Datasets: []ChartDataset{
|
||||
{Label: title, Data: values},
|
||||
},
|
||||
},
|
||||
Config: VisualizationConfig{
|
||||
ShowLegend: true,
|
||||
ShowTooltip: true,
|
||||
Animated: true,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) CreateLineChart(title string, labels []string, datasets []ChartDataset) Visualization {
|
||||
return Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizLineChart,
|
||||
Title: title,
|
||||
Data: &ChartData{
|
||||
Labels: labels,
|
||||
Datasets: datasets,
|
||||
},
|
||||
Config: VisualizationConfig{
|
||||
ShowLegend: true,
|
||||
ShowTooltip: true,
|
||||
ShowGrid: true,
|
||||
Animated: true,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) CreatePieChart(title string, labels []string, values []float64) Visualization {
|
||||
return Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizPieChart,
|
||||
Title: title,
|
||||
Data: &ChartData{
|
||||
Labels: labels,
|
||||
Datasets: []ChartDataset{
|
||||
{Label: title, Data: values},
|
||||
},
|
||||
},
|
||||
Config: VisualizationConfig{
|
||||
ShowLegend: true,
|
||||
ShowTooltip: true,
|
||||
ShowValues: true,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) CreateTable(title string, columns []TableColumn, rows []TableRow) Visualization {
|
||||
return Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizTable,
|
||||
Title: title,
|
||||
Data: &TableData{
|
||||
Columns: columns,
|
||||
Rows: rows,
|
||||
Summary: &TableSummary{TotalRows: len(rows)},
|
||||
},
|
||||
Config: VisualizationConfig{
|
||||
Sortable: true,
|
||||
Searchable: true,
|
||||
Paginated: len(rows) > 10,
|
||||
PageSize: 10,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) CreateStatCards(title string, cards []StatCard) Visualization {
|
||||
return Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizStatCards,
|
||||
Title: title,
|
||||
Data: &StatCardsData{Cards: cards},
|
||||
Config: VisualizationConfig{
|
||||
Animated: true,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) CreateKPI(title string, value interface{}, change float64, unit string) Visualization {
|
||||
changeType := "neutral"
|
||||
if change > 0 {
|
||||
changeType = "increase"
|
||||
} else if change < 0 {
|
||||
changeType = "decrease"
|
||||
}
|
||||
|
||||
return Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizKPI,
|
||||
Title: title,
|
||||
Data: &KPIData{
|
||||
Value: value,
|
||||
Change: change,
|
||||
ChangeType: changeType,
|
||||
Unit: unit,
|
||||
},
|
||||
Config: VisualizationConfig{
|
||||
Animated: true,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) CreateMarkdown(title string, content string) Visualization {
|
||||
return Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizMarkdown,
|
||||
Title: title,
|
||||
Data: &MarkdownData{Content: content},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) CreateCodeBlock(title, code, language string) Visualization {
|
||||
return Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizCodeBlock,
|
||||
Title: title,
|
||||
Data: &CodeBlockData{
|
||||
Code: code,
|
||||
Language: language,
|
||||
ShowLineNum: true,
|
||||
Copyable: true,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) CreateTabs(title string, tabs []TabItem) Visualization {
|
||||
return Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizTabs,
|
||||
Title: title,
|
||||
Data: &TabsData{Tabs: tabs},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) CreateAccordion(title string, items []AccordionItem) Visualization {
|
||||
return Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizAccordion,
|
||||
Title: title,
|
||||
Data: &AccordionData{Items: items},
|
||||
Config: VisualizationConfig{
|
||||
Animated: true,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) CreateHeatmap(title string, xLabels, yLabels []string, values [][]float64) Visualization {
|
||||
return Visualization{
|
||||
ID: uuid.New().String(),
|
||||
Type: VizHeatmap,
|
||||
Title: title,
|
||||
Data: &HeatmapData{
|
||||
XLabels: xLabels,
|
||||
YLabels: yLabels,
|
||||
Values: values,
|
||||
},
|
||||
Config: VisualizationConfig{
|
||||
ShowTooltip: true,
|
||||
ShowLabels: true,
|
||||
},
|
||||
Responsive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func extractJSON(text string) string {
|
||||
re := regexp.MustCompile(`(?s)\{.*\}`)
|
||||
match := re.FindString(text)
|
||||
if match != "" {
|
||||
return match
|
||||
}
|
||||
return "{}"
|
||||
}
|
||||
|
||||
func toFloat64(v interface{}) float64 {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return val
|
||||
case float32:
|
||||
return float64(val)
|
||||
case int:
|
||||
return float64(val)
|
||||
case int64:
|
||||
return float64(val)
|
||||
case string:
|
||||
f, _ := strconv.ParseFloat(val, 64)
|
||||
return f
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func formatColumnLabel(key string) string {
|
||||
key = strings.ReplaceAll(key, "_", " ")
|
||||
key = strings.ReplaceAll(key, "-", " ")
|
||||
|
||||
words := strings.Fields(key)
|
||||
for i, word := range words {
|
||||
if len(word) > 0 {
|
||||
words[i] = strings.ToUpper(string(word[0])) + strings.ToLower(word[1:])
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(words, " ")
|
||||
}
|
||||
|
||||
func extractStringFromData(data interface{}, mapping map[string]interface{}, key string) string {
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
if v, ok := dataMap[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%v", data)
|
||||
}
|
||||
|
||||
func formatDataAsMarkdown(data interface{}) string {
|
||||
jsonBytes, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%v", data)
|
||||
}
|
||||
return "```json\n" + string(jsonBytes) + "\n```"
|
||||
}
|
||||
335
backend/internal/labs/types.go
Normal file
335
backend/internal/labs/types.go
Normal file
@@ -0,0 +1,335 @@
|
||||
package labs
|
||||
|
||||
import "time"
|
||||
|
||||
type VisualizationType string
|
||||
|
||||
const (
|
||||
VizBarChart VisualizationType = "bar_chart"
|
||||
VizLineChart VisualizationType = "line_chart"
|
||||
VizPieChart VisualizationType = "pie_chart"
|
||||
VizDonutChart VisualizationType = "donut_chart"
|
||||
VizAreaChart VisualizationType = "area_chart"
|
||||
VizScatterPlot VisualizationType = "scatter_plot"
|
||||
VizHeatmap VisualizationType = "heatmap"
|
||||
VizTreemap VisualizationType = "treemap"
|
||||
VizGauge VisualizationType = "gauge"
|
||||
VizRadar VisualizationType = "radar"
|
||||
VizSankey VisualizationType = "sankey"
|
||||
VizTable VisualizationType = "table"
|
||||
VizTimeline VisualizationType = "timeline"
|
||||
VizKPI VisualizationType = "kpi"
|
||||
VizProgress VisualizationType = "progress"
|
||||
VizComparison VisualizationType = "comparison"
|
||||
VizStatCards VisualizationType = "stat_cards"
|
||||
VizMap VisualizationType = "map"
|
||||
VizFlowChart VisualizationType = "flow_chart"
|
||||
VizOrgChart VisualizationType = "org_chart"
|
||||
VizCodeBlock VisualizationType = "code_block"
|
||||
VizMarkdown VisualizationType = "markdown"
|
||||
VizCollapsible VisualizationType = "collapsible"
|
||||
VizTabs VisualizationType = "tabs"
|
||||
VizAccordion VisualizationType = "accordion"
|
||||
VizStepper VisualizationType = "stepper"
|
||||
VizForm VisualizationType = "form"
|
||||
)
|
||||
|
||||
type Visualization struct {
|
||||
ID string `json:"id"`
|
||||
Type VisualizationType `json:"type"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Data interface{} `json:"data"`
|
||||
Config VisualizationConfig `json:"config,omitempty"`
|
||||
Style VisualizationStyle `json:"style,omitempty"`
|
||||
Actions []VisualizationAction `json:"actions,omitempty"`
|
||||
Responsive bool `json:"responsive"`
|
||||
}
|
||||
|
||||
type VisualizationConfig struct {
|
||||
ShowLegend bool `json:"showLegend,omitempty"`
|
||||
ShowGrid bool `json:"showGrid,omitempty"`
|
||||
ShowTooltip bool `json:"showTooltip,omitempty"`
|
||||
ShowLabels bool `json:"showLabels,omitempty"`
|
||||
ShowValues bool `json:"showValues,omitempty"`
|
||||
Animated bool `json:"animated,omitempty"`
|
||||
Stacked bool `json:"stacked,omitempty"`
|
||||
Horizontal bool `json:"horizontal,omitempty"`
|
||||
Sortable bool `json:"sortable,omitempty"`
|
||||
Filterable bool `json:"filterable,omitempty"`
|
||||
Searchable bool `json:"searchable,omitempty"`
|
||||
Paginated bool `json:"paginated,omitempty"`
|
||||
PageSize int `json:"pageSize,omitempty"`
|
||||
Expandable bool `json:"expandable,omitempty"`
|
||||
DefaultExpanded bool `json:"defaultExpanded,omitempty"`
|
||||
XAxisLabel string `json:"xAxisLabel,omitempty"`
|
||||
YAxisLabel string `json:"yAxisLabel,omitempty"`
|
||||
Colors []string `json:"colors,omitempty"`
|
||||
DateFormat string `json:"dateFormat,omitempty"`
|
||||
NumberFormat string `json:"numberFormat,omitempty"`
|
||||
CurrencySymbol string `json:"currencySymbol,omitempty"`
|
||||
}
|
||||
|
||||
type VisualizationStyle struct {
|
||||
Width string `json:"width,omitempty"`
|
||||
Height string `json:"height,omitempty"`
|
||||
MinHeight string `json:"minHeight,omitempty"`
|
||||
MaxHeight string `json:"maxHeight,omitempty"`
|
||||
Padding string `json:"padding,omitempty"`
|
||||
Margin string `json:"margin,omitempty"`
|
||||
BorderRadius string `json:"borderRadius,omitempty"`
|
||||
Background string `json:"background,omitempty"`
|
||||
Shadow string `json:"shadow,omitempty"`
|
||||
FontFamily string `json:"fontFamily,omitempty"`
|
||||
FontSize string `json:"fontSize,omitempty"`
|
||||
TextColor string `json:"textColor,omitempty"`
|
||||
AccentColor string `json:"accentColor,omitempty"`
|
||||
GridColor string `json:"gridColor,omitempty"`
|
||||
}
|
||||
|
||||
type VisualizationAction struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Handler string `json:"handler,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
type ChartData struct {
|
||||
Labels []string `json:"labels"`
|
||||
Datasets []ChartDataset `json:"datasets"`
|
||||
}
|
||||
|
||||
type ChartDataset struct {
|
||||
Label string `json:"label"`
|
||||
Data []float64 `json:"data"`
|
||||
BackgroundColor string `json:"backgroundColor,omitempty"`
|
||||
BorderColor string `json:"borderColor,omitempty"`
|
||||
Fill bool `json:"fill,omitempty"`
|
||||
}
|
||||
|
||||
type TableData struct {
|
||||
Columns []TableColumn `json:"columns"`
|
||||
Rows []TableRow `json:"rows"`
|
||||
Summary *TableSummary `json:"summary,omitempty"`
|
||||
}
|
||||
|
||||
type TableColumn struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Width string `json:"width,omitempty"`
|
||||
Sortable bool `json:"sortable,omitempty"`
|
||||
Align string `json:"align,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
Highlight bool `json:"highlight,omitempty"`
|
||||
}
|
||||
|
||||
type TableRow map[string]interface{}
|
||||
|
||||
type TableSummary struct {
|
||||
TotalRows int `json:"totalRows"`
|
||||
Aggregations map[string]interface{} `json:"aggregations,omitempty"`
|
||||
}
|
||||
|
||||
type TimelineData struct {
|
||||
Events []TimelineEvent `json:"events"`
|
||||
}
|
||||
|
||||
type TimelineEvent struct {
|
||||
ID string `json:"id"`
|
||||
Date time.Time `json:"date"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Color string `json:"color,omitempty"`
|
||||
Link string `json:"link,omitempty"`
|
||||
}
|
||||
|
||||
type KPIData struct {
|
||||
Value interface{} `json:"value"`
|
||||
PrevValue interface{} `json:"prevValue,omitempty"`
|
||||
Change float64 `json:"change,omitempty"`
|
||||
ChangeType string `json:"changeType,omitempty"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Prefix string `json:"prefix,omitempty"`
|
||||
Suffix string `json:"suffix,omitempty"`
|
||||
Target interface{} `json:"target,omitempty"`
|
||||
Trend []float64 `json:"trend,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Color string `json:"color,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type StatCardsData struct {
|
||||
Cards []StatCard `json:"cards"`
|
||||
}
|
||||
|
||||
type StatCard struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Value interface{} `json:"value"`
|
||||
Change float64 `json:"change,omitempty"`
|
||||
ChangeLabel string `json:"changeLabel,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Color string `json:"color,omitempty"`
|
||||
Sparkline []float64 `json:"sparkline,omitempty"`
|
||||
}
|
||||
|
||||
type ComparisonData struct {
|
||||
Items []ComparisonItem `json:"items"`
|
||||
Categories []string `json:"categories"`
|
||||
}
|
||||
|
||||
type ComparisonItem struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image,omitempty"`
|
||||
Values map[string]interface{} `json:"values"`
|
||||
}
|
||||
|
||||
type ProgressData struct {
|
||||
Current float64 `json:"current"`
|
||||
Total float64 `json:"total"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Color string `json:"color,omitempty"`
|
||||
ShowValue bool `json:"showValue,omitempty"`
|
||||
Animated bool `json:"animated,omitempty"`
|
||||
}
|
||||
|
||||
type HeatmapData struct {
|
||||
XLabels []string `json:"xLabels"`
|
||||
YLabels []string `json:"yLabels"`
|
||||
Values [][]float64 `json:"values"`
|
||||
Min float64 `json:"min,omitempty"`
|
||||
Max float64 `json:"max,omitempty"`
|
||||
}
|
||||
|
||||
type MapData struct {
|
||||
Center []float64 `json:"center"`
|
||||
Zoom int `json:"zoom"`
|
||||
Markers []MapMarker `json:"markers,omitempty"`
|
||||
Regions []MapRegion `json:"regions,omitempty"`
|
||||
}
|
||||
|
||||
type MapMarker struct {
|
||||
ID string `json:"id"`
|
||||
Position []float64 `json:"position"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Color string `json:"color,omitempty"`
|
||||
Popup string `json:"popup,omitempty"`
|
||||
}
|
||||
|
||||
type MapRegion struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Value float64 `json:"value"`
|
||||
Color string `json:"color,omitempty"`
|
||||
}
|
||||
|
||||
type CollapsibleData struct {
|
||||
Title string `json:"title"`
|
||||
Content interface{} `json:"content"`
|
||||
DefaultOpen bool `json:"defaultOpen,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Children []Visualization `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
type TabsData struct {
|
||||
Tabs []TabItem `json:"tabs"`
|
||||
}
|
||||
|
||||
type TabItem struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Content interface{} `json:"content"`
|
||||
Children []Visualization `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
type AccordionData struct {
|
||||
Items []AccordionItem `json:"items"`
|
||||
}
|
||||
|
||||
type AccordionItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content interface{} `json:"content"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Open bool `json:"open,omitempty"`
|
||||
}
|
||||
|
||||
type StepperData struct {
|
||||
Steps []StepperStep `json:"steps"`
|
||||
CurrentStep int `json:"currentStep"`
|
||||
Orientation string `json:"orientation,omitempty"`
|
||||
}
|
||||
|
||||
type StepperStep struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Content interface{} `json:"content,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
}
|
||||
|
||||
type FormData struct {
|
||||
Fields []FormField `json:"fields"`
|
||||
SubmitLabel string `json:"submitLabel,omitempty"`
|
||||
Layout string `json:"layout,omitempty"`
|
||||
}
|
||||
|
||||
type FormField struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Label string `json:"label"`
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
Options []FormOption `json:"options,omitempty"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
Validation string `json:"validation,omitempty"`
|
||||
}
|
||||
|
||||
type FormOption struct {
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type CodeBlockData struct {
|
||||
Code string `json:"code"`
|
||||
Language string `json:"language"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Highlight []int `json:"highlight,omitempty"`
|
||||
ShowLineNum bool `json:"showLineNum,omitempty"`
|
||||
Copyable bool `json:"copyable,omitempty"`
|
||||
}
|
||||
|
||||
type MarkdownData struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Sections []ReportSection `json:"sections"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
Theme string `json:"theme,omitempty"`
|
||||
CustomCSS string `json:"customCss,omitempty"`
|
||||
}
|
||||
|
||||
type ReportSection struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Visualizations []Visualization `json:"visualizations"`
|
||||
Layout string `json:"layout,omitempty"`
|
||||
Columns int `json:"columns,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user