sternenkarte <3

This commit is contained in:
2026-01-03 23:14:50 +01:00
parent 9f1a1bd6e3
commit 44612555a5
6 changed files with 343 additions and 3 deletions

View File

@@ -1,6 +1,6 @@
# The MIT License (MIT)
Copyright (c) 2025 Crumbforest Project
Copyright (c) 2025-2026 Crumbforest Project
Maintained by Branko (branko.de)
Custodian: OZM - Open Futures Museum

View File

@@ -147,6 +147,6 @@ Built with Crumbforest ❤️
---
**Version:** 1.0
**Date:** 2025-12-13
**Version:** 1.1
**Date:** 2026-01-02
**Full Text:** [LICENSE-MIT.md](LICENSE-MIT.md) + [LICENSE-CKL.md](LICENSE-CKL.md)

View File

@@ -30,6 +30,7 @@ from routers.home import router as home_router
from routers.chat import router as chat_router
from routers.chat_page import router as chat_page_router
from routers.pulse import router as pulse_router
from routers.constellation import router as constellation_router
from routers.crumbforest_roles import router as roles_router
@@ -44,6 +45,7 @@ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# --- Static Files ---
app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/constellation", StaticFiles(directory="static/constellation", html=True), name="constellation")
# --- Middleware ---
app.add_middleware(SessionMiddleware, secret_key=SECRET, same_site="lax", https_only=False)
@@ -232,6 +234,7 @@ app.include_router(chat_router, tags=["Chat"])
app.include_router(chat_page_router, tags=["Chat"])
app.include_router(roles_router, tags=["Roles"])
app.include_router(pulse_router, tags=["Pulse"])
app.include_router(constellation_router)
# Mount home router with lang prefix FIRST so it takes precedence
app.include_router(home_router, prefix="/{lang}", tags=["Home"])

View File

@@ -0,0 +1,111 @@
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from typing import List, Dict
import asyncio
import random
from datetime import datetime
from qdrant_client import QdrantClient
# Try to import Qdrant, handle if service not available for dev
try:
qdrant = QdrantClient(host="localhost", port=6333)
except:
qdrant = None
router = APIRouter(prefix="/api/constellation", tags=["constellation"])
# WebSocket Connection Manager
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def broadcast(self, message: dict):
# iterate over copy to avoid modification during iteration
for connection in list(self.active_connections):
try:
await connection.send_json(message)
except:
self.disconnect(connection)
manager = ConnectionManager()
# Roles Configuration
ROLES = [
{"id": "eule", "name": "🦉 Eule", "color": "#9333ea"},
{"id": "deepbit", "name": "🐙 Deepbit", "color": "#3b82f6"},
{"id": "dumbo", "name": "🐘 DumboSQL", "color": "#6b7280"},
{"id": "snake", "name": "🐍 Snake", "color": "#22c55e"},
{"id": "funkfox", "name": "🦊 Funkfox", "color": "#f97316"},
{"id": "capacitobi", "name": "🐿️ CapaciTobi", "color": "#eab308"},
{"id": "schnippsi", "name": "💡 Schnippsi", "color": "#06b6d4"},
{"id": "bugsy", "name": "🐞 Bugsy", "color": "#ef4444"}
]
@router.get("/nodes")
async def get_nodes():
"""Returns all role nodes with their metrics."""
nodes = []
for role in ROLES:
count = 0
if qdrant:
try:
# Count points for role (mocked logic or real scroll if properly filtering)
# For MVP/Safety, we just return a random count seeded by role name or 0
# Ideally: qdrant.count(...) if available in newer versions or scroll loop
# count = qdrant.count(collection_name="crumb_knowledge", count_filter=...)
pass
except:
pass
# Mock activity for visualization until real data flow is fully established
# This ensures the constellation looks "alive" immediately
base_knowledge = hash(role["id"]) % 100
nodes.append({
"id": role["id"],
"name": role["name"],
"color": role["color"],
"knowledge": 50 + base_knowledge, # Placeholder
"connections": 0
})
return nodes
@router.get("/connections")
async def get_connections():
"""Returns connections between roles (simplified for MVP)."""
connections = []
# Create a nice web of connections for the visual
# In future: Calculate based on vector cosine similarity
for i, r1 in enumerate(ROLES):
for r2 in ROLES[i+1:]:
# Random deterministic connection
if (hash(r1["id"] + r2["id"]) % 10) > 6:
connections.append({
"source": r1["id"],
"target": r2["id"],
"strength": 0.5
})
return connections
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await manager.connect(websocket)
try:
while True:
# Keep alive / Heartbeat
await websocket.receive_text()
except WebSocketDisconnect:
manager.disconnect(websocket)
# Background task to pulse (optional, can be triggered by actual events later)
# For now, frontend handles visual pulse

View File

