Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

407 wiersze
12KB

  1. package mapping
  2. import (
  3. "fmt"
  4. "math"
  5. "regexp"
  6. "sort"
  7. "strconv"
  8. "strings"
  9. "qctextbuilder/internal/domain"
  10. )
  11. type SemanticSlotTarget struct {
  12. Slot string `json:"slot"`
  13. FieldPath string `json:"fieldPath"`
  14. FieldKey string `json:"fieldKey"`
  15. DisplayLabel string `json:"displayLabel,omitempty"`
  16. WebsiteSection string `json:"websiteSection,omitempty"`
  17. BlockID string `json:"blockId,omitempty"`
  18. Reason string `json:"reason,omitempty"`
  19. }
  20. type SemanticSlotMapping struct {
  21. Targets []SemanticSlotTarget `json:"targets"`
  22. BySlot map[string][]SemanticSlotTarget
  23. }
  24. var semanticBlockIDPattern = regexp.MustCompile(`(?i)(?:^|[_.])([mcr]\d{3,})(?:[_.]|$)`)
  25. var semanticLooseBlockIDPattern = regexp.MustCompile(`(?i)([mcr]\d{3,})`)
  26. var semanticIndexSuffixPattern = regexp.MustCompile(`_\d+$`)
  27. var semanticTrailingNumberPattern = regexp.MustCompile(`_(\d+)$`)
  28. func MapTemplateFieldsToSemanticSlots(fields []domain.TemplateField) SemanticSlotMapping {
  29. sectionGroupIndex := map[string]map[string]int{
  30. domain.WebsiteSectionServices: {},
  31. domain.WebsiteSectionServiceItem: {},
  32. domain.WebsiteSectionTeam: {},
  33. domain.WebsiteSectionTestimonials: {},
  34. }
  35. sectionGroupNext := map[string]int{
  36. domain.WebsiteSectionServices: 0,
  37. domain.WebsiteSectionServiceItem: 0,
  38. domain.WebsiteSectionTeam: 0,
  39. domain.WebsiteSectionTestimonials: 0,
  40. }
  41. repeatedIndexResolver := newSemanticRepeatedIndexResolver(fields)
  42. targets := make([]SemanticSlotTarget, 0)
  43. for _, field := range fields {
  44. if !field.IsEnabled || !strings.EqualFold(strings.TrimSpace(field.FieldKind), "text") {
  45. continue
  46. }
  47. blockID := semanticExtractBlockID(field)
  48. // Persisted slot (from AI or manual override) wins over rule-based logic.
  49. if storedSlot := strings.TrimSpace(field.SemanticSlot); storedSlot != "" {
  50. source := strings.TrimSpace(field.MappingSource)
  51. if source == "" {
  52. source = "stored"
  53. }
  54. targets = append(targets, SemanticSlotTarget{
  55. Slot: storedSlot,
  56. FieldPath: strings.TrimSpace(field.Path),
  57. FieldKey: strings.TrimSpace(field.KeyName),
  58. DisplayLabel: strings.TrimSpace(field.DisplayLabel),
  59. WebsiteSection: semanticSection(field),
  60. BlockID: blockID,
  61. Reason: "source=" + source,
  62. })
  63. continue
  64. }
  65. section := semanticSection(field)
  66. role := semanticRole(field)
  67. slot, mapped := semanticSlotForField(field, section, role, sectionGroupIndex, sectionGroupNext, repeatedIndexResolver)
  68. if !mapped {
  69. continue
  70. }
  71. reason := "rule: section=" + section + ", role=" + role
  72. if blockID != "" {
  73. reason += ", block=" + blockID
  74. }
  75. targets = append(targets, SemanticSlotTarget{
  76. Slot: slot,
  77. FieldPath: strings.TrimSpace(field.Path),
  78. FieldKey: strings.TrimSpace(field.KeyName),
  79. DisplayLabel: strings.TrimSpace(field.DisplayLabel),
  80. WebsiteSection: section,
  81. BlockID: blockID,
  82. Reason: reason,
  83. })
  84. }
  85. sort.SliceStable(targets, func(i, j int) bool {
  86. if targets[i].Slot == targets[j].Slot {
  87. return targets[i].FieldPath < targets[j].FieldPath
  88. }
  89. return targets[i].Slot < targets[j].Slot
  90. })
  91. bySlot := make(map[string][]SemanticSlotTarget, len(targets))
  92. for _, target := range targets {
  93. bySlot[target.Slot] = append(bySlot[target.Slot], target)
  94. }
  95. return SemanticSlotMapping{
  96. Targets: targets,
  97. BySlot: bySlot,
  98. }
  99. }
  100. func semanticSlotForField(
  101. field domain.TemplateField,
  102. section string,
  103. role string,
  104. sectionGroupIndex map[string]map[string]int,
  105. sectionGroupNext map[string]int,
  106. repeatedIndexResolver *semanticRepeatedIndexResolver,
  107. ) (string, bool) {
  108. switch section {
  109. case domain.WebsiteSectionHero:
  110. if role == "title" {
  111. return "hero.title", true
  112. }
  113. case domain.WebsiteSectionIntro:
  114. if role == "title" {
  115. return "intro.title", true
  116. }
  117. if role == "description" {
  118. return "intro.description", true
  119. }
  120. case domain.WebsiteSectionAbout:
  121. if role == "description" || role == "title" {
  122. return "about.description", true
  123. }
  124. case domain.WebsiteSectionServices, domain.WebsiteSectionServiceItem:
  125. if role == "title" || role == "description" {
  126. index := semanticRepeatedIndex(section, role, field, sectionGroupIndex, sectionGroupNext, repeatedIndexResolver)
  127. return fmt.Sprintf("service_items[%d].%s", index, role), true
  128. }
  129. case domain.WebsiteSectionTeam:
  130. if role == "name" || role == "description" {
  131. index := semanticRepeatedIndex(section, role, field, sectionGroupIndex, sectionGroupNext, repeatedIndexResolver)
  132. return fmt.Sprintf("team_items[%d].%s", index, role), true
  133. }
  134. case domain.WebsiteSectionTestimonials:
  135. if role == "title" || role == "description" || role == "name" {
  136. index := semanticRepeatedIndex(section, role, field, sectionGroupIndex, sectionGroupNext, repeatedIndexResolver)
  137. return fmt.Sprintf("testimonial_items[%d].%s", index, role), true
  138. }
  139. case domain.WebsiteSectionCTA:
  140. if role == "cta_text" || role == "title" || role == "description" {
  141. return "cta.text", true
  142. }
  143. }
  144. return "", false
  145. }
  146. func semanticRepeatedIndex(
  147. section string,
  148. role string,
  149. field domain.TemplateField,
  150. sectionGroupIndex map[string]map[string]int,
  151. sectionGroupNext map[string]int,
  152. repeatedIndexResolver *semanticRepeatedIndexResolver,
  153. ) int {
  154. if repeatedIndexResolver != nil {
  155. if idx, ok := repeatedIndexResolver.IndexFor(section, role, field); ok {
  156. return idx
  157. }
  158. }
  159. return semanticGroupIndex(section, field, sectionGroupIndex, sectionGroupNext)
  160. }
  161. func semanticGroupIndex(
  162. section string,
  163. field domain.TemplateField,
  164. sectionGroupIndex map[string]map[string]int,
  165. sectionGroupNext map[string]int,
  166. ) int {
  167. normalizedSection := domain.NormalizeWebsiteSection(section)
  168. group := semanticGroupKey(field)
  169. if _, ok := sectionGroupIndex[normalizedSection]; !ok {
  170. sectionGroupIndex[normalizedSection] = map[string]int{}
  171. }
  172. if idx, ok := sectionGroupIndex[normalizedSection][group]; ok {
  173. return idx
  174. }
  175. idx := sectionGroupNext[normalizedSection]
  176. sectionGroupNext[normalizedSection] = idx + 1
  177. sectionGroupIndex[normalizedSection][group] = idx
  178. return idx
  179. }
  180. func semanticGroupKey(field domain.TemplateField) string {
  181. if blockID := semanticExtractBlockID(field); blockID != "" {
  182. return "block:" + blockID
  183. }
  184. key := strings.ToLower(strings.TrimSpace(field.KeyName))
  185. if key != "" {
  186. return "key:" + semanticIndexSuffixPattern.ReplaceAllString(key, "")
  187. }
  188. path := strings.ToLower(strings.TrimSpace(field.Path))
  189. return "path:" + semanticIndexSuffixPattern.ReplaceAllString(path, "")
  190. }
  191. func semanticSection(field domain.TemplateField) string {
  192. websiteSection := domain.NormalizeWebsiteSection(field.WebsiteSection)
  193. if websiteSection != domain.WebsiteSectionOther {
  194. return websiteSection
  195. }
  196. return domain.SuggestWebsiteSection(field)
  197. }
  198. func semanticRole(field domain.TemplateField) string {
  199. parts := []string{
  200. strings.ToLower(strings.TrimSpace(field.KeyName)),
  201. strings.ToLower(strings.TrimSpace(field.Path)),
  202. strings.ToLower(strings.TrimSpace(field.DisplayLabel)),
  203. strings.ToLower(strings.TrimSpace(field.Section)),
  204. }
  205. combined := strings.Join(parts, " ")
  206. switch {
  207. case semanticContainsAny(combined, "description", "subtitle", "paragraph", "copy", "body", "content", "mission", "story", "bio", "quote"):
  208. return "description"
  209. case semanticContainsAny(combined, "button", "btn", "calltoaction", "call_to_action", "cta"):
  210. return "cta_text"
  211. case semanticContainsAny(combined, "headline", "heading", "title"):
  212. return "title"
  213. case semanticContainsAny(combined, "author", "customer", "person", "member", "name"):
  214. return "name"
  215. default:
  216. return "description"
  217. }
  218. }
  219. func semanticExtractBlockID(field domain.TemplateField) string {
  220. candidates := []string{
  221. strings.TrimSpace(field.KeyName),
  222. strings.TrimSpace(field.Path),
  223. strings.TrimSpace(field.DisplayLabel),
  224. }
  225. for _, candidate := range candidates {
  226. if candidate == "" {
  227. continue
  228. }
  229. if match := semanticBlockIDPattern.FindStringSubmatch(candidate); len(match) > 1 {
  230. return strings.ToLower(match[1])
  231. }
  232. }
  233. for _, candidate := range candidates {
  234. if candidate == "" {
  235. continue
  236. }
  237. if match := semanticLooseBlockIDPattern.FindStringSubmatch(candidate); len(match) > 1 {
  238. return strings.ToLower(match[1])
  239. }
  240. }
  241. return ""
  242. }
  243. func semanticContainsAny(value string, needles ...string) bool {
  244. for _, needle := range needles {
  245. if strings.Contains(value, needle) {
  246. return true
  247. }
  248. }
  249. return false
  250. }
  251. type semanticRepeatedIndexResolver struct {
  252. byFieldKey map[string]int
  253. }
  254. type semanticRepeatedField struct {
  255. fieldKey string
  256. suffix int
  257. path string
  258. }
  259. func newSemanticRepeatedIndexResolver(fields []domain.TemplateField) *semanticRepeatedIndexResolver {
  260. resolver := &semanticRepeatedIndexResolver{
  261. byFieldKey: map[string]int{},
  262. }
  263. // Pair repeated fields by section + block + role + numeric suffix ordering.
  264. buckets := map[string][]semanticRepeatedField{}
  265. for _, field := range fields {
  266. if !field.IsEnabled || !strings.EqualFold(strings.TrimSpace(field.FieldKind), "text") {
  267. continue
  268. }
  269. section := semanticSection(field)
  270. if !semanticIsRepeatedSection(section) {
  271. continue
  272. }
  273. role := semanticRole(field)
  274. if !semanticRoleAllowedForRepeated(section, role) {
  275. continue
  276. }
  277. suffix, ok := semanticTrailingNumber(field)
  278. if !ok {
  279. continue
  280. }
  281. bucket := semanticRepeatedBucketKey(section, semanticExtractBlockID(field), role)
  282. entry := semanticRepeatedField{
  283. fieldKey: semanticFieldIdentity(field),
  284. suffix: suffix,
  285. path: strings.TrimSpace(field.Path),
  286. }
  287. buckets[bucket] = append(buckets[bucket], entry)
  288. }
  289. for _, bucketEntries := range buckets {
  290. sort.SliceStable(bucketEntries, func(i, j int) bool {
  291. if bucketEntries[i].suffix != bucketEntries[j].suffix {
  292. return bucketEntries[i].suffix < bucketEntries[j].suffix
  293. }
  294. return bucketEntries[i].path < bucketEntries[j].path
  295. })
  296. for idx, entry := range bucketEntries {
  297. resolver.byFieldKey[entry.fieldKey] = idx
  298. }
  299. }
  300. return resolver
  301. }
  302. func (r *semanticRepeatedIndexResolver) IndexFor(section string, role string, field domain.TemplateField) (int, bool) {
  303. if r == nil || len(r.byFieldKey) == 0 {
  304. return 0, false
  305. }
  306. if !semanticIsRepeatedSection(section) || !semanticRoleAllowedForRepeated(section, role) {
  307. return 0, false
  308. }
  309. idx, ok := r.byFieldKey[semanticFieldIdentity(field)]
  310. return idx, ok
  311. }
  312. func semanticIsRepeatedSection(section string) bool {
  313. switch domain.NormalizeWebsiteSection(section) {
  314. case domain.WebsiteSectionServices, domain.WebsiteSectionServiceItem, domain.WebsiteSectionTeam, domain.WebsiteSectionTestimonials:
  315. return true
  316. default:
  317. return false
  318. }
  319. }
  320. func semanticRoleAllowedForRepeated(section string, role string) bool {
  321. switch domain.NormalizeWebsiteSection(section) {
  322. case domain.WebsiteSectionServices, domain.WebsiteSectionServiceItem:
  323. return role == "title" || role == "description"
  324. case domain.WebsiteSectionTeam:
  325. return role == "name" || role == "description"
  326. case domain.WebsiteSectionTestimonials:
  327. return role == "name" || role == "title" || role == "description"
  328. default:
  329. return false
  330. }
  331. }
  332. func semanticRepeatedBucketKey(section string, blockID string, role string) string {
  333. normalizedSection := domain.NormalizeWebsiteSection(section)
  334. if normalizedSection == domain.WebsiteSectionServiceItem {
  335. normalizedSection = domain.WebsiteSectionServices
  336. }
  337. block := strings.TrimSpace(strings.ToLower(blockID))
  338. if block == "" {
  339. block = "__no_block__"
  340. }
  341. return normalizedSection + "|" + block + "|" + role
  342. }
  343. func semanticTrailingNumber(field domain.TemplateField) (int, bool) {
  344. candidates := []string{
  345. strings.TrimSpace(strings.ToLower(field.Path)),
  346. strings.TrimSpace(strings.ToLower(field.KeyName)),
  347. }
  348. best := math.MaxInt
  349. found := false
  350. for _, candidate := range candidates {
  351. if candidate == "" {
  352. continue
  353. }
  354. match := semanticTrailingNumberPattern.FindStringSubmatch(candidate)
  355. if len(match) < 2 {
  356. continue
  357. }
  358. value, err := strconv.Atoi(match[1])
  359. if err != nil {
  360. continue
  361. }
  362. if value < best {
  363. best = value
  364. }
  365. found = true
  366. }
  367. if !found {
  368. return 0, false
  369. }
  370. return best, true
  371. }
  372. func semanticFieldIdentity(field domain.TemplateField) string {
  373. return strings.ToLower(strings.TrimSpace(field.Path)) + "|" + strings.ToLower(strings.TrimSpace(field.KeyName))
  374. }