132 lines
3.2 KiB
Go
132 lines
3.2 KiB
Go
|
|
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
|
||
|
|
}
|