Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_pi_canvas_intelligence_helpers.go

870 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
}