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
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user