870 lines
20 KiB
Go
870 lines
20 KiB
Go
package app
|
||
|
||
import (
|
||
"fmt"
|
||
"math"
|
||
"regexp"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"infinite-canvas/backend/internal/model"
|
||
)
|
||
|
||
// Canvas Intelligence 辅助方法
|
||
|
||
// isContextNode 判断节点是否为上下文节点(焦点节点的相邻节点)
|
||
func (ci *CanvasIntelligence) isContextNode(node model.CanvasNode, focusNodeIDs []string, allNodes []model.CanvasNode) bool {
|
||
focusMap := make(map[string]bool)
|
||
for _, id := range focusNodeIDs {
|
||
focusMap[id] = true
|
||
}
|
||
|
||
// 检查是否是焦点节点的父节点
|
||
if node.ParentID != nil && focusMap[*node.ParentID] {
|
||
return true
|
||
}
|
||
|
||
// 检查是否是焦点节点的子节点
|
||
for _, childID := range node.Children {
|
||
if focusMap[childID] {
|
||
return true
|
||
}
|
||
}
|
||
|
||
// 检查是否在焦点节点的空间邻近范围内
|
||
for _, focusID := range focusNodeIDs {
|
||
for _, n := range allNodes {
|
||
if n.ID == focusID && ci.isSpatiallyNear(node, n) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
// isSpatiallyNear 判断两个节点是否空间邻近
|
||
func (ci *CanvasIntelligence) isSpatiallyNear(node1, node2 model.CanvasNode) bool {
|
||
if node1.Position == nil || node2.Position == nil {
|
||
return false
|
||
}
|
||
|
||
// 计算距离
|
||
dx := node1.Position.X - node2.Position.X
|
||
dy := node1.Position.Y - node2.Position.Y
|
||
distance := math.Sqrt(dx*dx + dy*dy)
|
||
|
||
// 邻近阈值:200px
|
||
return distance < 200
|
||
}
|
||
|
||
// buildRelationships 构建关系视图
|
||
func (ci *CanvasIntelligence) buildRelationships(nodes []model.CanvasNode) []RelationshipView {
|
||
relationships := make([]RelationshipView, 0)
|
||
nodeMap := make(map[string]model.CanvasNode)
|
||
|
||
for _, node := range nodes {
|
||
nodeMap[node.ID] = node
|
||
}
|
||
|
||
for _, node := range nodes {
|
||
// 父子关系
|
||
if node.ParentID != nil {
|
||
relationships = append(relationships, RelationshipView{
|
||
From: *node.ParentID,
|
||
To: node.ID,
|
||
Type: "parent",
|
||
Strength: 1.0,
|
||
Bidirection: false,
|
||
})
|
||
}
|
||
|
||
// 引用关系(从内容中提取)
|
||
refs := ci.extractReferences(node.Content)
|
||
for _, refID := range refs {
|
||
if _, exists := nodeMap[refID]; exists {
|
||
relationships = append(relationships, RelationshipView{
|
||
From: node.ID,
|
||
To: refID,
|
||
Type: "reference",
|
||
Strength: 0.7,
|
||
Bidirection: false,
|
||
})
|
||
}
|
||
}
|
||
|
||
// 语义相似关系(简化实现)
|
||
for _, other := range nodes {
|
||
if node.ID != other.ID && ci.areSemanticallyRelated(node, other) {
|
||
relationships = append(relationships, RelationshipView{
|
||
From: node.ID,
|
||
To: other.ID,
|
||
Type: "semantic",
|
||
Strength: 0.5,
|
||
Bidirection: true,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
return relationships
|
||
}
|
||
|
||
// extractReferences 从内容中提取引用(简化实现:查找 [[nodeId]] 模式)
|
||
func (ci *CanvasIntelligence) extractReferences(content string) []string {
|
||
re := regexp.MustCompile(`\[\[([a-zA-Z0-9-]+)\]\]`)
|
||
matches := re.FindAllStringSubmatch(content, -1)
|
||
refs := make([]string, 0, len(matches))
|
||
for _, match := range matches {
|
||
if len(match) > 1 {
|
||
refs = append(refs, match[1])
|
||
}
|
||
}
|
||
return refs
|
||
}
|
||
|
||
// areSemanticallyRelated 判断两个节点是否语义相关(简化实现)
|
||
func (ci *CanvasIntelligence) areSemanticallyRelated(node1, node2 model.CanvasNode) bool {
|
||
// 简化实现:检查共同关键词
|
||
keywords1 := ci.extractKeywords(node1.Content)
|
||
keywords2 := ci.extractKeywords(node2.Content)
|
||
|
||
if len(keywords1) == 0 || len(keywords2) == 0 {
|
||
return false
|
||
}
|
||
|
||
common := 0
|
||
for _, k1 := range keywords1 {
|
||
for _, k2 := range keywords2 {
|
||
if k1 == k2 {
|
||
common++
|
||
}
|
||
}
|
||
}
|
||
|
||
// 共同关键词 >= 2 则认为相关
|
||
return common >= 2
|
||
}
|
||
|
||
// analyzeLayout 分析布局
|
||
func (ci *CanvasIntelligence) analyzeLayout(nodes []model.CanvasNode) LayoutAnalysis {
|
||
if len(nodes) == 0 {
|
||
return LayoutAnalysis{
|
||
Algorithm: "empty",
|
||
Direction: "none",
|
||
Density: 0,
|
||
Clusters: []ClusterInfo{},
|
||
Bounds: Bounds{},
|
||
}
|
||
}
|
||
|
||
// 计算边界
|
||
bounds := ci.calculateBounds(nodes)
|
||
|
||
// 检测布局算法
|
||
algorithm := ci.detectLayoutAlgorithm(nodes)
|
||
|
||
// 计算密度
|
||
density := ci.calculateDensity(nodes, bounds)
|
||
|
||
// 聚类分析
|
||
clusters := ci.performClustering(nodes)
|
||
|
||
// 推断方向
|
||
direction := ci.inferDirection(nodes)
|
||
|
||
return LayoutAnalysis{
|
||
Algorithm: algorithm,
|
||
Direction: direction,
|
||
Density: density,
|
||
Clusters: clusters,
|
||
Bounds: bounds,
|
||
ViewportFit: ci.suggestViewportFit(bounds, density),
|
||
}
|
||
}
|
||
|
||
// calculateBounds 计算边界
|
||
func (ci *CanvasIntelligence) calculateBounds(nodes []model.CanvasNode) Bounds {
|
||
if len(nodes) == 0 {
|
||
return Bounds{}
|
||
}
|
||
|
||
minX, minY := math.MaxFloat64, math.MaxFloat64
|
||
maxX, maxY := -math.MaxFloat64, -math.MaxFloat64
|
||
|
||
for _, node := range nodes {
|
||
if node.Position != nil {
|
||
minX = math.Min(minX, node.Position.X)
|
||
minY = math.Min(minY, node.Position.Y)
|
||
maxX = math.Max(maxX, node.Position.X)
|
||
maxY = math.Max(maxY, node.Position.Y)
|
||
|
||
if node.Size != nil {
|
||
maxX = math.Max(maxX, node.Position.X+node.Size.Width)
|
||
maxY = math.Max(maxY, node.Position.Y+node.Size.Height)
|
||
}
|
||
}
|
||
}
|
||
|
||
return Bounds{
|
||
MinX: minX,
|
||
MinY: minY,
|
||
MaxX: maxX,
|
||
MaxY: maxY,
|
||
}
|
||
}
|
||
|
||
// detectLayoutAlgorithm 检测布局算法
|
||
func (ci *CanvasIntelligence) detectLayoutAlgorithm(nodes []model.CanvasNode) string {
|
||
// 检查是否是树形布局(大部分节点有父节点)
|
||
withParent := 0
|
||
for _, node := range nodes {
|
||
if node.ParentID != nil {
|
||
withParent++
|
||
}
|
||
}
|
||
if float64(withParent)/float64(len(nodes)) > 0.6 {
|
||
return "tree"
|
||
}
|
||
|
||
// 检查是否是网格布局(位置规则)
|
||
if ci.isGridLayout(nodes) {
|
||
return "grid"
|
||
}
|
||
|
||
// 检查是否是力导向布局(分散均匀)
|
||
if ci.isForceLayout(nodes) {
|
||
return "force"
|
||
}
|
||
|
||
return "freeform"
|
||
}
|
||
|
||
// isGridLayout 判断是否是网格布局
|
||
func (ci *CanvasIntelligence) isGridLayout(nodes []model.CanvasNode) bool {
|
||
if len(nodes) < 3 {
|
||
return false
|
||
}
|
||
|
||
// 简化实现:检查X或Y坐标是否有明显对齐
|
||
xCoords := make(map[float64]int)
|
||
yCoords := make(map[float64]int)
|
||
|
||
for _, node := range nodes {
|
||
if node.Position != nil {
|
||
// 量化坐标(容忍10px误差)
|
||
xQuantized := math.Round(node.Position.X/10) * 10
|
||
yQuantized := math.Round(node.Position.Y/10) * 10
|
||
xCoords[xQuantized]++
|
||
yCoords[yQuantized]++
|
||
}
|
||
}
|
||
|
||
// 如果有3个以上坐标重复,认为是网格
|
||
for _, count := range xCoords {
|
||
if count >= 3 {
|
||
return true
|
||
}
|
||
}
|
||
for _, count := range yCoords {
|
||
if count >= 3 {
|
||
return true
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
// isForceLayout 判断是否是力导向布局
|
||
func (ci *CanvasIntelligence) isForceLayout(nodes []model.CanvasNode) bool {
|
||
// 简化实现:检查节点分布是否均匀
|
||
if len(nodes) < 5 {
|
||
return false
|
||
}
|
||
|
||
// 计算平均距离
|
||
totalDist := 0.0
|
||
count := 0
|
||
for i, node1 := range nodes {
|
||
if node1.Position == nil {
|
||
continue
|
||
}
|
||
for j, node2 := range nodes {
|
||
if i >= j || node2.Position == nil {
|
||
continue
|
||
}
|
||
dx := node1.Position.X - node2.Position.X
|
||
dy := node1.Position.Y - node2.Position.Y
|
||
dist := math.Sqrt(dx*dx + dy*dy)
|
||
totalDist += dist
|
||
count++
|
||
}
|
||
}
|
||
|
||
if count == 0 {
|
||
return false
|
||
}
|
||
|
||
avgDist := totalDist / float64(count)
|
||
|
||
// 计算距离方差
|
||
variance := 0.0
|
||
count = 0
|
||
for i, node1 := range nodes {
|
||
if node1.Position == nil {
|
||
continue
|
||
}
|
||
for j, node2 := range nodes {
|
||
if i >= j || node2.Position == nil {
|
||
continue
|
||
}
|
||
dx := node1.Position.X - node2.Position.X
|
||
dy := node1.Position.Y - node2.Position.Y
|
||
dist := math.Sqrt(dx*dx + dy*dy)
|
||
variance += (dist - avgDist) * (dist - avgDist)
|
||
count++
|
||
}
|
||
}
|
||
|
||
if count == 0 {
|
||
return false
|
||
}
|
||
|
||
variance /= float64(count)
|
||
stdDev := math.Sqrt(variance)
|
||
|
||
// 标准差/平均值 < 0.3 认为是均匀分布(力导向特征)
|
||
return stdDev/avgDist < 0.3
|
||
}
|
||
|
||
// calculateDensity 计算密度
|
||
func (ci *CanvasIntelligence) calculateDensity(nodes []model.CanvasNode, bounds Bounds) float64 {
|
||
if len(nodes) == 0 {
|
||
return 0
|
||
}
|
||
|
||
area := (bounds.MaxX - bounds.MinX) * (bounds.MaxY - bounds.MinY)
|
||
if area == 0 {
|
||
return 0
|
||
}
|
||
|
||
totalNodeArea := 0.0
|
||
for _, node := range nodes {
|
||
if node.Size != nil {
|
||
totalNodeArea += node.Size.Width * node.Size.Height
|
||
} else {
|
||
totalNodeArea += 100 * 100 // 默认大小
|
||
}
|
||
}
|
||
|
||
density := totalNodeArea / area
|
||
if density > 1 {
|
||
density = 1
|
||
}
|
||
|
||
return density
|
||
}
|
||
|
||
// performClustering 执行聚类分析(简化K-means)
|
||
func (ci *CanvasIntelligence) performClustering(nodes []model.CanvasNode) []ClusterInfo {
|
||
// 过滤有位置的节点
|
||
positioned := make([]model.CanvasNode, 0)
|
||
for _, node := range nodes {
|
||
if node.Position != nil {
|
||
positioned = append(positioned, node)
|
||
}
|
||
}
|
||
|
||
if len(positioned) < 3 {
|
||
return []ClusterInfo{}
|
||
}
|
||
|
||
// 简化实现:基于空间距离的简单聚类
|
||
k := int(math.Min(5, float64(len(positioned)/3))) // 最多5个聚类
|
||
if k < 2 {
|
||
k = 2
|
||
}
|
||
|
||
clusters := ci.simpleKMeans(positioned, k)
|
||
return clusters
|
||
}
|
||
|
||
// simpleKMeans 简化的K-means聚类
|
||
func (ci *CanvasIntelligence) simpleKMeans(nodes []model.CanvasNode, k int) []ClusterInfo {
|
||
if len(nodes) == 0 {
|
||
return []ClusterInfo{}
|
||
}
|
||
if k < 1 {
|
||
k = 1
|
||
}
|
||
if len(nodes) < k {
|
||
k = len(nodes)
|
||
}
|
||
|
||
// 初始化中心点(均匀分布)
|
||
centers := make([]Position, k)
|
||
step := len(nodes) / k
|
||
for i := 0; i < k; i++ {
|
||
idx := i * step
|
||
if idx >= len(nodes) {
|
||
idx = len(nodes) - 1
|
||
}
|
||
centers[i] = Position{
|
||
X: nodes[idx].Position.X,
|
||
Y: nodes[idx].Position.Y,
|
||
}
|
||
}
|
||
|
||
// 迭代3次(简化)
|
||
for iter := 0; iter < 3; iter++ {
|
||
// 分配节点到最近的聚类
|
||
assignments := make([]int, len(nodes))
|
||
for i, node := range nodes {
|
||
minDist := math.MaxFloat64
|
||
minCluster := 0
|
||
for j, center := range centers {
|
||
dx := node.Position.X - center.X
|
||
dy := node.Position.Y - center.Y
|
||
dist := math.Sqrt(dx*dx + dy*dy)
|
||
if dist < minDist {
|
||
minDist = dist
|
||
minCluster = j
|
||
}
|
||
}
|
||
assignments[i] = minCluster
|
||
}
|
||
|
||
// 更新中心点
|
||
for j := 0; j < k; j++ {
|
||
sumX, sumY := 0.0, 0.0
|
||
count := 0
|
||
for i, assignment := range assignments {
|
||
if assignment == j {
|
||
sumX += nodes[i].Position.X
|
||
sumY += nodes[i].Position.Y
|
||
count++
|
||
}
|
||
}
|
||
if count > 0 {
|
||
centers[j] = Position{
|
||
X: sumX / float64(count),
|
||
Y: sumY / float64(count),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 构建聚类结果
|
||
finalAssignments := make([]int, len(nodes))
|
||
for i, node := range nodes {
|
||
minDist := math.MaxFloat64
|
||
minCluster := 0
|
||
for j, center := range centers {
|
||
dx := node.Position.X - center.X
|
||
dy := node.Position.Y - center.Y
|
||
dist := math.Sqrt(dx*dx + dy*dy)
|
||
if dist < minDist {
|
||
minDist = dist
|
||
minCluster = j
|
||
}
|
||
}
|
||
finalAssignments[i] = minCluster
|
||
}
|
||
|
||
clusters := make([]ClusterInfo, k)
|
||
for j := 0; j < k; j++ {
|
||
nodeIDs := make([]string, 0)
|
||
for i, assignment := range finalAssignments {
|
||
if assignment == j {
|
||
nodeIDs = append(nodeIDs, nodes[i].ID)
|
||
}
|
||
}
|
||
|
||
if len(nodeIDs) == 0 {
|
||
continue
|
||
}
|
||
|
||
clusters[j] = ClusterInfo{
|
||
ID: fmt.Sprintf("cluster-%d", j),
|
||
NodeIDs: nodeIDs,
|
||
Center: centers[j],
|
||
Label: ci.generateClusterLabel(nodes, nodeIDs),
|
||
Cohesion: ci.calculateCohesion(nodes, nodeIDs, centers[j]),
|
||
}
|
||
}
|
||
|
||
// 过滤空聚类
|
||
result := make([]ClusterInfo, 0)
|
||
for _, cluster := range clusters {
|
||
if len(cluster.NodeIDs) > 0 {
|
||
result = append(result, cluster)
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// generateClusterLabel 生成聚类标签
|
||
func (ci *CanvasIntelligence) generateClusterLabel(allNodes []model.CanvasNode, clusterNodeIDs []string) string {
|
||
// 收集聚类中的关键词
|
||
keywords := make(map[string]int)
|
||
for _, nodeID := range clusterNodeIDs {
|
||
for _, node := range allNodes {
|
||
if node.ID == nodeID {
|
||
for _, kw := range ci.extractKeywords(node.Content) {
|
||
keywords[kw]++
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 找到最频繁的关键词
|
||
maxCount := 0
|
||
topKeyword := "Cluster"
|
||
for kw, count := range keywords {
|
||
if count > maxCount {
|
||
maxCount = count
|
||
topKeyword = kw
|
||
}
|
||
}
|
||
|
||
return topKeyword
|
||
}
|
||
|
||
// calculateCohesion 计算内聚度
|
||
func (ci *CanvasIntelligence) calculateCohesion(allNodes []model.CanvasNode, clusterNodeIDs []string, center Position) float64 {
|
||
if len(clusterNodeIDs) == 0 {
|
||
return 0
|
||
}
|
||
|
||
totalDist := 0.0
|
||
count := 0
|
||
for _, nodeID := range clusterNodeIDs {
|
||
for _, node := range allNodes {
|
||
if node.ID == nodeID && node.Position != nil {
|
||
dx := node.Position.X - center.X
|
||
dy := node.Position.Y - center.Y
|
||
dist := math.Sqrt(dx*dx + dy*dy)
|
||
totalDist += dist
|
||
count++
|
||
}
|
||
}
|
||
}
|
||
|
||
if count == 0 {
|
||
return 0
|
||
}
|
||
|
||
avgDist := totalDist / float64(count)
|
||
|
||
// 归一化:距离越小,内聚度越高
|
||
cohesion := 1.0 / (1.0 + avgDist/100.0)
|
||
return cohesion
|
||
}
|
||
|
||
// inferDirection 推断方向
|
||
func (ci *CanvasIntelligence) inferDirection(nodes []model.CanvasNode) string {
|
||
if len(nodes) < 2 {
|
||
return "none"
|
||
}
|
||
|
||
// 统计父子关系的方向
|
||
horizontalCount := 0
|
||
verticalCount := 0
|
||
|
||
for _, node := range nodes {
|
||
if node.ParentID == nil || node.Position == nil {
|
||
continue
|
||
}
|
||
|
||
// 找到父节点
|
||
for _, parent := range nodes {
|
||
if parent.ID == *node.ParentID && parent.Position != nil {
|
||
dx := math.Abs(node.Position.X - parent.Position.X)
|
||
dy := math.Abs(node.Position.Y - parent.Position.Y)
|
||
|
||
if dx > dy {
|
||
horizontalCount++
|
||
} else {
|
||
verticalCount++
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if horizontalCount > verticalCount {
|
||
return "horizontal"
|
||
} else if verticalCount > horizontalCount {
|
||
return "vertical"
|
||
}
|
||
|
||
return "mixed"
|
||
}
|
||
|
||
// suggestViewportFit 建议视口适配
|
||
func (ci *CanvasIntelligence) suggestViewportFit(bounds Bounds, density float64) string {
|
||
width := bounds.MaxX - bounds.MinX
|
||
height := bounds.MaxY - bounds.MinY
|
||
|
||
if width == 0 && height == 0 {
|
||
return "center"
|
||
}
|
||
|
||
if density > 0.7 {
|
||
return "zoom-out" // 密度高,建议缩小
|
||
} else if density < 0.2 {
|
||
return "zoom-in" // 密度低,建议放大
|
||
}
|
||
|
||
aspectRatio := width / height
|
||
if aspectRatio > 2 {
|
||
return "fit-width"
|
||
} else if aspectRatio < 0.5 {
|
||
return "fit-height"
|
||
}
|
||
|
||
return "fit-all"
|
||
}
|
||
|
||
// buildHierarchy 构建层级视图
|
||
func (ci *CanvasIntelligence) buildHierarchy(nodes []model.CanvasNode) HierarchyView {
|
||
nodeMap := make(map[string]model.CanvasNode)
|
||
for _, node := range nodes {
|
||
nodeMap[node.ID] = node
|
||
}
|
||
|
||
// 找到根节点
|
||
rootNodes := make([]string, 0)
|
||
for _, node := range nodes {
|
||
if node.ParentID == nil {
|
||
rootNodes = append(rootNodes, node.ID)
|
||
}
|
||
}
|
||
|
||
// 构建树
|
||
tree := make(map[string]TreeNode)
|
||
maxDepth := 0
|
||
|
||
var buildTree func(nodeID string, depth int, path []string)
|
||
buildTree = func(nodeID string, depth int, path []string) {
|
||
if depth > maxDepth {
|
||
maxDepth = depth
|
||
}
|
||
|
||
currentPath := append(path, nodeID)
|
||
node := nodeMap[nodeID]
|
||
|
||
tree[nodeID] = TreeNode{
|
||
ID: nodeID,
|
||
Depth: depth,
|
||
Children: node.Children,
|
||
Path: currentPath,
|
||
}
|
||
|
||
for _, childID := range node.Children {
|
||
buildTree(childID, depth+1, currentPath)
|
||
}
|
||
}
|
||
|
||
for _, rootID := range rootNodes {
|
||
buildTree(rootID, 0, []string{})
|
||
}
|
||
|
||
return HierarchyView{
|
||
MaxDepth: maxDepth,
|
||
RootNodes: rootNodes,
|
||
Tree: tree,
|
||
}
|
||
}
|
||
|
||
// extractKeywords 提取关键词(简化实现)
|
||
func (ci *CanvasIntelligence) extractKeywords(content string) []string {
|
||
// 移除常见停用词
|
||
stopWords := map[string]bool{
|
||
"the": true, "is": true, "at": true, "which": true, "on": true,
|
||
"a": true, "an": true, "and": true, "or": true, "but": true,
|
||
"in": true, "of": true, "to": true, "for": true, "with": true,
|
||
"这": true, "是": true, "在": true, "的": true, "了": true,
|
||
"和": true, "与": true, "或": true, "但": true, "可以": true,
|
||
}
|
||
|
||
// 简单分词(按空格和标点)
|
||
re := regexp.MustCompile(`[^\w\p{Han}]+`)
|
||
words := re.Split(strings.ToLower(content), -1)
|
||
|
||
// 统计词频
|
||
freq := make(map[string]int)
|
||
for _, word := range words {
|
||
word = strings.TrimSpace(word)
|
||
if len(word) > 1 && !stopWords[word] {
|
||
freq[word]++
|
||
}
|
||
}
|
||
|
||
// 排序并取前5个
|
||
type kv struct {
|
||
Key string
|
||
Value int
|
||
}
|
||
var sorted []kv
|
||
for k, v := range freq {
|
||
sorted = append(sorted, kv{k, v})
|
||
}
|
||
sort.Slice(sorted, func(i, j int) bool {
|
||
return sorted[i].Value > sorted[j].Value
|
||
})
|
||
|
||
keywords := make([]string, 0, 5)
|
||
for i := 0; i < len(sorted) && i < 5; i++ {
|
||
keywords = append(keywords, sorted[i].Key)
|
||
}
|
||
|
||
return keywords
|
||
}
|
||
|
||
// categorizeNode 自动分类节点
|
||
func (ci *CanvasIntelligence) categorizeNode(node model.CanvasNode) string {
|
||
content := strings.ToLower(node.Content)
|
||
|
||
// 基于内容特征的简单分类
|
||
if strings.Contains(content, "todo") || strings.Contains(content, "task") || strings.Contains(content, "待办") {
|
||
return "task"
|
||
}
|
||
if strings.Contains(content, "idea") || strings.Contains(content, "concept") || strings.Contains(content, "想法") || strings.Contains(content, "概念") {
|
||
return "idea"
|
||
}
|
||
if strings.Contains(content, "note") || strings.Contains(content, "memo") || strings.Contains(content, "笔记") {
|
||
return "note"
|
||
}
|
||
if strings.Contains(content, "question") || strings.Contains(content, "问题") || strings.Contains(content, "?") || strings.Contains(content, "?") {
|
||
return "question"
|
||
}
|
||
if strings.Contains(content, "decision") || strings.Contains(content, "决策") || strings.Contains(content, "结论") {
|
||
return "decision"
|
||
}
|
||
|
||
// 基于类型
|
||
switch node.Type {
|
||
case "code":
|
||
return "code"
|
||
case "image", "video", "audio":
|
||
return "media"
|
||
case "link":
|
||
return "reference"
|
||
case "diagram", "flowchart", "mindmap":
|
||
return "diagram"
|
||
default:
|
||
return "general"
|
||
}
|
||
}
|
||
|
||
// analyzeSentiment 分析情感(简化实现)
|
||
func (ci *CanvasIntelligence) analyzeSentiment(content string) string {
|
||
content = strings.ToLower(content)
|
||
|
||
// 正面词汇
|
||
positiveWords := []string{"good", "great", "excellent", "success", "完成", "成功", "优秀", "很好", "✅", "✓"}
|
||
// 负面词汇
|
||
negativeWords := []string{"bad", "error", "fail", "problem", "issue", "bug", "错误", "失败", "问题", "❌", "✗"}
|
||
// TODO词汇
|
||
todoWords := []string{"todo", "task", "待办", "需要", "应该"}
|
||
|
||
positiveScore := 0
|
||
negativeScore := 0
|
||
todoScore := 0
|
||
|
||
for _, word := range positiveWords {
|
||
if strings.Contains(content, word) {
|
||
positiveScore++
|
||
}
|
||
}
|
||
for _, word := range negativeWords {
|
||
if strings.Contains(content, word) {
|
||
negativeScore++
|
||
}
|
||
}
|
||
for _, word := range todoWords {
|
||
if strings.Contains(content, word) {
|
||
todoScore++
|
||
}
|
||
}
|
||
|
||
if todoScore > 0 {
|
||
return "todo"
|
||
}
|
||
if positiveScore > negativeScore {
|
||
return "positive"
|
||
}
|
||
if negativeScore > positiveScore {
|
||
return "negative"
|
||
}
|
||
|
||
return "neutral"
|
||
}
|
||
|
||
// convertPosition 转换位置
|
||
func (ci *CanvasIntelligence) convertPosition(pos *model.CanvasPosition) *Position {
|
||
if pos == nil {
|
||
return nil
|
||
}
|
||
return &Position{X: pos.X, Y: pos.Y}
|
||
}
|
||
|
||
// convertSize 转换尺寸
|
||
func (ci *CanvasIntelligence) convertSize(size *model.CanvasSize) *Size {
|
||
if size == nil {
|
||
return nil
|
||
}
|
||
return &Size{Width: size.Width, Height: size.Height}
|
||
}
|
||
|
||
// getZIndex 获取Z-index
|
||
func (ci *CanvasIntelligence) getZIndex(node model.CanvasNode) int {
|
||
if node.Metadata != nil {
|
||
if zIndex, ok := node.Metadata["zIndex"].(float64); ok {
|
||
return int(zIndex)
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// getParentID 获取父节点ID
|
||
func (ci *CanvasIntelligence) getParentID(parentID *string) string {
|
||
if parentID == nil {
|
||
return ""
|
||
}
|
||
return *parentID
|
||
}
|
||
|
||
// extractTags 提取标签
|
||
func (ci *CanvasIntelligence) extractTags(node model.CanvasNode) []string {
|
||
if node.Metadata != nil {
|
||
if tags, ok := node.Metadata["tags"].([]interface{}); ok {
|
||
result := make([]string, 0, len(tags))
|
||
for _, tag := range tags {
|
||
if s, ok := tag.(string); ok {
|
||
result = append(result, s)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
}
|
||
return []string{}
|
||
}
|
||
|
||
// isRecentlyModified 判断是否最近被修改
|
||
func (ci *CanvasIntelligence) isRecentlyModified(node model.CanvasNode) bool {
|
||
return time.Since(node.UpdatedAt) < 10*time.Minute
|
||
}
|
||
|
||
// getNodeStatus 获取节点状态
|
||
func (ci *CanvasIntelligence) getNodeStatus(node model.CanvasNode) string {
|
||
if node.Metadata != nil {
|
||
if status, ok := node.Metadata["status"].(string); ok {
|
||
return status
|
||
}
|
||
}
|
||
return "draft"
|
||
}
|