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```" }