2026-08-17 00:39:25 +02:00
package aiprompts
import (
"regexp"
"strings"
)
var (
reLegacyNameTag = regexp . MustCompile ( `(?is)<\s*name\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*name\s*>` )
reLegacyMetaTag = regexp . MustCompile ( `(?is)<\s*metaDescription\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*metaDescription\s*>` )
reLegacyDescPH = regexp . MustCompile ( `(?i)\{\s*""?\s*OPIS\s+IZDELKA\s*""?\s*\}` )
reLegacyNamePH = regexp . MustCompile ( `(?i)\{\s*""?\s*STARO\s+IME\s+IZDELKA\s*""?\s*\}` )
reDoubledQuotes = regexp . MustCompile ( `"{2,}` )
)
// LegacyEnhanceParts holds extracted role content from a combined PHP/A1 category Prompt.
type LegacyEnhanceParts struct {
TitleRules string
DescriptionRules string
MetaRules string
WasLegacy bool
}
// IsLegacyCombinedEnhancePrompt reports whether prompt looks like the old A1/PHP
// combined name+description(+meta) marketing blob (<name>/<metaDescription>/GPT predloga).
func IsLegacyCombinedEnhancePrompt ( prompt string ) bool {
p := strings . TrimSpace ( prompt )
if p == "" {
return false
}
// Require real tag wrappers — not instructional mentions like
// `do NOT … <name>/<metaDescription> tags` in CategoryEnhanceUserTemplate.
hasNameTag := reLegacyNameTag . MatchString ( p )
hasMetaTag := reLegacyMetaTag . MatchString ( p )
if hasNameTag && ( hasMetaTag || strings . Contains ( strings . ToLower ( p ), "gpt predloga" )) {
return true
}
if hasNameTag && ( strings . Contains ( p , "STARO IME IZDELKA" ) || strings . Contains ( p , "OPIS IZDELKA" )) {
return true
}
if strings . Contains ( p , "STARO IME IZDELKA" ) && strings . Contains ( p , "OPIS IZDELKA" ) && strings . Contains ( strings . ToLower ( p ), "gpt predloga" ) {
return true
}
return false
}
// ParseLegacyCombinedEnhancePrompt extracts title / description / meta instruction
// blobs from a legacy combined prompt. Placeholders are modernized to {{name}} /
// {{description}}. WasLegacy is false when no combined markers are found.
func ParseLegacyCombinedEnhancePrompt ( prompt string ) LegacyEnhanceParts {
raw := strings . TrimSpace ( prompt )
out := LegacyEnhanceParts {}
if raw == "" || ! IsLegacyCombinedEnhancePrompt ( raw ) {
return out
}
out . WasLegacy = true
if m := reLegacyNameTag . FindStringSubmatch ( raw ); len ( m ) == 2 {
out . TitleRules = cleanLegacyInstruction ( m [ 1 ])
}
if m := reLegacyMetaTag . FindStringSubmatch ( raw ); len ( m ) == 2 {
out . MetaRules = cleanLegacyInstruction ( m [ 1 ])
}
rest := reLegacyNameTag . ReplaceAllString ( raw , "" )
rest = reLegacyMetaTag . ReplaceAllString ( rest , "" )
if idx := strings . Index ( strings . ToLower ( rest ), "gpt predloga:" ); idx >= 0 {
rest = rest [ idx + len ( "gpt predloga:" ):]
}
// Drop Slovenian boilerplate intro lines that are not body structure.
rest = stripLegacyIntroNoise ( rest )
out . DescriptionRules = cleanLegacyInstruction ( rest )
return out
}
// SplitLegacyCombinedEnhancePrompt rewrites a legacy combined name+description(+meta)
// prompt into CategoryEnhanceUserTemplate role sections, preserving Slovenian
// naming / HTML / meta intent as category-specific rules under Title / Description / Meta.
// Non-legacy input returns CategoryEnhanceUserTemplate (canonical shared overlay).
func SplitLegacyCombinedEnhancePrompt ( prompt string ) string {
parts := ParseLegacyCombinedEnhancePrompt ( prompt )
if ! parts . WasLegacy {
return strings . TrimSpace ( CategoryEnhanceUserTemplate )
}
return BuildCategoryEnhanceOverlay ( parts )
}
// BuildCategoryEnhanceOverlay composes a role-sectioned enhance USER overlay from
// extracted legacy parts (or empty parts → canonical template text with no extra rules).
func BuildCategoryEnhanceOverlay ( parts LegacyEnhanceParts ) string {
var b strings . Builder
b . WriteString ( `Your reply is parsed as JSON {"name":"string","description":"string","meta_title":"string","meta_description":"string","attrs":{}} only (system schema). Write all string fields in {{ language }} (do not hardcode a language).` )
b . WriteString ( "\n\n" )
b . WriteString ( SectionTitleStart )
b . WriteByte ( '\n' )
b . WriteString ( TitleRoleInstruction )
b . WriteByte ( '\n' )
if rules := strings . TrimSpace ( parts . TitleRules ); rules != "" {
b . WriteString ( "Category naming rules (preserve intent): " )
b . WriteString ( rules )
b . WriteByte ( '\n' )
}
b . WriteString ( "Name: {{name}}\n" )
b . WriteString ( SectionTitleEnd )
b . WriteString ( "\n\n" )
b . WriteString ( SectionDescriptionStart )
b . WriteString ( "\nRole: description. Build JSON \"description\": product body HTML only (not SEO meta). When a Description formula follows, emit ONE HTML string covering each section in order (tags matching type: h1/h2/h3/h4, p, ul); otherwise prefer 1-3 factual paragraphs as ONE string with limited HTML (<h2><p><ul><li>) — do NOT emit a competing full HTML document or wrap the whole reply in <name>/<metaDescription> tags.\n" )
if rules := strings . TrimSpace ( parts . DescriptionRules ); rules != "" {
b . WriteString ( "Category HTML structure (preserve intent; emit as ONE description HTML string, not tagged name/meta blocks):\n" )
b . WriteString ( rules )
b . WriteByte ( '\n' )
}
b . WriteString ( "Description: {{description}}\n" )
b . WriteString ( SectionDescriptionEnd )
b . WriteString ( "\n\n" )
b . WriteString ( SectionMetaStart )
b . WriteString ( "\nRole: meta. Build JSON \"meta_title\" and \"meta_description\" as plain SEO text (never HTML). meta_title: 50-60 chars; meta_description: 120-155 chars; follow any SEO meta formula that follows; never copy the full description HTML into meta_description.\n" )
if rules := strings . TrimSpace ( parts . MetaRules ); rules != "" {
b . WriteString ( "Category SEO rules (preserve intent; plain text only): " )
b . WriteString ( rules )
b . WriteByte ( '\n' )
}
b . WriteString ( SectionMetaEnd )
b . WriteString ( "\n\n" )
2026-08-18 00:13:44 +02:00
b . WriteString ( "Product context:\n" )
2026-08-17 00:39:25 +02:00
b . WriteString ( "Category: {{category}}\n" )
2026-08-18 00:13:44 +02:00
b . WriteString ( "Attrs: {{attrs}}" )
2026-08-17 00:39:25 +02:00
return strings . TrimSpace ( b . String ())
}
func cleanLegacyInstruction ( s string ) string {
s = modernizeLegacyPlaceholders ( s )
s = reDoubledQuotes . ReplaceAllString ( s , `"` )
s = strings . ReplaceAll ( s , "\r\n" , "\n" )
s = strings . TrimSpace ( s )
// Collapse excessive blank lines.
for strings . Contains ( s , "\n\n\n" ) {
s = strings . ReplaceAll ( s , "\n\n\n" , "\n\n" )
}
return s
}
func modernizeLegacyPlaceholders ( prompt string ) string {
prompt = reLegacyDescPH . ReplaceAllString ( prompt , "{{description}}" )
prompt = reLegacyNamePH . ReplaceAllString ( prompt , "{{name}}" )
return prompt
}
func stripLegacyIntroNoise ( s string ) string {
lines := strings . Split ( s , "\n" )
out := make ([] string , 0 , len ( lines ))
for _ , line := range lines {
trim := strings . TrimSpace ( line )
lower := strings . ToLower ( trim )
switch {
case trim == "" :
if len ( out ) > 0 {
out = append ( out , "" )
}
case strings . HasPrefix ( lower , "ustvari nov opis" ),
strings . HasPrefix ( lower , "star_opis_izdelka" ),
strings . HasPrefix ( lower , "staro_ime_izdelka" ),
strings . HasPrefix ( lower , "uporabi spodnjo gpt" ),
strings . HasPrefix ( lower , "sledi tej gpt" ):
continue
default :
out = append ( out , trim )
}
}
return strings . TrimSpace ( strings . Join ( out , "\n" ))
}