Spaces:
Running
Running
| import { TemporalNode, TemporalLink, GraphFrame } from "@/types/temporal"; | |
| /** | |
| * Standardize node data from different sources | |
| */ | |
| export function standardizeNodes(entities: any[]): TemporalNode[] { | |
| if (!Array.isArray(entities)) { | |
| console.warn("Entities is not an array, returning empty array"); | |
| return []; | |
| } | |
| return entities | |
| .filter((entity) => { | |
| if (!entity || typeof entity !== "object") { | |
| console.warn("Skipping invalid entity (not an object):", entity); | |
| return false; | |
| } | |
| if (!entity.id || typeof entity.id !== "string") { | |
| console.warn("Skipping entity with invalid ID:", entity); | |
| return false; | |
| } | |
| return true; | |
| }) | |
| .map((entity) => ({ | |
| id: entity.id, | |
| name: entity.name || entity.id, | |
| type: entity.type || "Unknown", | |
| raw_prompt: entity.raw_prompt || "", | |
| x: entity.x, | |
| y: entity.y, | |
| fx: entity.fx || null, | |
| fy: entity.fy || null, | |
| })); | |
| } | |
| /** | |
| * Standardize link data from different sources | |
| */ | |
| export function standardizeLinks( | |
| relations: any[], | |
| validNodeIds: Set<string> | |
| ): TemporalLink[] { | |
| if (!Array.isArray(relations)) { | |
| console.warn("Relations is not an array, returning empty array"); | |
| return []; | |
| } | |
| return relations | |
| .filter((relation) => { | |
| if (!relation || typeof relation !== "object") { | |
| console.warn("Skipping invalid relation (not an object):", relation); | |
| return false; | |
| } | |
| if (!relation.id || typeof relation.id !== "string") { | |
| console.warn("Skipping relation with invalid ID:", relation); | |
| return false; | |
| } | |
| if (!relation.source || !relation.target) { | |
| console.warn("Skipping relation with missing source/target:", relation); | |
| return false; | |
| } | |
| // Check if both source and target nodes exist | |
| if (!validNodeIds.has(relation.source)) { | |
| console.warn( | |
| `Skipping relation ${relation.id}: source node '${relation.source}' not found` | |
| ); | |
| return false; | |
| } | |
| if (!validNodeIds.has(relation.target)) { | |
| console.warn( | |
| `Skipping relation ${relation.id}: target node '${relation.target}' not found` | |
| ); | |
| return false; | |
| } | |
| return true; | |
| }) | |
| .map((relation) => ({ | |
| id: relation.id, | |
| source: relation.source, | |
| target: relation.target, | |
| type: relation.type || "Unknown", | |
| interaction_prompt: relation.interaction_prompt || "", | |
| value: relation.value || 1, | |
| })); | |
| } | |
| /** | |
| * Validate and clean graph data | |
| */ | |
| export function validateGraphData(data: any): GraphFrame { | |
| const nodes = standardizeNodes(data.entities || data.nodes || []); | |
| const validNodeIds = new Set(nodes.map((node) => node.id)); | |
| const links = standardizeLinks( | |
| data.relations || data.links || [], | |
| validNodeIds | |
| ); | |
| // Log warnings if data was filtered | |
| const originalNodeCount = data.entities?.length || data.nodes?.length || 0; | |
| const originalLinkCount = data.relations?.length || data.links?.length || 0; | |
| if (nodes.length < originalNodeCount) { | |
| console.warn( | |
| `Filtered out ${originalNodeCount - nodes.length} invalid nodes` | |
| ); | |
| } | |
| if (links.length < originalLinkCount) { | |
| console.warn( | |
| `Filtered out ${originalLinkCount - links.length} invalid links` | |
| ); | |
| } | |
| return { | |
| nodes, | |
| links, | |
| metadata: data.metadata || { | |
| entity_count: nodes.length, | |
| relation_count: links.length, | |
| }, | |
| }; | |
| } | |
| /** | |
| * Get node color based on type - Modern design system colors | |
| */ | |
| export function getNodeColor(type?: string): string { | |
| const colors: Record<string, string> = { | |
| // Core entity types - aligned with sidebar design system | |
| Agent: "#ef4444", // Red - matches modern design | |
| Tool: "#10b981", // Emerald - matches sidebar entities | |
| Task: "#3b82f6", // Blue - matches sidebar relations | |
| Input: "#8b5cf6", // Purple - professional accent | |
| Output: "#f59e0b", // Amber - warm accent | |
| Human: "#ec4899", // Pink - human interaction | |
| Entity: "#06b6d4", // Cyan - information | |
| // Semantic types | |
| Person: "#ef4444", // Red - human entities | |
| Organization: "#10b981", // Emerald - structured entities | |
| Location: "#3b82f6", // Blue - geographic | |
| Event: "#8b5cf6", // Purple - temporal | |
| Concept: "#f59e0b", // Amber - abstract | |
| Product: "#ec4899", // Pink - tangible items | |
| // Technical types | |
| Service: "#06b6d4", // Cyan - technical services | |
| Database: "#84cc16", // Lime - data storage | |
| API: "#f97316", // Orange - interfaces | |
| Process: "#6366f1", // Indigo - workflows | |
| }; | |
| return colors[type || "Unknown"] || "#64748b"; // Modern gray default | |
| } | |
| /** | |
| * Get node size based on type | |
| */ | |
| export function getNodeSize(_type?: string): number { | |
| return 75; | |
| } | |
| /** | |
| * Format tooltip content for nodes | |
| */ | |
| export function formatNodeTooltip(node: TemporalNode): string { | |
| return `${node.name || node.id}\nType: ${node.type || "Unknown"}\n${ | |
| node.raw_prompt || "" | |
| }`; | |
| } | |
| /** | |
| * Format tooltip content for links | |
| */ | |
| export function formatLinkTooltip(link: TemporalLink): string { | |
| return `${link.type || "Relation"}\n${link.interaction_prompt || ""}`; | |
| } | |
| export function processLinksForVisualization( | |
| links: TemporalLink[] | |
| ): TemporalLink[] { | |
| // Group links by source-target pairs to handle multiple connections | |
| const linkGroups = new Map<string, TemporalLink[]>(); | |
| links.forEach((link) => { | |
| const sourceId = | |
| typeof link.source === "object" ? link.source.id : link.source; | |
| const targetId = | |
| typeof link.target === "object" ? link.target.id : link.target; | |
| const key = `${sourceId}-${targetId}`; | |
| const reverseKey = `${targetId}-${sourceId}`; | |
| // Use consistent key for bidirectional relationships | |
| const groupKey = sourceId < targetId ? key : reverseKey; | |
| if (!linkGroups.has(groupKey)) { | |
| linkGroups.set(groupKey, []); | |
| } | |
| linkGroups.get(groupKey)!.push(link); | |
| }); | |
| // Assign link indices for visual separation | |
| const processedLinks: TemporalLink[] = []; | |
| linkGroups.forEach((groupLinks, _key) => { | |
| groupLinks.forEach((link, index) => { | |
| processedLinks.push({ | |
| ...link, | |
| linkIndex: index, | |
| groupSize: groupLinks.length, | |
| }); | |
| }); | |
| }); | |
| return processedLinks; | |
| } | |
| /** | |
| * Get relation color based on type | |
| */ | |
| export function getRelationColor(type?: string): string { | |
| const colors: Record<string, string> = { | |
| // Data Flow Relations (Blue family) | |
| CONSUMED_BY: "#3b82f6", // Blue - data input | |
| PRODUCES: "#1d4ed8", // Darker blue - data output | |
| DELIVERS_TO: "#60a5fa", // Light blue - delivery | |
| // Execution Relations (Green family) | |
| PERFORMS: "#10b981", // Emerald - primary execution | |
| ASSIGNED_TO: "#059669", // Dark emerald - assignment | |
| // Tool Relations (Orange family) | |
| USES: "#f59e0b", // Amber - tool usage | |
| REQUIRED_BY: "#d97706", // Dark amber - tool dependency | |
| // Workflow Relations (Purple family) | |
| SUBTASK_OF: "#8b5cf6", // Violet - hierarchy | |
| NEXT: "#7c3aed", // Dark violet - sequence | |
| // Human Relations (Pink family) | |
| INTERVENES: "#ec4899", // Pink - human intervention | |
| REVIEWS: "#db2777", // Dark pink - human review | |
| }; | |
| return colors[type || "Unknown"] || "#64748b"; // Default gray | |
| } | |
| /** | |
| * Get relation line style based on type | |
| */ | |
| export function getRelationLineStyle(type?: string): string { | |
| const styles: Record<string, string> = { | |
| // Primary actions - solid thick lines | |
| PERFORMS: "solid", | |
| ASSIGNED_TO: "solid", | |
| // Data flow - dotted lines | |
| CONSUMED_BY: "dotted", | |
| PRODUCES: "dotted", | |
| DELIVERS_TO: "dotted", | |
| // Tool usage - dashed lines | |
| USES: "dashed", | |
| REQUIRED_BY: "dashed", | |
| // Workflow - solid thin lines | |
| SUBTASK_OF: "solid", | |
| NEXT: "solid", | |
| // Human interactions - solid lines | |
| INTERVENES: "solid", | |
| REVIEWS: "solid", | |
| }; | |
| return styles[type || "Unknown"] || "solid"; | |
| } | |
| /** | |
| * Get relation width multiplier based on importance and type | |
| */ | |
| export function getRelationWidth(type?: string, importance?: string): number { | |
| // Base width multipliers by type | |
| const typeWidths: Record<string, number> = { | |
| PERFORMS: 1.2, // Thicker for primary actions | |
| ASSIGNED_TO: 1.1, | |
| CONSUMED_BY: 1.0, // Standard for data flow | |
| PRODUCES: 1.0, | |
| DELIVERS_TO: 0.9, | |
| USES: 0.8, // Thinner for tool usage | |
| REQUIRED_BY: 0.8, | |
| SUBTASK_OF: 0.7, // Thinnest for workflow | |
| NEXT: 0.7, | |
| INTERVENES: 1.1, // Prominent for human actions | |
| REVIEWS: 1.0, | |
| }; | |
| // Importance multipliers | |
| const importanceMultipliers: Record<string, number> = { | |
| CRITICAL: 1.5, | |
| HIGH: 1.2, | |
| MEDIUM: 1.0, | |
| LOW: 0.8, | |
| NEGLIGIBLE: 0.6, | |
| }; | |
| const baseWidth = typeWidths[type || "Unknown"] || 1.0; | |
| const importanceMultiplier = | |
| importanceMultipliers[importance || "MEDIUM"] || 1.0; | |
| return baseWidth * importanceMultiplier; | |
| } | |
| /** | |
| * Get node shape based on type for better visual differentiation | |
| */ | |
| export function getNodeShape(_type?: string): string { | |
| return "ellipse"; | |
| } | |
| /** | |
| * Get border style based on entity importance | |
| */ | |
| export function getNodeBorderStyle(importance?: string): string { | |
| const styles: Record<string, string> = { | |
| CRITICAL: "solid", // Thick solid border | |
| HIGH: "dashed", // Dashed border | |
| MEDIUM: "solid", // Normal solid border | |
| LOW: "dotted", // Dotted border | |
| NEGLIGIBLE: "dotted", // Light dotted border | |
| }; | |
| return styles[importance || "MEDIUM"] || "solid"; | |
| } | |
| /** | |
| * Get border width based on importance | |
| */ | |
| export function getNodeBorderWidth(importance?: string): number { | |
| const widths: Record<string, number> = { | |
| CRITICAL: 4, // Very thick | |
| HIGH: 3, // Thick | |
| MEDIUM: 2, // Normal | |
| LOW: 1, // Thin | |
| NEGLIGIBLE: 1, // Very thin | |
| }; | |
| return widths[importance || "MEDIUM"] || 2; | |
| } | |
| /** | |
| * Get text styling based on node type | |
| */ | |
| export function getNodeTextStyle(type?: string): { | |
| weight: string; | |
| transform: string; | |
| decoration: string; | |
| } { | |
| const styles: Record< | |
| string, | |
| { weight: string; transform: string; decoration: string } | |
| > = { | |
| Agent: { weight: "bold", transform: "uppercase", decoration: "none" }, | |
| Human: { weight: "bold", transform: "capitalize", decoration: "none" }, | |
| Task: { weight: "600", transform: "none", decoration: "none" }, | |
| Tool: { weight: "500", transform: "lowercase", decoration: "none" }, | |
| Input: { weight: "400", transform: "none", decoration: "none" }, | |
| Output: { weight: "600", transform: "none", decoration: "underline" }, | |
| }; | |
| return ( | |
| styles[type || "Unknown"] || { | |
| weight: "500", | |
| transform: "none", | |
| decoration: "none", | |
| } | |
| ); | |
| } | |
| /** | |
| * Get shadow configuration based on node type | |
| */ | |
| export function getNodeShadow(type?: string): { | |
| blur: number; | |
| color: string; | |
| offsetX: number; | |
| offsetY: number; | |
| opacity: number; | |
| } { | |
| const shadows: Record<string, any> = { | |
| Agent: { | |
| blur: 16, | |
| color: "rgba(0, 0, 0, 0.25)", | |
| offsetX: 0, | |
| offsetY: 6, | |
| opacity: 0.4, | |
| }, | |
| Human: { | |
| blur: 20, | |
| color: "rgba(236, 72, 153, 0.3)", | |
| offsetX: 0, | |
| offsetY: 8, | |
| opacity: 0.5, | |
| }, | |
| Task: { | |
| blur: 12, | |
| color: "rgba(0, 0, 0, 0.15)", | |
| offsetX: 0, | |
| offsetY: 4, | |
| opacity: 0.3, | |
| }, | |
| Tool: { | |
| blur: 8, | |
| color: "rgba(245, 158, 11, 0.3)", | |
| offsetX: 2, | |
| offsetY: 2, | |
| opacity: 0.4, | |
| }, | |
| Input: { | |
| blur: 6, | |
| color: "rgba(139, 92, 246, 0.2)", | |
| offsetX: -2, | |
| offsetY: 2, | |
| opacity: 0.3, | |
| }, | |
| Output: { | |
| blur: 14, | |
| color: "rgba(239, 68, 68, 0.3)", | |
| offsetX: 2, | |
| offsetY: 6, | |
| opacity: 0.4, | |
| }, | |
| }; | |
| return ( | |
| shadows[type || "Unknown"] || { | |
| blur: 12, | |
| color: "rgba(0, 0, 0, 0.15)", | |
| offsetX: 0, | |
| offsetY: 4, | |
| opacity: 0.3, | |
| } | |
| ); | |
| } | |
| /** | |
| * Get high-contrast text color for relation labels | |
| */ | |
| export function getRelationTextColor(type?: string): string { | |
| const textColors: Record<string, string> = { | |
| // Data Flow Relations - Dark blue family for good contrast | |
| CONSUMED_BY: "#1e3a8a", // Dark blue | |
| PRODUCES: "#1e2a5e", // Very dark blue | |
| DELIVERS_TO: "#1e40af", // Medium dark blue | |
| // Execution Relations - Dark green family | |
| PERFORMS: "#047857", // Dark emerald | |
| ASSIGNED_TO: "#064e3b", // Very dark emerald | |
| // Tool Relations - Dark orange/amber family | |
| USES: "#b45309", // Dark amber | |
| REQUIRED_BY: "#92400e", // Very dark amber | |
| // Workflow Relations - Dark purple family | |
| SUBTASK_OF: "#6b21a8", // Dark violet | |
| NEXT: "#581c87", // Very dark violet | |
| // Human Relations - Dark pink family | |
| INTERVENES: "#be185d", // Dark pink | |
| REVIEWS: "#9d174d", // Very dark pink | |
| }; | |
| return textColors[type || "Unknown"] || "#374151"; // Default dark gray | |
| } | |