package aiprompts import ( "encoding/json" "regexp" "strings" ) var ( reLegacyHTMLBlock = regexp.MustCompile(`(?is)<\s*(h[1-4]|p|ul|ol|li|b|strong)\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*(?:h[1-4]|p|ul|ol|li|b|strong)\s*>`) reLegacyBoldUL = regexp.MustCompile(`(?is)<\s*b\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*b\s*>\s*<\s*ul\s*>\s*<\s*li\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*li\s*>\s*<\s*/\s*ul\s*>`) ) // LegacyHTMLFormulaPresent reports whether s contains A1-style HTML formula blocks // (

{…}

,

{…}

, …) used in category Description GPT predloga. func LegacyHTMLFormulaPresent(s string) bool { return reLegacyHTMLBlock.MatchString(s) } // DescriptionFormulaSection is one ordered block in categories.description_template. type DescriptionFormulaSection struct { Type string `json:"type"` Instructions string `json:"instructions"` } // DescriptionFormula is the JSON shape stored in categories.description_template // (sections + optional SEO meta instruction strings). type DescriptionFormula struct { Sections []DescriptionFormulaSection `json:"sections,omitempty"` MetaTitle string `json:"metaTitle,omitempty"` MetaDescription string `json:"metaDescription,omitempty"` } // DescriptionTemplateNeedsRepair is true when template is missing or has no sections // (and no usable meta instructions). func DescriptionTemplateNeedsRepair(template any) bool { f, ok := parseDescriptionFormula(template) if !ok { return true } if len(f.Sections) > 0 { return false } return strings.TrimSpace(f.MetaTitle) == "" && strings.TrimSpace(f.MetaDescription) == "" } // DefaultLegacyMetaTitleRule fills description_template.metaTitle for legacy A1 // prompts, which only carried a instruction. Mirrors the legacy // name-first Slovenian instruction style with the SEO title length bound. const DefaultLegacyMetaTitleRule = "Najprej napiši Novo ime izdelka in dodaj glavno prednost. Dolžina 50-60 znakov vključno s presledki." // DeriveDescriptionFormulaFromLegacyParts builds description_template from split // legacy DescriptionRules HTML + MetaRules. Legacy dumps never carried a meta // title rule, so meta intent (MetaRules present) also seeds a default metaTitle // instruction — the category UI then shows all four prompt areas filled. func DeriveDescriptionFormulaFromLegacyParts(parts LegacyEnhanceParts) DescriptionFormula { out := DescriptionFormula{ MetaDescription: strings.TrimSpace(parts.MetaRules), } if out.MetaDescription != "" { out.MetaTitle = DefaultLegacyMetaTitleRule } body := strings.TrimSpace(parts.DescriptionRules) if body == "" { return out } // Walk left-to-right so section order matches the legacy HTML template. remaining := body for remaining != "" { boldUL := reLegacyBoldUL.FindStringSubmatchIndex(remaining) block := reLegacyHTMLBlock.FindStringSubmatchIndex(remaining) switch { case boldUL != nil && (block == nil || boldUL[0] <= block[0]): m := reLegacyBoldUL.FindStringSubmatch(remaining[boldUL[0]:boldUL[1]]) heading, instr := "", "" if len(m) >= 3 { heading = cleanLegacyInstruction(m[1]) instr = cleanLegacyInstruction(m[2]) } if heading != "" && instr != "" && !strings.Contains(strings.ToLower(instr), strings.ToLower(heading)) { instr = heading + ": " + instr } else if instr == "" { instr = heading } if strings.TrimSpace(instr) != "" { out.Sections = append(out.Sections, DescriptionFormulaSection{Type: "ul", Instructions: instr}) } remaining = remaining[boldUL[1]:] case block != nil: m := reLegacyHTMLBlock.FindStringSubmatch(remaining[block[0]:block[1]]) if len(m) >= 3 { typ := strings.ToLower(strings.TrimSpace(m[1])) instr := cleanLegacyInstruction(m[2]) if instr != "" { switch typ { case "b", "strong": typ = "h2" case "li", "ol": typ = "ul" } out.Sections = append(out.Sections, DescriptionFormulaSection{Type: typ, Instructions: instr}) } } remaining = remaining[block[1]:] default: return out } } return out } // DeriveDescriptionTemplateJSON encodes DeriveDescriptionFormulaFromLegacyParts for DB write. func DeriveDescriptionTemplateJSON(parts LegacyEnhanceParts) string { f := DeriveDescriptionFormulaFromLegacyParts(parts) if len(f.Sections) == 0 && strings.TrimSpace(f.MetaTitle) == "" && strings.TrimSpace(f.MetaDescription) == "" { return "" } b, err := json.Marshal(f) if err != nil { return "" } return string(b) } func parseDescriptionFormula(template any) (DescriptionFormula, bool) { if template == nil { return DescriptionFormula{}, false } switch t := template.(type) { case DescriptionFormula: return t, true case *DescriptionFormula: if t == nil { return DescriptionFormula{}, false } return *t, true case []byte: if len(t) == 0 { return DescriptionFormula{}, false } var f DescriptionFormula if err := json.Unmarshal(t, &f); err != nil { return DescriptionFormula{}, false } return f, true case string: s := strings.TrimSpace(t) if s == "" || s == "null" || s == "{}" { return DescriptionFormula{}, false } var f DescriptionFormula if err := json.Unmarshal([]byte(s), &f); err != nil { return DescriptionFormula{}, false } return f, true default: b, err := json.Marshal(t) if err != nil { return DescriptionFormula{}, false } var f DescriptionFormula if err := json.Unmarshal(b, &f); err != nil { return DescriptionFormula{}, false } return f, true } } // EffectiveDescriptionFormula merges structured description_template with the // category enhance prompt Title/Description/Meta role sections. // // Precedence for HTML body sections: // 1. Prompt Description HTML overlay when it yields more sections than stored // (tenants author structure in /categories/.../prompt Section instructions). // 2. Otherwise stored description_template.sections. // // Meta instructions: stored metaTitle/metaDescription win; else --- Meta --- body. func EffectiveDescriptionFormula(stored any, categoryPrompt string) DescriptionFormula { storedF, storedOK := parseDescriptionFormula(stored) out := DescriptionFormula{} if storedOK { out = storedF } descBody := ExtractEnhanceSectionBody(categoryPrompt, SectionDescriptionStart, SectionDescriptionEnd) if descBody == "" && !promptHasRoleMarkers(categoryPrompt) { // Unsectioned prompt may still be a legacy HTML blob. descBody = strings.TrimSpace(categoryPrompt) } if isBuiltInDescriptionRoleBoilerplate(descBody) { descBody = "" } var derived DescriptionFormula if descBody != "" && reLegacyHTMLBlock.MatchString(descBody) { derived = DeriveDescriptionFormulaFromLegacyParts(LegacyEnhanceParts{ DescriptionRules: descBody, WasLegacy: true, }) } if len(derived.Sections) > len(out.Sections) { out.Sections = derived.Sections } metaBody := ExtractEnhanceSectionBody(categoryPrompt, SectionMetaStart, SectionMetaEnd) if isBuiltInMetaRoleBoilerplate(metaBody) { metaBody = "" } if strings.TrimSpace(out.MetaTitle) == "" && strings.TrimSpace(out.MetaDescription) == "" && strings.TrimSpace(metaBody) != "" { out.MetaDescription = strings.TrimSpace(metaBody) out.MetaTitle = DefaultLegacyMetaTitleRule } else { if strings.TrimSpace(out.MetaTitle) == "" && strings.TrimSpace(derived.MetaTitle) != "" { out.MetaTitle = derived.MetaTitle } if strings.TrimSpace(out.MetaDescription) == "" && strings.TrimSpace(derived.MetaDescription) != "" { out.MetaDescription = derived.MetaDescription } } return out } func isBuiltInMetaRoleBoilerplate(body string) bool { lower := strings.ToLower(strings.TrimSpace(body)) return strings.HasPrefix(lower, "role: meta") } func isBuiltInDescriptionRoleBoilerplate(body string) bool { lower := strings.ToLower(strings.TrimSpace(body)) return strings.HasPrefix(lower, "role: description") } func promptHasRoleMarkers(prompt string) bool { lower := strings.ToLower(prompt) return strings.Contains(lower, strings.ToLower(SectionTitleStart)) || strings.Contains(lower, strings.ToLower(SectionDescriptionStart)) || strings.Contains(lower, strings.ToLower(SectionMetaStart)) } // TitleSectionInstructions returns the --- Title --- body from a category prompt. func TitleSectionInstructions(categoryPrompt string) string { return ExtractEnhanceSectionBody(categoryPrompt, SectionTitleStart, SectionTitleEnd) } // DescriptionSectionInstructions returns the --- Description --- body, or empty // when it is only the built-in Role: description boilerplate. func DescriptionSectionInstructions(categoryPrompt string) string { body := ExtractEnhanceSectionBody(categoryPrompt, SectionDescriptionStart, SectionDescriptionEnd) if isBuiltInDescriptionRoleBoilerplate(body) { return "" } return body } // MetaSectionInstructions returns the --- Meta --- body, or empty when it is only // the built-in Role: meta boilerplate. func MetaSectionInstructions(categoryPrompt string) string { body := ExtractEnhanceSectionBody(categoryPrompt, SectionMetaStart, SectionMetaEnd) if isBuiltInMetaRoleBoilerplate(body) { return "" } return body } // EffectiveDescriptionTemplateAny is EffectiveDescriptionFormula as a JSON object // for processing.ProductInput.DescriptionTemplate (nil when empty). func EffectiveDescriptionTemplateAny(stored any, categoryPrompt string) any { f := EffectiveDescriptionFormula(stored, categoryPrompt) if len(f.Sections) == 0 && strings.TrimSpace(f.MetaTitle) == "" && strings.TrimSpace(f.MetaDescription) == "" { return nil } b, err := json.Marshal(f) if err != nil { return nil } var out map[string]any if err := json.Unmarshal(b, &out); err != nil { return nil } return out } // CategoryTitlePromptRequiresRewrite reports whether the Title role instructs a // new retail name (not keep/echo supplier title). Used to reject "Matched" copy-paste. func CategoryTitlePromptRequiresRewrite(categoryPrompt string) bool { body := TitleSectionInstructions(categoryPrompt) if body == "" { body = strings.TrimSpace(categoryPrompt) } if body == "" { return false } // Built-in CategoryEnhanceUserTemplate Title role mentions "Title formula" // generically — that is not a tenant rewrite formula. if strings.HasPrefix(strings.ToLower(strings.TrimSpace(body)), "role: title") { return false } lower := strings.ToLower(body) cues := []string{ "napiši novo ime", "napisi novo ime", "novo ime izdelka", "po formuli", "write a new", "new product name", "rename", "sentence case", "tip izdelka", "product type", } for _, c := range cues { if strings.Contains(lower, c) { return true } } return false }