Spaces:
Running
Running
| /** | |
| * AI Town — Relationship Graph Visualization | |
| * ============================================= | |
| * Shows connections between characters with sentiment colors. | |
| */ | |
| import React from "react"; | |
| import { View, Text, ScrollView } from "react-native"; | |
| import { Canvas, Circle, Line, vec } from "@shopify/react-native-skia"; | |
| interface Relationship { | |
| fromCharacterId: string; | |
| toCharacterId: string; | |
| sentiment: number; | |
| trust: number; | |
| interactionCount: number; | |
| relationshipType: string; | |
| } | |
| interface Character { | |
| _id: string; | |
| name: string; | |
| avatar: string; | |
| mood: string; | |
| } | |
| interface RelationshipGraphProps { | |
| characters: Character[]; | |
| relationships: Relationship[]; | |
| } | |
| export function RelationshipGraph({ characters, relationships }: RelationshipGraphProps) { | |
| if (characters.length === 0) { | |
| return ( | |
| <View className="flex-1 items-center justify-center py-20"> | |
| <Text className="text-gray-400">No characters yet</Text> | |
| </View> | |
| ); | |
| } | |
| // Arrange characters in a circle | |
| const centerX = 160; | |
| const centerY = 160; | |
| const radius = 120; | |
| const positions = characters.map((_, i) => { | |
| const angle = (i / characters.length) * Math.PI * 2 - Math.PI / 2; | |
| return { | |
| x: centerX + Math.cos(angle) * radius, | |
| y: centerY + Math.sin(angle) * radius, | |
| }; | |
| }); | |
| return ( | |
| <View className="bg-gray-900 rounded-2xl p-4"> | |
| <Text className="text-white font-bold text-lg mb-3">🕸️ Relationship Web</Text> | |
| <Canvas style={{ width: 320, height: 320 }}> | |
| {/* Draw relationship edges */} | |
| {relationships.map((rel, i) => { | |
| const fromIdx = characters.findIndex((c) => c._id === rel.fromCharacterId); | |
| const toIdx = characters.findIndex((c) => c._id === rel.toCharacterId); | |
| if (fromIdx === -1 || toIdx === -1) return null; | |
| const color = rel.sentiment > 0.5 ? "#22c55e" : rel.sentiment > 0 ? "#60a5fa" : rel.sentiment > -0.3 ? "#f59e0b" : "#ef4444"; | |
| const opacity = Math.min(1, Math.abs(rel.sentiment) + 0.3); | |
| return ( | |
| <Line | |
| key={`edge-${i}`} | |
| p1={vec(positions[fromIdx].x, positions[fromIdx].y)} | |
| p2={vec(positions[toIdx].x, positions[toIdx].y)} | |
| color={color} | |
| strokeWidth={Math.max(1, rel.interactionCount * 0.5)} | |
| opacity={opacity} | |
| /> | |
| ); | |
| })} | |
| {/* Draw character nodes */} | |
| {positions.map((pos, i) => ( | |
| <Circle key={`node-${i}`} cx={pos.x} cy={pos.y} r={20} color="#374151" /> | |
| ))} | |
| {positions.map((pos, i) => ( | |
| <Circle key={`inner-${i}`} cx={pos.x} cy={pos.y} r={16} color="#1f2937" /> | |
| ))} | |
| </Canvas> | |
| {/* Character labels */} | |
| <View className="flex-row flex-wrap gap-2 mt-3 justify-center"> | |
| {characters.map((char, i) => ( | |
| <View key={char._id} className="flex-row items-center gap-1 bg-gray-800 px-2 py-1 rounded-full"> | |
| <Text className="text-sm">{char.avatar}</Text> | |
| <Text className="text-gray-300 text-xs">{char.name}</Text> | |
| </View> | |
| ))} | |
| </View> | |
| {/* Legend */} | |
| <View className="flex-row justify-center gap-4 mt-3"> | |
| <View className="flex-row items-center gap-1"> | |
| <View className="w-3 h-3 rounded-full bg-green-500" /> | |
| <Text className="text-gray-400 text-xs">Close</Text> | |
| </View> | |
| <View className="flex-row items-center gap-1"> | |
| <View className="w-3 h-3 rounded-full bg-blue-500" /> | |
| <Text className="text-gray-400 text-xs">Friendly</Text> | |
| </View> | |
| <View className="flex-row items-center gap-1"> | |
| <View className="w-3 h-3 rounded-full bg-amber-500" /> | |
| <Text className="text-gray-400 text-xs">Neutral</Text> | |
| </View> | |
| <View className="flex-row items-center gap-1"> | |
| <View className="w-3 h-3 rounded-full bg-red-500" /> | |
| <Text className="text-gray-400 text-xs">Tense</Text> | |
| </View> | |
| </View> | |
| </View> | |
| ); | |
| } | |