fix
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
package aiprompts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
reQuotedNameSlot = regexp.MustCompile(`"([^"]+)"`)
|
||||
reNameFormulaTail = regexp.MustCompile(`(?is)formuli\s*:\s*(.*?)(?:\.?\s*Ne uporabljaj|\.?$)`)
|
||||
)
|
||||
|
||||
// TitleFormulaElement is one ordered slot in categories.title_template.
|
||||
type TitleFormulaElement struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"` // "text" | "variable"
|
||||
Label string `json:"label,omitempty"`
|
||||
Value string `json:"value"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Example string `json:"example,omitempty"`
|
||||
}
|
||||
|
||||
// TitleFormula is the JSON shape stored in categories.title_template.
|
||||
type TitleFormula struct {
|
||||
Separator string `json:"separator"`
|
||||
Elements []TitleFormulaElement `json:"elements"`
|
||||
}
|
||||
|
||||
// DefaultRetailTitleFormula matches the dominant old A1/PHP <name> pattern:
|
||||
// product type + brand + full model (never brand alone).
|
||||
func DefaultRetailTitleFormula() TitleFormula {
|
||||
return TitleFormula{
|
||||
Separator: " ",
|
||||
Elements: []TitleFormulaElement{
|
||||
{ID: "0-variable-product_type", Type: "variable", Label: "Product type", Value: "product_type", Description: "Specific product type (sentence case)", Example: "Gaming monitor"},
|
||||
{ID: "1-variable-brand", Type: "variable", Label: "Brand", Value: "brand", Description: "Brand with correct capitalization", Example: "Samsung"},
|
||||
{ID: "2-variable-product_model", Type: "variable", Label: "Model", Value: "product_model", Description: "Full product model / ID", Example: "Odyssey G5"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TitleRoleInstruction is the shared Title-section body (CategoryEnhanceUserTemplate
|
||||
// and BuildCategoryEnhanceOverlay). Drives enhance JSON "name"; never brand-only.
|
||||
const TitleRoleInstruction = `Role: title. Build JSON "name": short retail title from Title formula + Attrs — never brand-only. Include product type and full model when evidence exists; follow any Title formula constraints that follow; use Attrs.`
|
||||
|
||||
// TitleTemplateIsBrandOnly reports stubs that only encode brand (the migrated A1
|
||||
// default) so enhance collapses to brand-only names.
|
||||
func TitleTemplateIsBrandOnly(template any) bool {
|
||||
els, ok := titleFormulaElements(template)
|
||||
if !ok || len(els) == 0 {
|
||||
return false
|
||||
}
|
||||
vars := 0
|
||||
brandOnly := true
|
||||
for _, el := range els {
|
||||
switch strings.ToLower(strings.TrimSpace(el.Type)) {
|
||||
case "variable":
|
||||
vars++
|
||||
v := strings.ToLower(strings.TrimSpace(el.Value))
|
||||
v = strings.ReplaceAll(v, "-", "_")
|
||||
if v != "brand" && v != "znamka" {
|
||||
brandOnly = false
|
||||
}
|
||||
case "text":
|
||||
// Free-form naming-rule blobs are not brand-only stubs.
|
||||
if strings.TrimSpace(el.Value) != "" {
|
||||
brandOnly = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return vars > 0 && brandOnly
|
||||
}
|
||||
|
||||
// TitleTemplateNeedsRepair is true when template is missing, unparseable, or
|
||||
// brand-only (insufficient vs old name/title rules).
|
||||
func TitleTemplateNeedsRepair(template any) bool {
|
||||
els, ok := titleFormulaElements(template)
|
||||
if !ok || len(els) == 0 {
|
||||
return true
|
||||
}
|
||||
return TitleTemplateIsBrandOnly(template)
|
||||
}
|
||||
|
||||
// DeriveTitleFormulaFromLegacyNameRules turns PHP/A1 <name> formula text
|
||||
// (e.g. "tip izdelka …", "znamka …", "poln model …") into a structured
|
||||
// title_template. Falls back to DefaultRetailTitleFormula when parsing yields
|
||||
// fewer than two variable slots.
|
||||
func DeriveTitleFormulaFromLegacyNameRules(titleRules string) TitleFormula {
|
||||
slots := extractLegacyNameSlots(titleRules)
|
||||
out := TitleFormula{Separator: " ", Elements: make([]TitleFormulaElement, 0, len(slots))}
|
||||
seen := map[string]struct{}{}
|
||||
for _, slot := range slots {
|
||||
el, ok := mapLegacyNameSlot(slot)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key := el.Type + ":" + strings.ToLower(el.Value)
|
||||
if _, dup := seen[key]; dup {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
el.ID = fmt.Sprintf("%d-%s-%s", len(out.Elements), el.Type, sanitizeFormulaID(el.Value))
|
||||
out.Elements = append(out.Elements, el)
|
||||
}
|
||||
if countFormulaVariables(out) < 2 {
|
||||
return ensureRetailTitleCoverage(out)
|
||||
}
|
||||
return ensureRetailTitleCoverage(out)
|
||||
}
|
||||
|
||||
// DeriveTitleTemplateJSON is the JSON encoding used when repairing categories.title_template.
|
||||
func DeriveTitleTemplateJSON(titleRules string) string {
|
||||
f := DeriveTitleFormulaFromLegacyNameRules(titleRules)
|
||||
b, err := json.Marshal(f)
|
||||
if err != nil {
|
||||
def := DefaultRetailTitleFormula()
|
||||
b, _ = json.Marshal(def)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func titleFormulaElements(template any) ([]TitleFormulaElement, bool) {
|
||||
if template == nil {
|
||||
return nil, false
|
||||
}
|
||||
switch t := template.(type) {
|
||||
case TitleFormula:
|
||||
return t.Elements, len(t.Elements) > 0
|
||||
case *TitleFormula:
|
||||
if t == nil {
|
||||
return nil, false
|
||||
}
|
||||
return t.Elements, len(t.Elements) > 0
|
||||
case []byte:
|
||||
if len(t) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
var f TitleFormula
|
||||
if err := json.Unmarshal(t, &f); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return f.Elements, len(f.Elements) > 0
|
||||
case string:
|
||||
s := strings.TrimSpace(t)
|
||||
if s == "" || s == "null" || s == "{}" {
|
||||
return nil, false
|
||||
}
|
||||
var f TitleFormula
|
||||
if err := json.Unmarshal([]byte(s), &f); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return f.Elements, len(f.Elements) > 0
|
||||
case map[string]any:
|
||||
b, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
var f TitleFormula
|
||||
if err := json.Unmarshal(b, &f); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return f.Elements, len(f.Elements) > 0
|
||||
default:
|
||||
b, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
var f TitleFormula
|
||||
if err := json.Unmarshal(b, &f); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return f.Elements, len(f.Elements) > 0
|
||||
}
|
||||
}
|
||||
|
||||
func extractLegacyNameSlots(rules string) []string {
|
||||
rules = strings.TrimSpace(rules)
|
||||
if rules == "" {
|
||||
return nil
|
||||
}
|
||||
body := rules
|
||||
if m := reNameFormulaTail.FindStringSubmatch(rules); len(m) == 2 {
|
||||
body = m[1]
|
||||
}
|
||||
raw := reQuotedNameSlot.FindAllStringSubmatch(body, -1)
|
||||
out := make([]string, 0, len(raw))
|
||||
for _, m := range raw {
|
||||
s := strings.TrimSpace(m[1])
|
||||
s = strings.Trim(s, `",. `)
|
||||
if s == "" || s == "," {
|
||||
continue
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mapLegacyNameSlot(slot string) (TitleFormulaElement, bool) {
|
||||
s := strings.TrimSpace(slot)
|
||||
if s == "" {
|
||||
return TitleFormulaElement{}, false
|
||||
}
|
||||
low := strings.ToLower(s)
|
||||
switch {
|
||||
case strings.Contains(low, "znamka"):
|
||||
return TitleFormulaElement{Type: "variable", Label: "Brand", Value: "brand", Description: s, Example: "Samsung"}, true
|
||||
case strings.Contains(low, "poln model"), strings.Contains(low, "model izdelka"),
|
||||
strings.HasPrefix(low, "model "), low == "model", strings.Contains(low, "model uppercase"):
|
||||
return TitleFormulaElement{Type: "variable", Label: "Model", Value: "product_model", Description: s, Example: "EHT6020"}, true
|
||||
case strings.Contains(low, "tip izdelka"):
|
||||
return TitleFormulaElement{Type: "variable", Label: "Product type", Value: "product_type", Description: s, Example: "Bluetooth speaker"}, true
|
||||
case strings.HasPrefix(low, "barva"):
|
||||
return TitleFormulaElement{Type: "variable", Label: "Color", Value: "barva", Description: s}, true
|
||||
case strings.Contains(low, "dimenzij"):
|
||||
return TitleFormulaElement{Type: "variable", Label: "Dimensions", Value: "dimensions", Description: s}, true
|
||||
case strings.Contains(low, "diagonala"):
|
||||
return TitleFormulaElement{Type: "variable", Label: "Diagonal", Value: "diagonala_zaslona", Description: s}, true
|
||||
case strings.Contains(low, "pomnilnik"):
|
||||
return TitleFormulaElement{Type: "variable", Label: "Memory", Value: "kapaciteta_ram_pomnilnika", Description: s}, true
|
||||
case strings.Contains(low, "procesor"):
|
||||
return TitleFormulaElement{Type: "variable", Label: "CPU", Value: "procesor", Description: s}, true
|
||||
case strings.Contains(low, "grafi"):
|
||||
return TitleFormulaElement{Type: "variable", Label: "GPU", Value: "graficna_kartica", Description: s}, true
|
||||
case strings.Contains(low, "kapaciteta"):
|
||||
return TitleFormulaElement{Type: "variable", Label: "Capacity", Value: "kapaciteta", Description: s}, true
|
||||
case strings.Contains(low, "skupina po te") || strings.Contains(low, "teži") || strings.Contains(low, "tezi"):
|
||||
return TitleFormulaElement{Type: "variable", Label: "Weight group", Value: "weight_group", Description: s}, true
|
||||
case strings.Contains(low, "priklju"):
|
||||
return TitleFormulaElement{Type: "variable", Label: "Connector", Value: "connector", Description: s}, true
|
||||
case strings.Contains(low, "osvež") || strings.Contains(low, "osvez") || strings.Contains(low, "hz"):
|
||||
return TitleFormulaElement{Type: "variable", Label: "Refresh rate", Value: "refresh_rate", Description: s}, true
|
||||
case strings.Contains(low, "lowercase") || strings.Contains(low, "sentence case") || strings.Contains(low, "uppercase"):
|
||||
// Category-specific type word ("cvrtnik lowercase", "monitor lowercase").
|
||||
return TitleFormulaElement{Type: "variable", Label: "Product type", Value: "product_type", Description: s}, true
|
||||
default:
|
||||
// Keep unrecognized instructional slots as text so order/intent survive.
|
||||
if len([]rune(s)) > 80 {
|
||||
s = string([]rune(s)[:80])
|
||||
}
|
||||
return TitleFormulaElement{Type: "text", Label: "Naming detail", Value: s, Description: "Legacy name-formula slot"}, true
|
||||
}
|
||||
}
|
||||
|
||||
func countFormulaVariables(f TitleFormula) int {
|
||||
n := 0
|
||||
for _, el := range f.Elements {
|
||||
if el.Type == "variable" {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func ensureRetailTitleCoverage(f TitleFormula) TitleFormula {
|
||||
has := map[string]bool{}
|
||||
for _, el := range f.Elements {
|
||||
if el.Type != "variable" {
|
||||
continue
|
||||
}
|
||||
k := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(el.Value), "-", "_"))
|
||||
has[k] = true
|
||||
}
|
||||
def := DefaultRetailTitleFormula()
|
||||
for _, el := range def.Elements {
|
||||
k := strings.ToLower(el.Value)
|
||||
if has[k] {
|
||||
continue
|
||||
}
|
||||
// Only inject core retail slots when missing.
|
||||
if k == "brand" || k == "product_model" || (k == "product_type" && countFormulaVariables(f) < 2) {
|
||||
el.ID = fmt.Sprintf("%d-%s-%s", len(f.Elements), el.Type, el.Value)
|
||||
f.Elements = append(f.Elements, el)
|
||||
has[k] = true
|
||||
}
|
||||
}
|
||||
if countFormulaVariables(f) < 2 {
|
||||
return def
|
||||
}
|
||||
if f.Separator == "" {
|
||||
f.Separator = " "
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func sanitizeFormulaID(v string) string {
|
||||
v = strings.ToLower(strings.TrimSpace(v))
|
||||
v = strings.ReplaceAll(v, " ", "_")
|
||||
v = strings.ReplaceAll(v, "-", "_")
|
||||
if v == "" {
|
||||
return "slot"
|
||||
}
|
||||
if len(v) > 40 {
|
||||
v = v[:40]
|
||||
}
|
||||
return v
|
||||
}
|
||||
Reference in New Issue
Block a user