package knowledge import ( "strings" "unicode/utf8" "github.com/google/uuid" ) const ( maxPatrolLabelRunes = 14 minBreadthNodeCount = 12 ) func SupplementGraphFromResearchMap(g *Graph, seed string, pillars, questions []string) { if g == nil { return } seen := nodeLabelSet(g.Nodes) ensureSeedCore(g, seed, seen) for _, pillar := range pillars { if len(g.Nodes) >= 32 { break } addBreadthNode(g, strings.TrimSpace(pillar), 1, "symptom", seen, pillar, "") } for _, question := range questions { if len(g.Nodes) >= 32 { break } q := strings.TrimSpace(question) if q == "" { continue } label := patrolLabelFromQuestion(q) addBreadthNode(g, label, 2, "pain", seen, q, defaultPlacementForQuestion(q)) } } func ensureSeedCore(g *Graph, seed string, seen map[string]struct{}) { seed = strings.TrimSpace(seed) if seed == "" { return } if _, ok := seen[strings.ToLower(seed)]; ok { return } g.Nodes = append([]Node{{ ID: uuid.NewString(), Label: seed, NodeKind: "pain", Type: "core", Layer: 0, Relation: "核心種子主題,使用者圍繞此困擾在社群發文求助", PlacementValue: "核心討論串最常求推薦與真實心得,適合以產品使用經驗自然回覆", ProductFitScore: 90, }}, g.Nodes...) seen[strings.ToLower(seed)] = struct{}{} } func addBreadthNode(g *Graph, label string, layer int, kind string, seen map[string]struct{}, relation, placement string) { label = strings.TrimSpace(label) if label == "" { return } key := strings.ToLower(label) if _, ok := seen[key]; ok { return } if relation == "" { relation = "與種子主題相關的「" + label + "」討論,常見於 Threads 求助串" } if placement == "" { placement = "這類帖常求產品推薦或使用心得,適合以自身經驗自然回覆" } g.Nodes = append(g.Nodes, Node{ ID: uuid.NewString(), Label: label, NodeKind: kind, Type: breadthNodeType(layer, kind), Layer: layer, Relation: relation, PlacementValue: placement, ProductFitScore: defaultProductFit(kind, layer), }) seen[key] = struct{}{} } func breadthNodeType(layer int, kind string) string { if layer == 0 { return "core" } if kind == "cause" { return "cause" } if kind == "symptom" { return "symptom" } return "mechanism" } func nodeLabelSet(nodes []Node) map[string]struct{} { seen := map[string]struct{}{} for _, node := range nodes { label := strings.ToLower(strings.TrimSpace(node.Label)) if label != "" { seen[label] = struct{}{} } } return seen } func patrolLabelFromQuestion(question string) string { question = strings.TrimSpace(question) if question == "" { return "" } if utf8.RuneCountInString(question) <= maxPatrolLabelRunes { return question } runes := []rune(question) return string(runes[:maxPatrolLabelRunes]) } func defaultPlacementForQuestion(question string) string { return "受眾常這樣發文求助,適合在留言區以產品使用經驗回覆「" + strings.TrimSpace(question) + "」這類問題" } func GraphNeedsBootstrap(g Graph) bool { return len(g.Nodes) < minBreadthNodeCount }