mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
upload
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AIController handles AI-assisted endpoints
|
||||
type AIController struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// GenerateAboutPage creates about page content using the OpenRouter API
|
||||
func (ac *AIController) GenerateAboutPage(c *gin.Context) {
|
||||
var req aiAboutRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
clubName := strings.TrimSpace(req.ClubName)
|
||||
if clubName == "" {
|
||||
clubName = "Fotbalový klub"
|
||||
}
|
||||
style := strings.TrimSpace(req.Style)
|
||||
if style == "" {
|
||||
style = "default"
|
||||
}
|
||||
audience := strings.TrimSpace(req.Audience)
|
||||
if audience == "" {
|
||||
audience = "fanoušci klubu"
|
||||
}
|
||||
|
||||
system := "Jsi zkušený editor klubových webů. Pomůžeš napsat stránku 'O klubu'. Odpovídej česky, srozumitelně a profesionálně."
|
||||
user := fmt.Sprintf("Poznámky k vytvoření stránky O klubu:\n---\n%s\n---\nNázev klubu: %s\nPreferovaný styl: %s\nCílové publikum: %s\n\nPovinné požadavky:\n1) Zachovej fakta z poznámek a rozšiř je o kontext (historie, hodnoty, úspěchy, tým, zázemí, komunita).\n2) Rozděl text do sekcí s HTML nadpisy (h2/h3) a odstavci (p). Můžeš použít seznamy (ul/li) tam, kde to dává smysl. Bez inline stylů.\n3) Napiš krátký podnadpis (subtitle) vystihující náladu klubu.\n4) Vytvoř SEO titulek (do 60 znaků) a SEO popis (do 160 znaků).\n5) Odpověz POUZE JSON: {\"title\":\"...\", \"subtitle\":\"...\", \"html\":\"...\", \"seo_title\":\"...\", \"seo_description\":\"...\"}.\n6) HTML pole musí obsahovat kompletní obsah stránky dle požadavků.\n", strings.TrimSpace(req.Prompt), clubName, style, audience)
|
||||
|
||||
baseURL := getOpenRouterBaseURL()
|
||||
apiKey := getOpenRouterAPIKey()
|
||||
if strings.TrimSpace(apiKey) == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "OPENROUTER_API_KEY není nastaven"})
|
||||
return
|
||||
}
|
||||
|
||||
model := getOpenRouterModel()
|
||||
if model == "" {
|
||||
model = "mistralai/mistral-small-3.2-24b-instruct:free"
|
||||
}
|
||||
fallbackModel := getOpenRouterFallbackModel()
|
||||
if fallbackModel == "" {
|
||||
fallbackModel = "mistralai/mistral-nemo:free"
|
||||
}
|
||||
|
||||
callModel := func(modelName string) (string, int, error) {
|
||||
payload := map[string]interface{}{
|
||||
"model": modelName,
|
||||
"messages": []map[string]string{
|
||||
{"role": "system", "content": system},
|
||||
{"role": "user", "content": user},
|
||||
},
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 2200,
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
|
||||
endpoint := strings.TrimRight(baseURL, "/") + "/chat/completions"
|
||||
reqHTTP, err := http.NewRequest("POST", endpoint, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", http.StatusInternalServerError, err
|
||||
}
|
||||
reqHTTP.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
reqHTTP.Header.Set("Content-Type", "application/json")
|
||||
if ref := strings.TrimSpace(getenv("OPENROUTER_SITE_URL")); ref != "" {
|
||||
reqHTTP.Header.Set("HTTP-Referer", ref)
|
||||
}
|
||||
if ttl := strings.TrimSpace(getenv("OPENROUTER_APP_NAME")); ttl != "" {
|
||||
reqHTTP.Header.Set("X-Title", ttl)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 45 * time.Second}
|
||||
resp, err := client.Do(reqHTTP)
|
||||
if err != nil {
|
||||
return "", http.StatusBadGateway, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
var e map[string]interface{}
|
||||
_ = json.NewDecoder(resp.Body).Decode(&e)
|
||||
return "", resp.StatusCode, fmt.Errorf("OpenRouter API error: %v", e)
|
||||
}
|
||||
|
||||
var or struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&or); err != nil {
|
||||
return "", http.StatusBadGateway, err
|
||||
}
|
||||
if len(or.Choices) == 0 {
|
||||
return "", http.StatusBadGateway, fmt.Errorf("empty choices")
|
||||
}
|
||||
return strings.TrimSpace(or.Choices[0].Message.Content), http.StatusOK, nil
|
||||
}
|
||||
|
||||
content, _, err := callModel(model)
|
||||
if err != nil || strings.TrimSpace(content) == "" {
|
||||
if fbContent, _, fbErr := callModel(fallbackModel); fbErr == nil && strings.TrimSpace(fbContent) != "" {
|
||||
content = fbContent
|
||||
} else {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "OpenRouter selhal (včetně fallbacku)", "details": err.Error()})
|
||||
} else if fbErr != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "OpenRouter fallback selhal", "details": fbErr.Error()})
|
||||
} else {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "OpenRouter vrátil prázdnou odpověď"})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var out aiAboutResponse
|
||||
sanitized := sanitizeAIResponse(content)
|
||||
if err := json.Unmarshal([]byte(sanitized), &out); err != nil {
|
||||
re := regexp.MustCompile(`(?s)\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||
if m := re.FindString(sanitized); m != "" {
|
||||
_ = json.Unmarshal([]byte(m), &out)
|
||||
}
|
||||
}
|
||||
|
||||
if out.HTML != "" {
|
||||
out.HTML = html.UnescapeString(out.HTML)
|
||||
}
|
||||
if out.SEOTitle == "" {
|
||||
out.SEOTitle = fmt.Sprintf("%s | %s", clubName, "Oficiální informace")
|
||||
}
|
||||
if out.SEODescription == "" {
|
||||
out.SEODescription = fmt.Sprintf("Přečtěte si informace o klubu %s.", clubName)
|
||||
}
|
||||
if out.Title == "" {
|
||||
out.Title = clubName
|
||||
}
|
||||
if out.Subtitle == "" {
|
||||
out.Subtitle = "Oficiální klubový profil"
|
||||
}
|
||||
if out.HTML == "" {
|
||||
out.HTML = fmt.Sprintf("<h2>O klubu %s</h2><p>%s</p>", htmlEscape(clubName), htmlEscape(strings.TrimSpace(req.Prompt)))
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
func NewAIController(db *gorm.DB) *AIController {
|
||||
return &AIController{DB: db}
|
||||
}
|
||||
|
||||
type aiBlogRequest struct {
|
||||
// Short user input that describes the topic/notes for the blog in Czech
|
||||
Prompt string `json:"prompt" binding:"required"`
|
||||
// Optional extra hints
|
||||
Audience string `json:"audience"`
|
||||
MinWords int `json:"min_words"`
|
||||
}
|
||||
|
||||
type aiBlogResponse struct {
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
HTML string `json:"html"`
|
||||
}
|
||||
|
||||
type aiAboutRequest struct {
|
||||
Prompt string `json:"prompt" binding:"required"`
|
||||
ClubName string `json:"club_name"`
|
||||
Style string `json:"style"`
|
||||
Audience string `json:"audience"`
|
||||
}
|
||||
|
||||
type aiAboutResponse struct {
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
HTML string `json:"html"`
|
||||
SEOTitle string `json:"seo_title"`
|
||||
SEODescription string `json:"seo_description"`
|
||||
}
|
||||
|
||||
// GenerateBlog creates a blog article using the OpenRouter API (with Mistral models)
|
||||
func (ac *AIController) GenerateBlog(c *gin.Context) {
|
||||
var req aiBlogRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.MinWords <= 0 {
|
||||
req.MinWords = 450
|
||||
}
|
||||
|
||||
// Build instruction in Czech - emphasizing user text as primary source, but allow expansion if needed
|
||||
system := "Jsi asistent pro tvorbu článků. Tvým HLAVNÍM úkolem je: PŘEVZÍT TEXT OD UŽIVATELE a rozvinout ho do čitelného článku. Vždy vycházej z textu uživatele - zachovej VŠECHNY jeho informace, fakta a události. Pokud je text krátký, přidej kontext, rozvinutí a souvislosti, ale vždy kolem témat a informací z textu uživatele. Rozděl text do logických odstavců, přidej vhodné HTML značky (nadpisy h2/h3, odstavce p, seznamy ul/ol). Píšeš česky, srozumitelně a čtivě pro fotbalové fanoušky. HTML výstup bez inline stylů."
|
||||
user := fmt.Sprintf("Text od uživatele (VŽDY z něj vycházej, zachovej všechny jeho informace):\n---\n%s\n---\nPublikum: %s\nCílová délka: %d slov.\n\nPOVINNÉ POŽADAVKY:\n1) ZACHOVEJ všechny informace, jména, události a fakta z textu uživatele. To je ZÁKLAD článku.\n2) Pokud je text krátký (pod %d slov), ROZVIŇ ho - přidej kontext, atmosféru, detaily kolem událostí z textu uživatele. Buď čtivý a zajímavý.\n3) Pokud je text dostatečně dlouhý, pouze ho strukturuj do HTML s nadpisy a odstavci.\n4) Vygeneruj výstižný titulek vycházející z obsahu textu uživatele.\n5) Vytvoř URL slug (3-5 slov, max. 40 znaků, lowercase, bez diakritiky, jen písmena/číslice a pomlčky).\n6) Odpověz POUZE JSON: {\"title\": \"...\", \"slug\": \"...\", \"html\": \"...\"}\n7) HTML obsah = text uživatele + rozvinutí (pokud nutné) strukturovaný do HTML tagů (h2, p, ul, ol). BEZ inline stylů.\n\nPAMATUJ: Text uživatele = základ. Pokud je krátký, rozviň ho čtivě a zajímavě pro %s.\n", strings.TrimSpace(req.Prompt), strings.TrimSpace(req.Audience), req.MinWords, req.MinWords, strings.TrimSpace(req.Audience))
|
||||
|
||||
// Prepare OpenRouter request
|
||||
baseURL := getOpenRouterBaseURL()
|
||||
apiKey := getOpenRouterAPIKey()
|
||||
if strings.TrimSpace(apiKey) == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "OPENROUTER_API_KEY není nastaven"})
|
||||
return
|
||||
}
|
||||
|
||||
// Primary and fallback models
|
||||
model := getOpenRouterModel()
|
||||
if model == "" {
|
||||
model = "mistralai/mistral-small-3.2-24b-instruct:free"
|
||||
}
|
||||
fallbackModel := getOpenRouterFallbackModel()
|
||||
if fallbackModel == "" {
|
||||
fallbackModel = "mistralai/mistral-nemo:free"
|
||||
}
|
||||
|
||||
// Helper to call OpenRouter with a given model and return content
|
||||
callModel := func(modelName string) (string, int, error) {
|
||||
payload := map[string]interface{}{
|
||||
"model": modelName,
|
||||
"messages": []map[string]string{
|
||||
{"role": "system", "content": system},
|
||||
{"role": "user", "content": user},
|
||||
},
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 2000,
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
|
||||
endpoint := strings.TrimRight(baseURL, "/") + "/chat/completions"
|
||||
reqHTTP, err := http.NewRequest("POST", endpoint, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", http.StatusInternalServerError, err
|
||||
}
|
||||
reqHTTP.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
reqHTTP.Header.Set("Content-Type", "application/json")
|
||||
// Optional but recommended headers for OpenRouter
|
||||
if ref := strings.TrimSpace(getenv("OPENROUTER_SITE_URL")); ref != "" { reqHTTP.Header.Set("HTTP-Referer", ref) }
|
||||
if ttl := strings.TrimSpace(getenv("OPENROUTER_APP_NAME")); ttl != "" { reqHTTP.Header.Set("X-Title", ttl) }
|
||||
|
||||
client := &http.Client{Timeout: 45 * time.Second}
|
||||
resp, err := client.Do(reqHTTP)
|
||||
if err != nil {
|
||||
return "", http.StatusBadGateway, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
var e map[string]interface{}
|
||||
_ = json.NewDecoder(resp.Body).Decode(&e)
|
||||
return "", resp.StatusCode, fmt.Errorf("OpenRouter API error: %v", e)
|
||||
}
|
||||
|
||||
// OpenAI-compatible response
|
||||
var or struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&or); err != nil {
|
||||
return "", http.StatusBadGateway, err
|
||||
}
|
||||
if len(or.Choices) == 0 {
|
||||
return "", http.StatusBadGateway, fmt.Errorf("empty choices")
|
||||
}
|
||||
return strings.TrimSpace(or.Choices[0].Message.Content), http.StatusOK, nil
|
||||
}
|
||||
|
||||
// Try primary, then fallback
|
||||
content, _, err := callModel(model)
|
||||
if err != nil || strings.TrimSpace(content) == "" {
|
||||
// Attempt fallback model
|
||||
if fbContent, _, fbErr := callModel(fallbackModel); fbErr == nil && strings.TrimSpace(fbContent) != "" {
|
||||
content = fbContent
|
||||
} else {
|
||||
// Provide the primary error if available
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "OpenRouter selhal (včetně fallbacku)", "details": err.Error()})
|
||||
} else if fbErr != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "OpenRouter fallback selhal", "details": fbErr.Error()})
|
||||
} else {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "OpenRouter vrátil prázdnou odpověď"})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize and parse JSON returned by the model
|
||||
var out aiBlogResponse
|
||||
|
||||
// Clean up the response: remove markdown code blocks, backticks, etc.
|
||||
sanitized := sanitizeAIResponse(content)
|
||||
|
||||
// Try to parse the sanitized content
|
||||
if err := json.Unmarshal([]byte(sanitized), &out); err != nil {
|
||||
// Best-effort: try to find JSON block
|
||||
re := regexp.MustCompile(`(?s)\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||
if m := re.FindString(sanitized); m != "" {
|
||||
_ = json.Unmarshal([]byte(m), &out)
|
||||
}
|
||||
}
|
||||
|
||||
// Decode HTML entities in the html field
|
||||
if out.HTML != "" {
|
||||
out.HTML = html.UnescapeString(out.HTML)
|
||||
}
|
||||
|
||||
// Fallbacks if the model did not provide title/slug
|
||||
if out.Title == "" {
|
||||
out.Title = deriveTitle(req.Prompt)
|
||||
}
|
||||
// Validate slug: short, independent from title. If not valid, derive from prompt.
|
||||
if !isValidShortSlug(out.Slug) || out.Slug == slugify(out.Title) {
|
||||
out.Slug = shortSlugFromPrompt(req.Prompt)
|
||||
}
|
||||
if out.HTML == "" {
|
||||
// Wrap raw content as paragraph fallback
|
||||
out.HTML = "<h1>" + htmlEscape(out.Title) + "</h1><p>" + htmlEscape(content) + "</p>"
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// Helpers for OpenRouter config
|
||||
func getOpenRouterAPIKey() string {
|
||||
if v := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(getenv("OPENROUTER_API_KEY")), "\"")); v != "" {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getOpenRouterBaseURL() string {
|
||||
if v := strings.TrimSpace(getenv("OPENROUTER_BASE_URL")); v != "" {
|
||||
return v
|
||||
}
|
||||
return "https://openrouter.ai/api/v1"
|
||||
}
|
||||
|
||||
func getOpenRouterModel() string {
|
||||
if v := strings.TrimSpace(getenv("OPENROUTER_MODEL")); v != "" {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getOpenRouterFallbackModel() string {
|
||||
if v := strings.TrimSpace(getenv("OPENROUTER_FALLBACK_MODEL")); v != "" {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Small utility wrappers to avoid importing os directly multiple times
|
||||
func getenv(k string) string { return strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(os.Getenv(k)), "\r", ""), "\n", "")) }
|
||||
|
||||
// deriveTitle returns a readable title from user prompt
|
||||
func deriveTitle(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return "Novinky z klubu"
|
||||
}
|
||||
// Capitalize first letter, keep it concise
|
||||
if len(s) > 120 {
|
||||
s = s[:120]
|
||||
}
|
||||
return strings.ToUpper(string([]rune(s)[:1])) + s[1:]
|
||||
}
|
||||
|
||||
// slugify creates a URL-friendly slug without diacritics
|
||||
func slugify(s string) string {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
// Replace diacritics (basic map); for full support, consider x/text/unicode/norm and transform
|
||||
replacer := strings.NewReplacer(
|
||||
"á", "a", "č", "c", "ď", "d", "é", "e", "ě", "e", "í", "i", "ň", "n",
|
||||
"ó", "o", "ř", "r", "š", "s", "ť", "t", "ú", "u", "ů", "u", "ý", "y", "ž", "z",
|
||||
)
|
||||
s = replacer.Replace(s)
|
||||
// Replace any non alnum with hyphen
|
||||
re := regexp.MustCompile("[^a-z0-9]+")
|
||||
s = re.ReplaceAllString(s, "-")
|
||||
s = strings.Trim(s, "-")
|
||||
if s == "" {
|
||||
return "clanek"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// isValidShortSlug checks basic constraints: non-empty, <= 40 chars, 3-5 words (by hyphens), allowed charset
|
||||
func isValidShortSlug(s string) bool {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" { return false }
|
||||
if len(s) > 40 { return false }
|
||||
parts := strings.Split(s, "-")
|
||||
// filter empty parts
|
||||
w := 0
|
||||
for _, p := range parts { if p != "" { w++ } }
|
||||
if w < 3 || w > 5 { return false }
|
||||
// allowed chars: a-z0-9-
|
||||
re := regexp.MustCompile(`^[a-z0-9-]+$`)
|
||||
return re.MatchString(s)
|
||||
}
|
||||
|
||||
// shortSlugFromPrompt creates a compact, independent slug from the prompt text
|
||||
func shortSlugFromPrompt(prompt string) string {
|
||||
p := strings.ToLower(strings.TrimSpace(prompt))
|
||||
if p == "" { return "clanek" }
|
||||
// basic diacritics removal via slugify, then split to words
|
||||
p = slugify(p)
|
||||
parts := strings.Split(p, "-")
|
||||
// simple Czech stopwords list (subset)
|
||||
stop := map[string]struct{}{"a":{},"i":{},"v":{},"ve":{},"z":{},"za":{},"od":{},"do":{},"u":{},"o":{},"s":{},"se":{},"na":{},"po":{},"pod":{},"nad":{},"proti":{},"pri":{},"bez":{},"k":{},"ke":{},"ten":{},"ta":{},"to":{},"ty":{},"tento":{},"tato":{},"toto":{},"jak":{},"jako":{},"ze":{}}
|
||||
var kept []string
|
||||
for _, w := range parts {
|
||||
if w == "" { continue }
|
||||
if _, ok := stop[w]; ok { continue }
|
||||
kept = append(kept, w)
|
||||
if len(kept) >= 5 { break }
|
||||
}
|
||||
if len(kept) == 0 { kept = parts }
|
||||
// prefer 3-5 words, trim to 4 if too many
|
||||
if len(kept) > 5 { kept = kept[:5] }
|
||||
if len(kept) >= 4 { kept = kept[:4] }
|
||||
s := strings.Join(kept, "-")
|
||||
if len(s) > 40 { s = s[:40] }
|
||||
s = strings.Trim(s, "-")
|
||||
if !isValidShortSlug(s) {
|
||||
// final fallback
|
||||
s = slugify(deriveTitle(prompt))
|
||||
if len(s) > 40 { s = s[:40] }
|
||||
s = strings.Trim(s, "-")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func htmlEscape(s string) string {
|
||||
s = strings.ReplaceAll(s, "&", "&")
|
||||
s = strings.ReplaceAll(s, "<", "<")
|
||||
s = strings.ReplaceAll(s, ">", ">")
|
||||
s = strings.ReplaceAll(s, "\"", """)
|
||||
s = strings.ReplaceAll(s, "'", "'")
|
||||
return s
|
||||
}
|
||||
|
||||
// sanitizeAIResponse cleans up AI response to extract valid JSON
|
||||
// Handles markdown code blocks, extra backticks, and other formatting issues
|
||||
func sanitizeAIResponse(content string) string {
|
||||
// Trim whitespace
|
||||
content = strings.TrimSpace(content)
|
||||
|
||||
// Remove markdown code block markers (```json, ``json, `, etc.)
|
||||
// Handle various formats: ```json\n{...}\n```, ``json{...}``, `{...}`
|
||||
content = regexp.MustCompile(`^\s*`+"`"+`{1,3}\s*json\s*`).ReplaceAllString(content, "")
|
||||
content = regexp.MustCompile(`\s*`+"`"+`{1,3}\s*$`).ReplaceAllString(content, "")
|
||||
|
||||
// Remove any remaining backticks at start/end
|
||||
content = strings.Trim(content, "`")
|
||||
content = strings.TrimSpace(content)
|
||||
|
||||
return content
|
||||
}
|
||||
Reference in New Issue
Block a user