mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
dev day #69
This commit is contained in:
@@ -20,6 +20,96 @@ type AIController struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// GenerateCSS creates scoped CSS for a page element
|
||||
func (ac *AIController) GenerateCSS(c *gin.Context) {
|
||||
var req aiCSSRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
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" }
|
||||
|
||||
rootSelector := strings.TrimSpace(req.RootSelector)
|
||||
if rootSelector == "" {
|
||||
en := strings.TrimSpace(req.ElementName)
|
||||
if en == "" { en = "element" }
|
||||
rootSelector = fmt.Sprintf("[data-element=\"%s\"]", en)
|
||||
}
|
||||
|
||||
themeJSON, _ := json.Marshal(req.Theme)
|
||||
stylesJSON, _ := json.Marshal(req.CurrentStyles)
|
||||
|
||||
system := "Jsi zkušený CSS návrhář pro klubové weby. Piš čistý, přístupný a responzivní CSS. VÝSTUP POUZE JSON: {\"css\":\"...\"}. Nepoužívej reset, neovlivňuj globální prvky. CSS MUSÍ být scope-nuté POUZE pod kořenový selektor, žádný selektor mimo. Používej CSS proměnné (např. --club-primary, --club-secondary). Čeština není nutná v kódu, ale požadavky jsou v češtině."
|
||||
user := fmt.Sprintf("Požadavek: %s\nKořenový selektor: %s\nAktuální CSS (může být prázdné):\n---\n%s\n---\nAktuální styly (JSON): %s\nTéma (JSON): %s\nBreakpoints: %v\nPožadavky: 1) Scope pouze pod kořenový selektor. 2) Žádné !important. 3) Media queries pro mobil/tablet/desktop dle potřeby. 4) Zaměř se na vzhled prvků uvnitř bloku. 5) Nepřidávej inline styly ani globální sel. 6) Používej proměnné, zachovej kontrast a čitelnost.",
|
||||
strings.TrimSpace(req.Prompt), rootSelector, strings.TrimSpace(req.CurrentCSS), string(stylesJSON), string(themeJSON), req.Breakpoints)
|
||||
|
||||
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.3,
|
||||
"max_tokens": 1200,
|
||||
}
|
||||
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()}); return }
|
||||
if fbErr != nil { c.JSON(http.StatusBadGateway, gin.H{"error": "OpenRouter fallback selhal", "details": fbErr.Error()}); return }
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "OpenRouter vrátil prázdnou odpověď"}); return
|
||||
}
|
||||
}
|
||||
|
||||
sanitized := sanitizeAIResponse(content)
|
||||
var out aiCSSResponse
|
||||
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 strings.TrimSpace(out.CSS) == "" {
|
||||
out.CSS = fmt.Sprintf("%s { }", rootSelector)
|
||||
}
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// GenerateAboutPage creates about page content using the OpenRouter API
|
||||
func (ac *AIController) GenerateAboutPage(c *gin.Context) {
|
||||
var req aiAboutRequest
|
||||
@@ -194,6 +284,20 @@ type aiAboutResponse struct {
|
||||
SEODescription string `json:"seo_description"`
|
||||
}
|
||||
|
||||
type aiCSSRequest struct {
|
||||
Prompt string `json:"prompt" binding:"required"`
|
||||
ElementName string `json:"element_name"`
|
||||
RootSelector string `json:"root_selector"`
|
||||
CurrentCSS string `json:"current_css"`
|
||||
CurrentStyles map[string]interface{} `json:"current_styles"`
|
||||
Theme map[string]string `json:"theme"`
|
||||
Breakpoints []int `json:"breakpoints"`
|
||||
}
|
||||
|
||||
type aiCSSResponse struct {
|
||||
CSS string `json:"css"`
|
||||
}
|
||||
|
||||
// GenerateBlog creates a blog article using the OpenRouter API (with Mistral models)
|
||||
func (ac *AIController) GenerateBlog(c *gin.Context) {
|
||||
var req aiBlogRequest
|
||||
|
||||
Reference in New Issue
Block a user