From 85ab93145e9db7ae086bd634bb89c578aa9ce1cc Mon Sep 17 00:00:00 2001 From: logikonline Date: Sat, 7 Mar 2026 15:52:46 -0500 Subject: [PATCH] refactor(pages): move AI prompts to plugin definitions and add JSON extraction Simplify AI content generation by delegating to plugin system: - Remove inline prompt instructions (now in LandingPageContentPlugin) - Add extractJSON helper to handle markdown-wrapped responses - Hardcode CTA URLs to repo URL instead of relying on AI - Apply JSON extraction to both content generation and translation - Reduces code duplication and improves maintainability --- services/pages/generate.go | 113 ++++++++++++------------------------- 1 file changed, 36 insertions(+), 77 deletions(-) diff --git a/services/pages/generate.go b/services/pages/generate.go index de77034e2f..0764586b21 100644 --- a/services/pages/generate.go +++ b/services/pages/generate.go @@ -71,45 +71,6 @@ func GenerateLandingPageContent(ctx context.Context, repo *repo_model.Repository "stars": strconv.Itoa(repo.NumStars), "forks": strconv.Itoa(repo.NumForks), "readme": truncateReadme(readme), - "instruction": `You are a landing page copywriter. Analyze this open-source repository and generate compelling landing page content. - -Return valid JSON with this exact structure: -{ - "brand": {"name": "Project Name", "tagline": "Short tagline"}, - "hero": { - "headline": "Compelling headline (max 10 words)", - "subheadline": "Supporting text explaining the value (1-2 sentences)", - "primary_cta": {"label": "Get Started", "url": "` + repoURL + `"}, - "secondary_cta": {"label": "View on GitHub", "url": "` + repoURL + `"} - }, - "stats": [ - {"value": "...", "label": "..."} - ], - "value_props": [ - {"title": "...", "description": "...", "icon": "one of: zap,shield,rocket,check,star,heart,lock,globe,clock,gear,code,terminal,package,database,cloud,cpu,graph,people,tools,light-bulb"} - ], - "features": [ - {"title": "...", "description": "...", "icon": "same icon options as value_props"} - ], - "cta_section": { - "headline": "Ready to get started?", - "subheadline": "...", - "button_label": "Get Started Free", - "button_url": "` + repoURL + `" - }, - "seo": { - "title": "SEO title (50-60 chars)", - "description": "Meta description (150-160 chars)" - } -} - -Guidelines: -- Generate 3-4 stats based on actual repo data (stars, forks, etc.) or compelling project metrics -- Generate exactly 3 value propositions that highlight the project's key strengths -- Generate 3-6 features based on what the README describes -- Use action-oriented, benefit-focused copy -- Keep it professional but engaging -- Icon choices should match the content semantically`, }, }) if err != nil { @@ -121,7 +82,7 @@ Guidelines: } var generated aiGeneratedConfig - if err := json.Unmarshal([]byte(resp.Result), &generated); err != nil { + if err := json.Unmarshal([]byte(extractJSON(resp.Result)), &generated); err != nil { return nil, fmt.Errorf("failed to parse AI response: %w", err) } @@ -133,11 +94,11 @@ Guidelines: config.Hero.Subheadline = generated.Hero.Subheadline config.Hero.PrimaryCTA = pages_module.CTAButton{ Label: generated.Hero.PrimaryCTA.Label, - URL: generated.Hero.PrimaryCTA.URL, + URL: repoURL, } config.Hero.SecondaryCTA = pages_module.CTAButton{ Label: generated.Hero.SecondaryCTA.Label, - URL: generated.Hero.SecondaryCTA.URL, + URL: repoURL, } if len(generated.Stats) > 0 { config.Stats = generated.Stats @@ -153,7 +114,7 @@ Guidelines: Subheadline: generated.CTASection.Subheadline, Button: pages_module.CTAButton{ Label: generated.CTASection.ButtonLabel, - URL: generated.CTASection.ButtonURL, + URL: repoURL, }, } if generated.SEO.Title != "" { @@ -174,6 +135,35 @@ func getPrimaryLanguageName(repo *repo_model.Repository) string { 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 @@ -207,38 +197,6 @@ func TranslateLandingPageContent(ctx context.Context, repo *repo_model.Repositor "target_language": langName, "target_code": targetLang, "source_content": sourceContent, - "instruction": `You are a professional translator. Translate the following landing page content to ` + langName + ` (` + targetLang + `). - -Return valid JSON as a partial config overlay. Only include the fields that have translatable text content. -Do NOT translate URLs, brand names, or technical terms. Keep the same JSON structure. - -Return this exact JSON structure (only include fields with actual translated text): -{ - "hero": { - "headline": "translated headline", - "subheadline": "translated subheadline", - "primary_cta": {"label": "translated label"}, - "secondary_cta": {"label": "translated label"} - }, - "stats": [{"value": "keep original", "label": "translated label"}], - "value_props": [{"title": "translated", "description": "translated"}], - "features": [{"title": "translated", "description": "translated"}], - "cta_section": { - "headline": "translated", - "subheadline": "translated", - "button": {"label": "translated"} - }, - "seo": { - "title": "translated", - "description": "translated" - } -} - -Important: -- Maintain the exact same number of items in arrays (stats, value_props, features) -- Keep icon names, URLs, and image_urls unchanged -- Use natural, marketing-quality translations (not literal/robotic) -- Adapt idioms and expressions for the target culture`, }, }) if err != nil { @@ -250,12 +208,13 @@ Important: } // Validate it's valid JSON + result := extractJSON(resp.Result) var check map[string]any - if err := json.Unmarshal([]byte(resp.Result), &check); err != nil { + if err := json.Unmarshal([]byte(result), &check); err != nil { return "", fmt.Errorf("AI returned invalid JSON: %w", err) } - return resp.Result, nil + return result, nil } // buildTranslatableContent extracts translatable text from a config for the AI