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.

263 wiersze
7.0KB

  1. package onboarding
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "regexp"
  7. "sort"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "qctextbuilder/internal/domain"
  12. "qctextbuilder/internal/qcclient"
  13. "qctextbuilder/internal/store"
  14. )
  15. var imagePlaceholderPattern = regexp.MustCompile(`^\[\s*(image|img|photo|picture)(\s+(image|img|photo|picture))*\s*\]$`)
  16. var imageLikePathHints = []string{
  17. "image",
  18. "img",
  19. "photo",
  20. "picture",
  21. "thumbnail",
  22. "gallery",
  23. "logo",
  24. "icon",
  25. "avatar",
  26. "background",
  27. "banner",
  28. }
  29. type Service struct {
  30. qc qcclient.Client
  31. templateStore store.TemplateStore
  32. manifestStore store.ManifestStore
  33. }
  34. type FieldPatch struct {
  35. Path string
  36. IsEnabled *bool
  37. IsRequiredByUs *bool
  38. DisplayLabel *string
  39. DisplayOrder *int
  40. Notes *string
  41. }
  42. func New(qc qcclient.Client, templateStore store.TemplateStore, manifestStore store.ManifestStore) *Service {
  43. return &Service{
  44. qc: qc,
  45. templateStore: templateStore,
  46. manifestStore: manifestStore,
  47. }
  48. }
  49. func (s *Service) OnboardTemplate(ctx context.Context, templateID int64) (*domain.TemplateManifest, []domain.TemplateField, error) {
  50. template, err := s.templateStore.GetTemplateByID(ctx, templateID)
  51. if err != nil {
  52. return nil, nil, fmt.Errorf("get template: %w", err)
  53. }
  54. if !template.IsAITemplate {
  55. return nil, nil, fmt.Errorf("invalid template type: only ai template is allowed")
  56. }
  57. req := defaultDiscoveryRequest(templateID)
  58. data, raw, err := s.qc.GenerateContent(ctx, req)
  59. if err != nil {
  60. return nil, nil, fmt.Errorf("generate discovery content: %w", err)
  61. }
  62. manifestID := strconv.FormatInt(time.Now().UnixNano(), 10)
  63. now := time.Now().UTC()
  64. fields := flattenDiscovery(templateID, manifestID, data)
  65. flattened, _ := json.Marshal(fields)
  66. reqRaw, _ := json.Marshal(req)
  67. manifest := domain.TemplateManifest{
  68. ID: manifestID,
  69. TemplateID: templateID,
  70. Version: 1,
  71. Source: "generate-content",
  72. LanguageUsedDiscovery: "EN",
  73. DiscoveryPayloadJSON: reqRaw,
  74. DiscoveryResponseJSON: raw,
  75. FlattenedManifestJSON: flattened,
  76. IsActive: true,
  77. CreatedAt: now,
  78. UpdatedAt: now,
  79. }
  80. if err := s.manifestStore.CreateManifest(ctx, manifest, fields); err != nil {
  81. return nil, nil, fmt.Errorf("save manifest: %w", err)
  82. }
  83. if err := s.templateStore.SetTemplateManifestStatus(ctx, templateID, "reviewed", true); err != nil {
  84. return nil, nil, fmt.Errorf("update template status: %w", err)
  85. }
  86. return &manifest, fields, nil
  87. }
  88. func (s *Service) UpdateTemplateFields(ctx context.Context, templateID int64, manifestID string, patches []FieldPatch) (*domain.TemplateManifest, []domain.TemplateField, error) {
  89. template, err := s.templateStore.GetTemplateByID(ctx, templateID)
  90. if err != nil {
  91. return nil, nil, fmt.Errorf("get template: %w", err)
  92. }
  93. if !template.IsAITemplate {
  94. return nil, nil, fmt.Errorf("invalid template type: only ai template is allowed")
  95. }
  96. manifest, err := s.manifestStore.GetActiveManifestByTemplateID(ctx, templateID)
  97. if err != nil {
  98. return nil, nil, fmt.Errorf("get active manifest: %w", err)
  99. }
  100. if strings.TrimSpace(manifestID) != "" && manifest.ID != manifestID {
  101. return nil, nil, fmt.Errorf("manifest mismatch: active=%s requested=%s", manifest.ID, manifestID)
  102. }
  103. fields, err := s.manifestStore.ListFieldsByManifestID(ctx, manifest.ID)
  104. if err != nil {
  105. return nil, nil, fmt.Errorf("list fields: %w", err)
  106. }
  107. byPath := make(map[string]int, len(fields))
  108. for i := range fields {
  109. byPath[fields[i].Path] = i
  110. }
  111. for _, patch := range patches {
  112. path := strings.TrimSpace(patch.Path)
  113. if path == "" {
  114. return nil, nil, fmt.Errorf("field patch path is required")
  115. }
  116. idx, ok := byPath[path]
  117. if !ok {
  118. return nil, nil, fmt.Errorf("unknown field path: %s", path)
  119. }
  120. if patch.IsEnabled != nil {
  121. if *patch.IsEnabled && fields[idx].FieldKind != "text" {
  122. return nil, nil, fmt.Errorf("field %s cannot be enabled for kind %s", path, fields[idx].FieldKind)
  123. }
  124. fields[idx].IsEnabled = *patch.IsEnabled
  125. }
  126. if patch.IsRequiredByUs != nil {
  127. fields[idx].IsRequiredByUs = *patch.IsRequiredByUs
  128. }
  129. if patch.DisplayLabel != nil {
  130. fields[idx].DisplayLabel = strings.TrimSpace(*patch.DisplayLabel)
  131. }
  132. if patch.DisplayOrder != nil {
  133. fields[idx].DisplayOrder = *patch.DisplayOrder
  134. }
  135. if patch.Notes != nil {
  136. fields[idx].Notes = strings.TrimSpace(*patch.Notes)
  137. }
  138. }
  139. if err := s.manifestStore.UpdateFields(ctx, manifest.ID, fields); err != nil {
  140. return nil, nil, fmt.Errorf("update fields: %w", err)
  141. }
  142. sort.Slice(fields, func(i, j int) bool {
  143. if fields[i].DisplayOrder == fields[j].DisplayOrder {
  144. return fields[i].Path < fields[j].Path
  145. }
  146. return fields[i].DisplayOrder < fields[j].DisplayOrder
  147. })
  148. return manifest, fields, nil
  149. }
  150. func defaultDiscoveryRequest(templateID int64) qcclient.GenerateContentRequest {
  151. return qcclient.GenerateContentRequest{
  152. TemplateID: templateID,
  153. GlobalData: map[string]any{
  154. "companyName": "Discovery Company",
  155. "businessType": "dentist",
  156. "siteLanguage": "EN",
  157. "email": "discovery@example.com",
  158. "phone": "+41 44 000 00 00",
  159. "address": map[string]any{
  160. "line1": "Discovery Street 1",
  161. "line2": "",
  162. "city": "Zurich",
  163. "region": "ZH",
  164. "postalCode": "8000",
  165. "country": "CH",
  166. },
  167. },
  168. Empty: false,
  169. ToneOfVoice: "Professional",
  170. TargetAudience: "B2B",
  171. }
  172. }
  173. func flattenDiscovery(templateID int64, manifestID string, data qcclient.GenerateContentData) []domain.TemplateField {
  174. fields := make([]domain.TemplateField, 0)
  175. order := 0
  176. for section, kv := range data {
  177. for key, value := range kv {
  178. sample := fmt.Sprint(value)
  179. path := section + "." + key
  180. kind := detectFieldKind(path, sample)
  181. enabled := kind == "text"
  182. fields = append(fields, domain.TemplateField{
  183. ID: fmt.Sprintf("%s-%d", manifestID, order+1),
  184. TemplateID: templateID,
  185. ManifestID: manifestID,
  186. Section: section,
  187. KeyName: key,
  188. Path: path,
  189. FieldKind: kind,
  190. SampleValue: sample,
  191. IsEnabled: enabled,
  192. DisplayLabel: path,
  193. DisplayOrder: order,
  194. Notes: "",
  195. })
  196. order++
  197. }
  198. }
  199. return fields
  200. }
  201. func detectFieldKind(path string, sample string) string {
  202. sampleTrim := strings.TrimSpace(sample)
  203. if sampleTrim == "" {
  204. return "unknown"
  205. }
  206. if strings.EqualFold(sampleTrim, "#IMAGE#") {
  207. return "image"
  208. }
  209. if isLikelyImagePlaceholder(sampleTrim) {
  210. return "image"
  211. }
  212. if isLikelyImagePath(path) {
  213. return "image"
  214. }
  215. return "text"
  216. }
  217. func isLikelyImagePlaceholder(sample string) bool {
  218. normalized := strings.ToLower(strings.TrimSpace(sample))
  219. if imagePlaceholderPattern.MatchString(normalized) {
  220. return true
  221. }
  222. if normalized == "image" || normalized == "img" || normalized == "photo" || normalized == "picture" {
  223. return true
  224. }
  225. return strings.Contains(normalized, " image image ")
  226. }
  227. func isLikelyImagePath(path string) bool {
  228. normalized := strings.ToLower(strings.TrimSpace(path))
  229. for _, hint := range imageLikePathHints {
  230. if strings.Contains(normalized, hint) {
  231. return true
  232. }
  233. }
  234. return false
  235. }