// Copyright 2026 MarketAlly. All rights reserved. // SPDX-License-Identifier: MIT package pages import ( "context" "errors" "fmt" "strconv" "strings" repo_model "code.gitcaddy.com/server/v3/models/repo" "code.gitcaddy.com/server/v3/modules/ai" "code.gitcaddy.com/server/v3/modules/json" pages_module "code.gitcaddy.com/server/v3/modules/pages" ) // aiGeneratedConfig holds the AI-generated landing page content type aiGeneratedConfig struct { Brand struct { Name string `json:"name"` Tagline string `json:"tagline"` } `json:"brand"` Hero struct { Headline string `json:"headline"` Subheadline string `json:"subheadline"` PrimaryCTA struct { Label string `json:"label"` URL string `json:"url"` } `json:"primary_cta"` SecondaryCTA struct { Label string `json:"label"` URL string `json:"url"` } `json:"secondary_cta"` } `json:"hero"` Stats []pages_module.StatConfig `json:"stats"` ValueProps []pages_module.ValuePropConfig `json:"value_props"` Features []pages_module.FeatureConfig `json:"features"` CTASection struct { Headline string `json:"headline"` Subheadline string `json:"subheadline"` ButtonLabel string `json:"button_label"` ButtonURL string `json:"button_url"` } `json:"cta_section"` SEO struct { Title string `json:"title"` Description string `json:"description"` } `json:"seo"` } // GenerateLandingPageContent uses AI to auto-generate landing page content from repo metadata. func GenerateLandingPageContent(ctx context.Context, repo *repo_model.Repository, readme string) (*pages_module.LandingConfig, error) { if !ai.IsEnabled() { return nil, errors.New("AI service is not enabled") } repoURL := repo.HTMLURL() topics := strings.Join(repo.Topics, ", ") client := ai.GetClient() resp, err := client.ExecuteTask(ctx, &ai.ExecuteTaskRequest{ RepoID: repo.ID, Task: "landing_page_generate", Context: map[string]string{ "repo_name": repo.Name, "repo_description": repo.Description, "repo_url": repoURL, "topics": topics, "primary_language": getPrimaryLanguageName(repo), "stars": strconv.Itoa(repo.NumStars), "forks": strconv.Itoa(repo.NumForks), "readme": truncateReadme(readme), }, }) if err != nil { return nil, fmt.Errorf("AI generation failed: %w", err) } if !resp.Success { return nil, fmt.Errorf("AI generation error: %s", resp.Error) } var generated aiGeneratedConfig if err := json.Unmarshal([]byte(extractJSON(resp.Result)), &generated); err != nil { return nil, fmt.Errorf("failed to parse AI response: %w", err) } // Build LandingConfig from AI response config := pages_module.DefaultConfig() config.Brand.Name = generated.Brand.Name config.Brand.Tagline = generated.Brand.Tagline config.Hero.Headline = generated.Hero.Headline config.Hero.Subheadline = generated.Hero.Subheadline config.Hero.PrimaryCTA = pages_module.CTAButton{ Label: generated.Hero.PrimaryCTA.Label, URL: repoURL, } config.Hero.SecondaryCTA = pages_module.CTAButton{ Label: generated.Hero.SecondaryCTA.Label, URL: repoURL, } if len(generated.Stats) > 0 { config.Stats = generated.Stats } if len(generated.ValueProps) > 0 { config.ValueProps = generated.ValueProps } if len(generated.Features) > 0 { config.Features = generated.Features } config.CTASection = pages_module.CTASectionConfig{ Headline: generated.CTASection.Headline, Subheadline: generated.CTASection.Subheadline, Button: pages_module.CTAButton{ Label: generated.CTASection.ButtonLabel, URL: repoURL, }, } if generated.SEO.Title != "" { config.SEO.Title = generated.SEO.Title } if generated.SEO.Description != "" { config.SEO.Description = generated.SEO.Description } return config, nil } // getPrimaryLanguageName returns the primary language name for the repo, or empty string func getPrimaryLanguageName(repo *repo_model.Repository) string { if repo.PrimaryLanguage != nil { return repo.PrimaryLanguage.Language } return "" } // extractJSON extracts JSON from an AI response that may be wrapped in markdown code fences. func extractJSON(s string) string { s = strings.TrimSpace(s) // Strip ```json ... ``` or ``` ... ``` wrapper if strings.HasPrefix(s, "```") { // Remove opening fence (```json or ```) if idx := strings.Index(s, "\n"); idx != -1 { s = s[idx+1:] } // Remove closing fence if idx := strings.LastIndex(s, "```"); idx != -1 { s = s[:idx] } s = strings.TrimSpace(s) } // If still not starting with {, try to find first { and last } if !strings.HasPrefix(s, "{") { start := strings.Index(s, "{") end := strings.LastIndex(s, "}") if start != -1 && end != -1 && end > start { s = s[start : end+1] } } return s } // truncateReadme limits README content to avoid sending too much to the AI func truncateReadme(readme string) string { const maxLen = 4000 if len(readme) <= maxLen { return readme } return readme[:maxLen] + "\n... (truncated)" } // TranslateLandingPageContent uses AI to translate landing page content to a target language. // Returns a JSON overlay string that can be deep-merged onto the base config. func TranslateLandingPageContent(ctx context.Context, repo *repo_model.Repository, config *pages_module.LandingConfig, targetLang string) (string, error) { if !ai.IsEnabled() { return "", errors.New("AI service is not enabled") } langNames := pages_module.LanguageDisplayNames() langName := langNames[targetLang] if langName == "" { langName = targetLang } // Build the source content to translate sourceContent := buildTranslatableContent(config) client := ai.GetClient() resp, err := client.ExecuteTask(ctx, &ai.ExecuteTaskRequest{ RepoID: repo.ID, Task: "landing_page_translate", Context: map[string]string{ "target_language": langName, "target_code": targetLang, "source_content": sourceContent, }, }) if err != nil { return "", fmt.Errorf("AI translation failed: %w", err) } if !resp.Success { return "", fmt.Errorf("AI translation error: %s", resp.Error) } // Validate it's valid JSON result := extractJSON(resp.Result) var check map[string]any if err := json.Unmarshal([]byte(result), &check); err != nil { return "", fmt.Errorf("AI returned invalid JSON: %w", err) } return result, nil } // buildTranslatableContent extracts translatable text from a config for the AI func buildTranslatableContent(config *pages_module.LandingConfig) string { content := map[string]any{} // Brand if config.Brand.Name != "" || config.Brand.Tagline != "" { content["brand"] = map[string]any{ "name": config.Brand.Name, "tagline": config.Brand.Tagline, } } // Hero content["hero"] = map[string]any{ "headline": config.Hero.Headline, "subheadline": config.Hero.Subheadline, "primary_cta": map[string]string{"label": config.Hero.PrimaryCTA.Label}, "secondary_cta": map[string]string{"label": config.Hero.SecondaryCTA.Label}, } // Stats, Value Props, Features (already struct-serializable) if len(config.Stats) > 0 { content["stats"] = config.Stats } if len(config.ValueProps) > 0 { content["value_props"] = config.ValueProps } if len(config.Features) > 0 { content["features"] = config.Features } // Testimonials if len(config.SocialProof.Testimonials) > 0 { testimonials := make([]map[string]string, 0, len(config.SocialProof.Testimonials)) for _, t := range config.SocialProof.Testimonials { testimonials = append(testimonials, map[string]string{ "quote": t.Quote, "role": t.Role, }) } content["social_proof"] = map[string]any{"testimonials": testimonials} } // Pricing if config.Pricing.Headline != "" || len(config.Pricing.Plans) > 0 { pricing := map[string]any{ "headline": config.Pricing.Headline, "subheadline": config.Pricing.Subheadline, } if len(config.Pricing.Plans) > 0 { plans := make([]map[string]string, 0, len(config.Pricing.Plans)) for _, p := range config.Pricing.Plans { plans = append(plans, map[string]string{ "name": p.Name, "period": p.Period, "cta": p.CTA, }) } pricing["plans"] = plans } content["pricing"] = pricing } // CTA Section content["cta_section"] = map[string]any{ "headline": config.CTASection.Headline, "subheadline": config.CTASection.Subheadline, "button": map[string]string{"label": config.CTASection.Button.Label}, } // Blog if config.Blog.Enabled { blogHeadline := config.Blog.Headline if blogHeadline == "" { blogHeadline = "Latest Posts" } blog := map[string]any{ "headline": blogHeadline, "subheadline": config.Blog.Subheadline, } if config.Blog.CTAButton.Label != "" { blog["cta_button"] = map[string]string{"label": config.Blog.CTAButton.Label} } content["blog"] = blog } // Gallery if config.Gallery.Enabled { galleryHeadline := config.Gallery.Headline if galleryHeadline == "" { galleryHeadline = "Gallery" } content["gallery"] = map[string]any{ "headline": galleryHeadline, "subheadline": config.Gallery.Subheadline, } } // Comparison if config.Comparison.Enabled { compHeadline := config.Comparison.Headline if compHeadline == "" { compHeadline = "How We Compare" } content["comparison"] = map[string]any{ "headline": compHeadline, "subheadline": config.Comparison.Subheadline, } } // Footer if config.Footer.Copyright != "" || len(config.Footer.Links) > 0 { footer := map[string]any{ "copyright": config.Footer.Copyright, } if len(config.Footer.Links) > 0 { links := make([]map[string]string, 0, len(config.Footer.Links)) for _, l := range config.Footer.Links { links = append(links, map[string]string{"label": l.Label}) } footer["links"] = links } content["footer"] = footer } // SEO if config.SEO.Title != "" || config.SEO.Description != "" { content["seo"] = map[string]any{ "title": config.SEO.Title, "description": config.SEO.Description, } } // Navigation labels (for translating nav items and section headers) // Use template-specific defaults so AI translates the correct terms // (e.g. "Systems Analysis" for architecture-deep-dive, not generic "Value Props") defaults := pages_module.TemplateDefaultLabels(config.Template) labelOrDefault := func(label, def string) string { if label != "" { return label } return def } content["navigation"] = map[string]any{ "label_value_props": labelOrDefault(config.Navigation.LabelValueProps, defaults.LabelValueProps), "label_features": labelOrDefault(config.Navigation.LabelFeatures, defaults.LabelFeatures), "label_pricing": labelOrDefault(config.Navigation.LabelPricing, defaults.LabelPricing), "label_blog": labelOrDefault(config.Navigation.LabelBlog, defaults.LabelBlog), "label_gallery": labelOrDefault(config.Navigation.LabelGallery, defaults.LabelGallery), "label_compare": labelOrDefault(config.Navigation.LabelCompare, defaults.LabelCompare), "label_docs": labelOrDefault(config.Navigation.LabelDocs, "Docs"), "label_releases": labelOrDefault(config.Navigation.LabelReleases, "Releases"), "label_api": labelOrDefault(config.Navigation.LabelAPI, "API"), "label_issues": labelOrDefault(config.Navigation.LabelIssues, "Issues"), } data, _ := json.Marshal(content) return string(data) }