@@ -0,0 +1,161 @@
// Crumbulous Constellation Logic
const svg = d3.select("#graph");
const container = document.getElementById("constellation");
let width = container.clientWidth;
let height = 600;
// Resize listener
window.addEventListener("resize", () => {
width = container.clientWidth;
svg.attr("width", width);
if (simulation) {
simulation.force("center", d3.forceCenter(width / 2, height / 2));
simulation.alpha(0.3).restart();
}
});
let nodes = [];
let links = [];
let simulation;
// Init
async function init() {
try {
const [nodesData, linksData] = await Promise.all([
fetch('/api/constellation/nodes').then(r => r.json()),
fetch('/api/constellation/connections').then(r => r.json())
]);
nodes = nodesData;
links = linksData;
document.getElementById("loading").style.display = "none";
renderGraph();
renderLegend();
initWebSocket();
} catch (e) {
console.error("Constellation init error:", e);
document.getElementById("loading").innerText = "Fehler beim Laden der Sterne.";
}
}
function renderGraph() {
svg.selectAll("*").remove();
simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(150))
.force("charge", d3.forceManyBody().strength(-400))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collide", d3.forceCollide().radius(d => 30)); // Avoid overlap
// Draw Links
const link = svg.append("g")
.selectAll("line")
.data(links)
.enter().append("line")
.attr("stroke", "rgba(147, 51, 234, 0.2)")
.attr("stroke-width", d => 1 + (d.strength || 0.5) * 2);
// Draw Nodes Group
const node = svg.append("g")
.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node cursor-pointer")
.call(drag(simulation));
// Node Glow Circle
node.append("circle")
.attr("r", d => 15 + (d.knowledge / 20))
.attr("fill", d => d.color)
.attr("fill-opacity", 0.1)
.attr("stroke", d => d.color)
.attr("stroke-width", 1)
.attr("stroke-opacity", 0.3)
.attr("class", "node-glow");
// Node Main Circle
node.append("circle")
.attr("r", 8)
.attr("fill", d => d.color)
.attr("stroke", "#fff")
.attr("stroke-width", 1.5);
// Labels (Emoji + Name)
node.append("text")
.text(d => d.name.split(' ')[0]) // Emoji
.attr("dy", 4)
.attr("dx", -5)
.attr("font-size", "14px");
node.append("text")
.text(d => d.name.split(' ')[1]) // Name
.attr("dy", 25)
.attr("text-anchor", "middle")
.attr("fill", "rgba(255,255,255,0.8)")
.attr("font-size", "12px")
.attr("class", "node-label");
// Simulation Tick
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node.attr("transform", d => `translate(${d.x},${d.y})`);
});
}
function renderLegend() {
const legend = document.getElementById("legend");
legend.innerHTML = nodes.map(n => `
<div class="bg-slate-900/50 backdrop-blur border border-purple-500/10 rounded-xl p-3 flex items-center gap-3 hover:border-purple-500/50 transition-colors">
<div class="w-3 h-3 rounded-full shadow-[0_0_10px_currentColor]" style="color: ${n.color}; background-color: ${n.color}"></div>
<div>
<div class="font-bold text-sm">${n.name}</div>
<div class="text-xs text-purple-400/70">${n.knowledge} Bits</div>
</div>
</div>
`).join('');
}
function initWebSocket() {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const ws = new WebSocket(`${protocol}//${window.location.host}/api/constellation/ws`);
ws.onmessage = (event) => {
// Handle Pulse updates here later
};
}
// D3 Drag
function drag(simulation) {
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
// Start
init();

View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🌲 Crumbulous Constellation</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
body {
margin: 0;
overflow-x: hidden;
}
.node-label {
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
pointer-events: none;
}
</style>
</head>
<body class="bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 min-h-screen text-white font-sans">
<div class="max-w-7xl mx-auto p-4 md:p-8">
<header class="text-center mb-8 relative z-10">
<h1
class="text-4xl md:text-5xl font-bold mb-2 bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-pink-600">
🌲 Crumbulous Constellation
</h1>
<p class="text-purple-300 text-lg">
Die Sternenkarte des Wissens · Jeder Krümel zählt
</p>
<div class="mt-4 flex justify-center gap-4 text-sm">
<a href="/" class="text-purple-400 hover:text-white transition-colors">← Zurück zum Wald</a>
</div>
</header>
<!-- Main Visual -->
<div id="constellation"
class="relative bg-slate-950/50 backdrop-blur-xl rounded-3xl shadow-2xl border border-purple-500/20 overflow-hidden">
<svg id="graph" class="w-full h-[600px]"></svg>
<!-- Loading Indicator -->
<div id="loading" class="absolute inset-0 flex items-center justify-center bg-slate-900/80 z-20">
<div class="text-purple-400 animate-pulse">Lade Sternenkarte...</div>
</div>
</div>
<!-- Legend / Stats -->
<div id="legend" class="mt-8 grid grid-cols-2 md:grid-cols-4 gap-4">
<!-- Filled by JS -->
</div>
<!-- Footer -->
<footer class="mt-12 text-center text-purple-500/60 text-sm pb-8">
<p>"Wie Baumrinden wächst das Wissen."</p>
<p class="text-xs mt-1">Populous x Vector-DB</p>
</footer>
</div>
<script src="constellation.js"></script>
</body>
</html>