Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

638 linhas
20KB

  1. package mapping
  2. import (
  3. "context"
  4. "fmt"
  5. "sort"
  6. "strings"
  7. "time"
  8. "qctextbuilder/internal/domain"
  9. )
  10. type SuggestionRequest struct {
  11. TemplateID int64
  12. DraftID string
  13. Fields []domain.TemplateField
  14. GlobalData map[string]any
  15. DraftContext *domain.DraftContext
  16. MasterPrompt string
  17. PromptBlocks []domain.PromptBlockConfig
  18. Existing map[string]string
  19. IncludeFilled bool
  20. }
  21. type Suggestion struct {
  22. FieldPath string `json:"fieldPath"`
  23. Slot string `json:"slot,omitempty"`
  24. Value string `json:"value"`
  25. Reason string `json:"reason,omitempty"`
  26. Source string `json:"source,omitempty"`
  27. }
  28. type SuggestionResult struct {
  29. Suggestions []Suggestion `json:"suggestions"`
  30. ByFieldPath map[string]Suggestion `json:"byFieldPath"`
  31. }
  32. func SuggestFieldValues(req SuggestionRequest) SuggestionResult {
  33. return SuggestFieldValuesRuleBased(req)
  34. }
  35. func SuggestFieldValuesRuleBased(req SuggestionRequest) SuggestionResult {
  36. existing := req.Existing
  37. if existing == nil {
  38. existing = map[string]string{}
  39. }
  40. mappingResult := MapTemplateFieldsToSemanticSlots(req.Fields)
  41. ctx := suggestionContextFrom(req.GlobalData, req.DraftContext)
  42. out := SuggestionResult{
  43. Suggestions: make([]Suggestion, 0),
  44. ByFieldPath: map[string]Suggestion{},
  45. }
  46. seen := map[string]struct{}{}
  47. targets := append([]SemanticSlotTarget(nil), mappingResult.Targets...)
  48. sort.SliceStable(targets, func(i, j int) bool {
  49. if targets[i].FieldPath == targets[j].FieldPath {
  50. return targets[i].Slot < targets[j].Slot
  51. }
  52. return targets[i].FieldPath < targets[j].FieldPath
  53. })
  54. for _, target := range targets {
  55. if _, ok := seen[target.FieldPath]; ok {
  56. continue
  57. }
  58. if !req.IncludeFilled && strings.TrimSpace(existing[target.FieldPath]) != "" {
  59. continue
  60. }
  61. value, reason, ok := suggestValueForSlot(target.Slot, ctx)
  62. if !ok || strings.TrimSpace(value) == "" {
  63. continue
  64. }
  65. suggestion := Suggestion{
  66. FieldPath: target.FieldPath,
  67. Slot: target.Slot,
  68. Value: value,
  69. Reason: reason,
  70. Source: domain.DraftSuggestionSourceFallbackRuleBased,
  71. }
  72. out.Suggestions = append(out.Suggestions, suggestion)
  73. out.ByFieldPath[target.FieldPath] = suggestion
  74. seen[target.FieldPath] = struct{}{}
  75. }
  76. return out
  77. }
  78. type suggestionContext struct {
  79. CompanyName string
  80. BusinessType string
  81. WebsiteSummary string
  82. LocaleStyle string
  83. MarketStyle string
  84. AddressMode string
  85. ContentTone string
  86. PromptNote string
  87. DescriptionShort string
  88. DescriptionLong string
  89. Mission string
  90. }
  91. func suggestionContextFrom(globalData map[string]any, draftContext *domain.DraftContext) suggestionContext {
  92. ctx := suggestionContext{
  93. CompanyName: getMapString(globalData, "companyName"),
  94. BusinessType: getMapString(globalData, "businessType"),
  95. DescriptionShort: getMapString(globalData, "descriptionShort"),
  96. DescriptionLong: getMapString(globalData, "descriptionLong"),
  97. Mission: getMapString(globalData, "mission"),
  98. }
  99. if draftContext == nil {
  100. return ctx
  101. }
  102. if strings.TrimSpace(ctx.BusinessType) == "" {
  103. ctx.BusinessType = strings.TrimSpace(draftContext.LLM.BusinessType)
  104. }
  105. ctx.WebsiteSummary = strings.TrimSpace(draftContext.LLM.WebsiteSummary)
  106. ctx.LocaleStyle = strings.TrimSpace(draftContext.LLM.StyleProfile.LocaleStyle)
  107. ctx.MarketStyle = strings.TrimSpace(draftContext.LLM.StyleProfile.MarketStyle)
  108. ctx.AddressMode = strings.TrimSpace(draftContext.LLM.StyleProfile.AddressMode)
  109. ctx.ContentTone = strings.TrimSpace(draftContext.LLM.StyleProfile.ContentTone)
  110. ctx.PromptNote = strings.TrimSpace(draftContext.LLM.StyleProfile.PromptInstructions)
  111. return ctx
  112. }
  113. func suggestValueForSlot(slot string, ctx suggestionContext) (string, string, bool) {
  114. company := fallback(ctx.CompanyName, "Ihr Unternehmen")
  115. business := fallback(ctx.BusinessType, "Angebot")
  116. toneAdj := toneAdjective(ctx.ContentTone)
  117. audienceLine := audienceFlavor(ctx)
  118. switch {
  119. case slot == "hero.title":
  120. return strings.TrimSpace(fmt.Sprintf("%s fuer %s mit %s Klarheit", company, business, toneAdj)), "slot-based hero headline", true
  121. case slot == "intro.title":
  122. return strings.TrimSpace(fmt.Sprintf("Was %s fuer Sie einfacher macht", company)), "slot-based intro title", true
  123. case slot == "intro.description":
  124. return firstNonEmpty(
  125. shortenSentence(ctx.WebsiteSummary, 180),
  126. fmt.Sprintf("%s unterstuetzt Kunden mit %s Leistungen, klarer Kommunikation und einem %s Auftritt.", company, business, toneAdj),
  127. ), "slot-based intro description", true
  128. case slot == "about.description":
  129. return firstNonEmpty(
  130. shortenSentence(ctx.DescriptionLong, 260),
  131. shortenSentence(ctx.Mission, 220),
  132. fmt.Sprintf("%s steht fuer %s, verlaessliche Zusammenarbeit und einen %s Anspruch in Beratung und Umsetzung.", company, business, toneAdj),
  133. ), "slot-based about description", true
  134. case strings.HasPrefix(slot, "service_items[") && strings.HasSuffix(slot, "].title"):
  135. idx := repeatedSlotIndex(slot)
  136. return fmt.Sprintf("%s Leistung %d", titleCaseBusiness(business), idx+1), "slot-based service title", true
  137. case strings.HasPrefix(slot, "service_items[") && strings.HasSuffix(slot, "].description"):
  138. idx := repeatedSlotIndex(slot)
  139. return fmt.Sprintf("Praezise Umsetzung von %s mit Fokus auf Nutzen, Verstaendlichkeit und %s Wirkung%s.", business, toneAdj, audienceLineForIndex(audienceLine, idx)), "slot-based service description", true
  140. case strings.HasPrefix(slot, "team_items[") && strings.HasSuffix(slot, "].name"):
  141. idx := repeatedSlotIndex(slot)
  142. return fmt.Sprintf("Ansprechperson %d", idx+1), "slot-based team placeholder", true
  143. case strings.HasPrefix(slot, "team_items[") && strings.HasSuffix(slot, "].description"):
  144. idx := repeatedSlotIndex(slot)
  145. return fmt.Sprintf("Begleitet Projekte bei %s mit fachlicher Sicherheit, klarer Kommunikation und %s Auftreten.", business, toneAdjForIndex(toneAdj, idx)), "slot-based team description", true
  146. case strings.HasPrefix(slot, "testimonial_items[") && strings.HasSuffix(slot, "].name"):
  147. idx := repeatedSlotIndex(slot)
  148. return fmt.Sprintf("Kundin/Kunde %d", idx+1), "slot-based testimonial placeholder", true
  149. case strings.HasPrefix(slot, "testimonial_items[") && strings.HasSuffix(slot, "].title"):
  150. return firstNonEmpty(
  151. testimonialLead(ctx),
  152. "Vertrauen durch saubere Zusammenarbeit",
  153. ), "slot-based testimonial title", true
  154. case strings.HasPrefix(slot, "testimonial_items[") && strings.HasSuffix(slot, "].description"):
  155. return fmt.Sprintf("%s ueberzeugt mit %s Prozessen, klaren Ergebnissen und einer Zusammenarbeit, die angenehm effizient bleibt.", company, toneAdj), "slot-based testimonial description", true
  156. case slot == "cta.text":
  157. if ctx.AddressMode == "du" {
  158. return "Jetzt unverbindlich anfragen", "slot-based cta", true
  159. }
  160. return "Jetzt unverbindlich anfragen", "slot-based cta", true
  161. default:
  162. return "", "", false
  163. }
  164. }
  165. func getMapString(values map[string]any, key string) string {
  166. if values == nil {
  167. return ""
  168. }
  169. raw, _ := values[key].(string)
  170. return strings.TrimSpace(raw)
  171. }
  172. func fallback(value, alt string) string {
  173. if strings.TrimSpace(value) == "" {
  174. return alt
  175. }
  176. return strings.TrimSpace(value)
  177. }
  178. func firstNonEmpty(values ...string) string {
  179. for _, value := range values {
  180. if strings.TrimSpace(value) != "" {
  181. return strings.TrimSpace(value)
  182. }
  183. }
  184. return ""
  185. }
  186. func shortenSentence(value string, max int) string {
  187. trimmed := strings.TrimSpace(value)
  188. if trimmed == "" || max <= 0 {
  189. return ""
  190. }
  191. if len([]rune(trimmed)) <= max {
  192. return trimmed
  193. }
  194. runes := []rune(trimmed)
  195. return strings.TrimSpace(string(runes[:max])) + "..."
  196. }
  197. func repeatedSlotIndex(slot string) int {
  198. start := strings.Index(slot, "[")
  199. end := strings.Index(slot, "]")
  200. if start < 0 || end <= start+1 {
  201. return 0
  202. }
  203. value := strings.TrimSpace(slot[start+1 : end])
  204. var idx int
  205. _, _ = fmt.Sscanf(value, "%d", &idx)
  206. if idx < 0 {
  207. return 0
  208. }
  209. return idx
  210. }
  211. func toneAdjective(tone string) string {
  212. switch strings.ToLower(strings.TrimSpace(tone)) {
  213. case "locker":
  214. return "lockerer"
  215. case "modern":
  216. return "moderner"
  217. case "premium":
  218. return "hochwertiger"
  219. case "freundlich":
  220. return "freundlicher"
  221. case "professionell":
  222. return "professioneller"
  223. default:
  224. return "klarer"
  225. }
  226. }
  227. func toneAdjForIndex(adj string, idx int) string {
  228. if idx%2 == 0 {
  229. return adj
  230. }
  231. return adj
  232. }
  233. func audienceFlavor(ctx suggestionContext) string {
  234. parts := make([]string, 0, 2)
  235. if strings.TrimSpace(ctx.LocaleStyle) != "" {
  236. parts = append(parts, ctx.LocaleStyle)
  237. }
  238. if strings.TrimSpace(ctx.MarketStyle) != "" {
  239. parts = append(parts, ctx.MarketStyle)
  240. }
  241. return strings.Join(parts, " / ")
  242. }
  243. func audienceLineForIndex(value string, idx int) string {
  244. if strings.TrimSpace(value) == "" || idx != 0 {
  245. return ""
  246. }
  247. return " fuer " + value
  248. }
  249. func titleCaseBusiness(value string) string {
  250. trimmed := strings.TrimSpace(value)
  251. if trimmed == "" {
  252. return "Service"
  253. }
  254. return strings.ToUpper(string([]rune(trimmed)[0])) + string([]rune(trimmed)[1:])
  255. }
  256. func testimonialLead(ctx suggestionContext) string {
  257. if strings.TrimSpace(ctx.WebsiteSummary) == "" {
  258. return ""
  259. }
  260. return shortenSentence(ctx.WebsiteSummary, 80)
  261. }
  262. func GenerateAllSuggestions(ctx context.Context, generator SuggestionGenerator, req SuggestionRequest, current domain.DraftSuggestionState, now time.Time) domain.DraftSuggestionState {
  263. next := cloneSuggestionState(current)
  264. if next.ByFieldPath == nil {
  265. next.ByFieldPath = map[string]domain.DraftSuggestion{}
  266. }
  267. generated, err := suggestionResultWithFallback(ctx, generator, SuggestionRequest{
  268. TemplateID: req.TemplateID,
  269. DraftID: req.DraftID,
  270. Fields: req.Fields,
  271. GlobalData: req.GlobalData,
  272. DraftContext: req.DraftContext,
  273. MasterPrompt: req.MasterPrompt,
  274. PromptBlocks: req.PromptBlocks,
  275. Existing: req.Existing,
  276. IncludeFilled: true,
  277. })
  278. if err != nil {
  279. return next
  280. }
  281. mappingLogger().InfoContext(ctx, "autofill state transition",
  282. "component", "autofill",
  283. "step", "post_generate_result",
  284. "action", "generate_all",
  285. "generated_count", len(generated.ByFieldPath),
  286. "generated_sources", summarizeResultSources(generated),
  287. "sample_sources", sampleResultSources(generated, 5),
  288. )
  289. for _, s := range generated.Suggestions {
  290. sliceSource := strings.TrimSpace(s.Source)
  291. canonicalSource := ""
  292. if canonical, ok := generated.ByFieldPath[strings.TrimSpace(s.FieldPath)]; ok {
  293. canonicalSource = strings.TrimSpace(canonical.Source)
  294. s = canonical
  295. }
  296. if existing, exists := next.ByFieldPath[s.FieldPath]; exists {
  297. if !shouldReplaceExistingSuggestion(existing, s) {
  298. continue
  299. }
  300. }
  301. stored := toDraftSuggestion(s, now)
  302. if explicitSource := strings.TrimSpace(s.Source); explicitSource != "" {
  303. stored.Source = explicitSource
  304. }
  305. mappingLogger().InfoContext(ctx, "autofill state transition",
  306. "component", "autofill",
  307. "step", "apply_field_transition",
  308. "action", "generate_all",
  309. "field_path", strings.TrimSpace(s.FieldPath),
  310. "slice_source", firstNonEmpty(sliceSource, "unknown"),
  311. "canonical_source", firstNonEmpty(canonicalSource, "unknown"),
  312. "stored_source", firstNonEmpty(strings.TrimSpace(stored.Source), "unknown"),
  313. )
  314. next.ByFieldPath[s.FieldPath] = stored
  315. }
  316. mappingLogger().InfoContext(ctx, "autofill state transition",
  317. "component", "autofill",
  318. "step", "post_generate_apply_state",
  319. "action", "generate_all",
  320. "state_count", len(next.ByFieldPath),
  321. "state_sources", summarizeDraftSuggestionSources(next),
  322. "sample_sources", sampleDraftSuggestionSources(next, 5),
  323. )
  324. next.UpdatedAt = now.UTC()
  325. return next
  326. }
  327. func RegenerateAllSuggestions(ctx context.Context, generator SuggestionGenerator, req SuggestionRequest, current domain.DraftSuggestionState, now time.Time) domain.DraftSuggestionState {
  328. next := cloneSuggestionState(current)
  329. next.ByFieldPath = map[string]domain.DraftSuggestion{}
  330. generated, err := suggestionResultWithFallback(ctx, generator, SuggestionRequest{
  331. TemplateID: req.TemplateID,
  332. DraftID: req.DraftID,
  333. Fields: req.Fields,
  334. GlobalData: req.GlobalData,
  335. DraftContext: req.DraftContext,
  336. MasterPrompt: req.MasterPrompt,
  337. PromptBlocks: req.PromptBlocks,
  338. Existing: req.Existing,
  339. IncludeFilled: true,
  340. })
  341. if err != nil {
  342. return next
  343. }
  344. mappingLogger().InfoContext(ctx, "autofill state transition",
  345. "component", "autofill",
  346. "step", "post_generate_result",
  347. "action", "regenerate_all",
  348. "generated_count", len(generated.ByFieldPath),
  349. "generated_sources", summarizeResultSources(generated),
  350. "sample_sources", sampleResultSources(generated, 5),
  351. )
  352. for _, s := range generated.Suggestions {
  353. sliceSource := strings.TrimSpace(s.Source)
  354. canonicalSource := ""
  355. if canonical, ok := generated.ByFieldPath[strings.TrimSpace(s.FieldPath)]; ok {
  356. canonicalSource = strings.TrimSpace(canonical.Source)
  357. s = canonical
  358. }
  359. stored := toDraftSuggestion(s, now)
  360. if explicitSource := strings.TrimSpace(s.Source); explicitSource != "" {
  361. stored.Source = explicitSource
  362. }
  363. mappingLogger().InfoContext(ctx, "autofill state transition",
  364. "component", "autofill",
  365. "step", "apply_field_transition",
  366. "action", "regenerate_all",
  367. "field_path", strings.TrimSpace(s.FieldPath),
  368. "slice_source", firstNonEmpty(sliceSource, "unknown"),
  369. "canonical_source", firstNonEmpty(canonicalSource, "unknown"),
  370. "stored_source", firstNonEmpty(strings.TrimSpace(stored.Source), "unknown"),
  371. )
  372. next.ByFieldPath[s.FieldPath] = stored
  373. }
  374. mappingLogger().InfoContext(ctx, "autofill state transition",
  375. "component", "autofill",
  376. "step", "post_generate_apply_state",
  377. "action", "regenerate_all",
  378. "state_count", len(next.ByFieldPath),
  379. "state_sources", summarizeDraftSuggestionSources(next),
  380. "sample_sources", sampleDraftSuggestionSources(next, 5),
  381. )
  382. next.UpdatedAt = now.UTC()
  383. return next
  384. }
  385. func RegenerateFieldSuggestion(ctx context.Context, generator SuggestionGenerator, req SuggestionRequest, current domain.DraftSuggestionState, fieldPath string, now time.Time) domain.DraftSuggestionState {
  386. target := strings.TrimSpace(fieldPath)
  387. if target == "" {
  388. return cloneSuggestionState(current)
  389. }
  390. next := cloneSuggestionState(current)
  391. if next.ByFieldPath == nil {
  392. next.ByFieldPath = map[string]domain.DraftSuggestion{}
  393. }
  394. generated, err := suggestionResultWithFallback(ctx, generator, SuggestionRequest{
  395. TemplateID: req.TemplateID,
  396. DraftID: req.DraftID,
  397. Fields: req.Fields,
  398. GlobalData: req.GlobalData,
  399. DraftContext: req.DraftContext,
  400. MasterPrompt: req.MasterPrompt,
  401. PromptBlocks: req.PromptBlocks,
  402. Existing: req.Existing,
  403. IncludeFilled: true,
  404. })
  405. if err != nil {
  406. return next
  407. }
  408. if suggestion, ok := generated.ByFieldPath[target]; ok {
  409. next.ByFieldPath[target] = toDraftSuggestion(suggestion, now)
  410. next.UpdatedAt = now.UTC()
  411. }
  412. return next
  413. }
  414. func ApplySuggestionsToEmptyFields(fieldValues map[string]string, state domain.DraftSuggestionState, now time.Time) (map[string]string, domain.DraftSuggestionState) {
  415. values := cloneFieldValues(fieldValues)
  416. next := cloneSuggestionState(state)
  417. if next.ByFieldPath == nil {
  418. next.ByFieldPath = map[string]domain.DraftSuggestion{}
  419. }
  420. for path, suggestion := range next.ByFieldPath {
  421. if strings.TrimSpace(values[path]) != "" {
  422. continue
  423. }
  424. if strings.TrimSpace(suggestion.Value) == "" {
  425. continue
  426. }
  427. values[path] = strings.TrimSpace(suggestion.Value)
  428. suggestion.Status = domain.DraftSuggestionStatusApplied
  429. suggestion.UpdatedAt = now.UTC()
  430. next.ByFieldPath[path] = suggestion
  431. }
  432. next.UpdatedAt = now.UTC()
  433. return values, next
  434. }
  435. func ApplyAllSuggestions(fieldValues map[string]string, state domain.DraftSuggestionState, now time.Time) (map[string]string, domain.DraftSuggestionState) {
  436. values := cloneFieldValues(fieldValues)
  437. next := cloneSuggestionState(state)
  438. if next.ByFieldPath == nil {
  439. next.ByFieldPath = map[string]domain.DraftSuggestion{}
  440. }
  441. for path, suggestion := range next.ByFieldPath {
  442. if strings.TrimSpace(suggestion.Value) == "" {
  443. continue
  444. }
  445. values[path] = strings.TrimSpace(suggestion.Value)
  446. suggestion.Status = domain.DraftSuggestionStatusApplied
  447. suggestion.UpdatedAt = now.UTC()
  448. next.ByFieldPath[path] = suggestion
  449. }
  450. next.UpdatedAt = now.UTC()
  451. return values, next
  452. }
  453. func ApplySuggestionToField(fieldValues map[string]string, state domain.DraftSuggestionState, fieldPath string, now time.Time) (map[string]string, domain.DraftSuggestionState) {
  454. target := strings.TrimSpace(fieldPath)
  455. values := cloneFieldValues(fieldValues)
  456. next := cloneSuggestionState(state)
  457. if target == "" || next.ByFieldPath == nil {
  458. return values, next
  459. }
  460. suggestion, ok := next.ByFieldPath[target]
  461. if !ok || strings.TrimSpace(suggestion.Value) == "" {
  462. return values, next
  463. }
  464. values[target] = strings.TrimSpace(suggestion.Value)
  465. suggestion.Status = domain.DraftSuggestionStatusApplied
  466. suggestion.UpdatedAt = now.UTC()
  467. next.ByFieldPath[target] = suggestion
  468. next.UpdatedAt = now.UTC()
  469. return values, next
  470. }
  471. func cloneFieldValues(values map[string]string) map[string]string {
  472. if values == nil {
  473. return map[string]string{}
  474. }
  475. out := make(map[string]string, len(values))
  476. for k, v := range values {
  477. out[k] = v
  478. }
  479. return out
  480. }
  481. func cloneSuggestionState(state domain.DraftSuggestionState) domain.DraftSuggestionState {
  482. out := domain.DraftSuggestionState{
  483. ByFieldPath: map[string]domain.DraftSuggestion{},
  484. UpdatedAt: state.UpdatedAt,
  485. }
  486. for path, suggestion := range state.ByFieldPath {
  487. out.ByFieldPath[path] = suggestion
  488. }
  489. return out
  490. }
  491. func toDraftSuggestion(s Suggestion, now time.Time) domain.DraftSuggestion {
  492. ts := now.UTC()
  493. source := strings.TrimSpace(s.Source)
  494. if source == "" {
  495. source = "unknown"
  496. }
  497. return domain.DraftSuggestion{
  498. FieldPath: strings.TrimSpace(s.FieldPath),
  499. Slot: strings.TrimSpace(s.Slot),
  500. Value: strings.TrimSpace(s.Value),
  501. Reason: strings.TrimSpace(s.Reason),
  502. Source: source,
  503. Status: domain.DraftSuggestionStatusSuggested,
  504. GeneratedAt: ts,
  505. UpdatedAt: ts,
  506. }
  507. }
  508. func shouldReplaceExistingSuggestion(existing domain.DraftSuggestion, generated Suggestion) bool {
  509. existingSource := strings.TrimSpace(existing.Source)
  510. generatedSource := strings.TrimSpace(generated.Source)
  511. if generatedSource == "" {
  512. return false
  513. }
  514. if generatedSource == domain.DraftSuggestionSourceFallbackRuleBased {
  515. return false
  516. }
  517. return existingSource == domain.DraftSuggestionSourceFallbackRuleBased
  518. }
  519. func suggestionResultWithFallback(ctx context.Context, generator SuggestionGenerator, req SuggestionRequest) (SuggestionResult, error) {
  520. if generator == nil {
  521. return NewRuleBasedSuggestionGenerator().Generate(ctx, req)
  522. }
  523. return generator.Generate(ctx, req)
  524. }
  525. func summarizeResultSources(result SuggestionResult) map[string]int {
  526. if len(result.ByFieldPath) == 0 {
  527. return map[string]int{}
  528. }
  529. out := map[string]int{}
  530. for _, suggestion := range result.ByFieldPath {
  531. source := strings.TrimSpace(suggestion.Source)
  532. if source == "" {
  533. source = "unknown"
  534. }
  535. out[source]++
  536. }
  537. return out
  538. }
  539. func summarizeDraftSuggestionSources(state domain.DraftSuggestionState) map[string]int {
  540. if len(state.ByFieldPath) == 0 {
  541. return map[string]int{}
  542. }
  543. out := map[string]int{}
  544. for _, suggestion := range state.ByFieldPath {
  545. source := strings.TrimSpace(suggestion.Source)
  546. if source == "" {
  547. source = "unknown"
  548. }
  549. out[source]++
  550. }
  551. return out
  552. }
  553. func sampleResultSources(result SuggestionResult, limit int) map[string]string {
  554. if limit <= 0 || len(result.ByFieldPath) == 0 {
  555. return map[string]string{}
  556. }
  557. paths := make([]string, 0, len(result.ByFieldPath))
  558. for path := range result.ByFieldPath {
  559. paths = append(paths, path)
  560. }
  561. sort.Strings(paths)
  562. if len(paths) > limit {
  563. paths = paths[:limit]
  564. }
  565. out := make(map[string]string, len(paths))
  566. for _, path := range paths {
  567. source := strings.TrimSpace(result.ByFieldPath[path].Source)
  568. if source == "" {
  569. source = "unknown"
  570. }
  571. out[path] = source
  572. }
  573. return out
  574. }
  575. func sampleDraftSuggestionSources(state domain.DraftSuggestionState, limit int) map[string]string {
  576. if limit <= 0 || len(state.ByFieldPath) == 0 {
  577. return map[string]string{}
  578. }
  579. paths := make([]string, 0, len(state.ByFieldPath))
  580. for path := range state.ByFieldPath {
  581. paths = append(paths, path)
  582. }
  583. sort.Strings(paths)
  584. if len(paths) > limit {
  585. paths = paths[:limit]
  586. }
  587. out := make(map[string]string, len(paths))
  588. for _, path := range paths {
  589. source := strings.TrimSpace(state.ByFieldPath[path].Source)
  590. if source == "" {
  591. source = "unknown"
  592. }
  593. out[path] = source
  594. }
  595. return out
  596. }