Spaces:
Running
Running
Komalpreet Kaur commited on
feat: implement memory consolidation service with sleep cycle, add frontend visualization components, and integrate backend database orchestration.
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- app/api/auth_router.py +1 -1
- app/api/endpoints.py +101 -11
- app/db/chroma.py +25 -0
- app/db/session.py +1 -1
- app/main.py +8 -0
- app/services/brain_trace.py +1 -1
- app/services/memory.py +5 -5
- app/services/orchestrator.py +20 -22
- app/services/sleep_cycle.py +2 -2
- frontend/package-lock.json +133 -0
- frontend/package.json +1 -0
- frontend/src/App.css +222 -19
- frontend/src/App.jsx +125 -80
- frontend/src/assets/{hero.png → brain/Brain.png} +2 -2
- frontend/src/assets/brain/Brain_nobg.png +3 -0
- frontend/src/assets/brain/sleep.png +3 -0
- frontend/src/assets/brain/sleep_nobg.png +3 -0
- frontend/src/components/ChatPanel.css +184 -25
- frontend/src/components/ChatPanel.jsx +25 -9
- frontend/src/components/CognitiveBrain3DScene.css +110 -0
- frontend/src/components/CognitiveBrain3DScene.jsx +256 -0
- frontend/src/components/CognitiveBrainImageScene.jsx +43 -43
- frontend/src/components/CognitiveBrainScene.css +87 -42
- frontend/src/components/CognitiveDashboard.css +66 -64
- frontend/src/components/CognitiveDashboard.jsx +56 -15
- frontend/src/components/CognitiveTimeline.css +7 -11
- frontend/src/components/CognitiveTimeline.jsx +32 -5
- frontend/src/components/DreamSequence.css +402 -107
- frontend/src/components/DreamSequence.jsx +158 -56
- frontend/src/components/KnowledgeGraph.css +310 -103
- frontend/src/components/KnowledgeGraph.jsx +338 -82
- frontend/src/components/KnowledgeInput.css +331 -64
- frontend/src/components/KnowledgeInput.jsx +207 -26
- frontend/src/components/MemoryExplorer.css +405 -88
- frontend/src/components/MemoryExplorer.jsx +309 -108
- frontend/src/index.css +647 -1
- scratch/check_deps.py +15 -0
- scratch/check_graph_nodes.py +18 -0
- scratch/check_soma.py +10 -0
- scratch/color_to_alpha.py +63 -0
- scratch/copy_alpha.py +34 -0
- scratch/cutout_brain.py +66 -0
- scratch/ellipse_cutout.py +48 -0
- scratch/fill_holes.py +56 -0
- scratch/key_bg.py +29 -0
- scratch/morph_cutout.py +83 -0
- scratch/morph_cutout_v2.py +83 -0
- scratch/perfect_cutout.py +72 -0
- scratch/perfect_cutout_v2.py +71 -0
- scratch/perfect_cutout_v3.py +87 -0
app/api/auth_router.py
CHANGED
|
@@ -22,7 +22,7 @@ class AuthRequest(BaseModel):
|
|
| 22 |
def validate_username(cls, v: str) -> str:
|
| 23 |
v = v.strip()
|
| 24 |
if not 3 <= len(v) <= 30:
|
| 25 |
-
raise ValueError("Username must be 3
|
| 26 |
if not re.match(r"^[a-zA-Z0-9_]+$", v):
|
| 27 |
raise ValueError("Username can only contain letters, numbers, and underscores")
|
| 28 |
return v.lower()
|
|
|
|
| 22 |
def validate_username(cls, v: str) -> str:
|
| 23 |
v = v.strip()
|
| 24 |
if not 3 <= len(v) <= 30:
|
| 25 |
+
raise ValueError("Username must be 3-30 characters")
|
| 26 |
if not re.match(r"^[a-zA-Z0-9_]+$", v):
|
| 27 |
raise ValueError("Username can only contain letters, numbers, and underscores")
|
| 28 |
return v.lower()
|
app/api/endpoints.py
CHANGED
|
@@ -164,6 +164,58 @@ async def process_ingest(request: IngestRequest, current_user: str = Depends(get
|
|
| 164 |
|
| 165 |
# ── Stream Query ──────────────────────────────────────────────────
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
@router.post("/query/stream")
|
| 168 |
async def process_query_stream(request: QueryRequest, current_user: str = Depends(get_current_user)):
|
| 169 |
async def event_generator():
|
|
@@ -192,7 +244,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
|
|
| 192 |
data={"query": request.text}
|
| 193 |
))
|
| 194 |
yield sse_event("trace", {"phase": "perception", "message": perception_msg, "data": {"query": request.text}})
|
| 195 |
-
await asyncio.sleep(0.
|
| 196 |
|
| 197 |
yield sse_event("brain_trace", build_brain_event(
|
| 198 |
"attention",
|
|
@@ -207,7 +259,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
|
|
| 207 |
"message": f"Attention salience computed at {attention['salience']}%.",
|
| 208 |
"data": attention
|
| 209 |
})
|
| 210 |
-
await asyncio.sleep(0.
|
| 211 |
|
| 212 |
if attention["emotional_intensity"] >= 70:
|
| 213 |
yield sse_event("brain_trace", build_brain_event(
|
|
@@ -223,7 +275,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
|
|
| 223 |
"message": f"Amygdala analogue flagged {attention['emotion_label']} salience.",
|
| 224 |
"data": {"emotion": attention["emotion_label"]}
|
| 225 |
})
|
| 226 |
-
await asyncio.sleep(0.
|
| 227 |
|
| 228 |
yield sse_event("brain_trace", build_brain_event(
|
| 229 |
"routing",
|
|
@@ -238,7 +290,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
|
|
| 238 |
"message": f"Routed cognition through {', '.join(routing['regions'])}.",
|
| 239 |
"data": {"regions": routing["regions"]}
|
| 240 |
})
|
| 241 |
-
await asyncio.sleep(0.
|
| 242 |
|
| 243 |
yield sse_event("brain_trace", build_brain_event(
|
| 244 |
"prediction",
|
|
@@ -253,7 +305,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
|
|
| 253 |
"message": prediction["intent"],
|
| 254 |
"data": prediction
|
| 255 |
})
|
| 256 |
-
await asyncio.sleep(0.
|
| 257 |
|
| 258 |
yield sse_event("brain_trace", build_brain_event(
|
| 259 |
"working_memory",
|
|
@@ -268,7 +320,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
|
|
| 268 |
"message": f"Loaded {len(history)} recent messages into working memory.",
|
| 269 |
"data": {"history_count": len(history)}
|
| 270 |
})
|
| 271 |
-
await asyncio.sleep(0.
|
| 272 |
|
| 273 |
for output in orchestrator.stream(state_input):
|
| 274 |
for node_name, node_output in output.items():
|
|
@@ -284,7 +336,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
|
|
| 284 |
data={"reflection": reflection}
|
| 285 |
))
|
| 286 |
yield sse_event("trace", {"phase": "reflection", "message": "Intent map formed.", "data": {"reflection": reflection}})
|
| 287 |
-
await asyncio.sleep(0.
|
| 288 |
|
| 289 |
elif node_name == "retrieve":
|
| 290 |
trace_data = node_output.get("trace_data", {})
|
|
@@ -302,7 +354,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
|
|
| 302 |
}
|
| 303 |
))
|
| 304 |
yield sse_event("trace", {"phase": "recall", "message": recall_msg, "data": node_output.get("context")})
|
| 305 |
-
await asyncio.sleep(0.
|
| 306 |
|
| 307 |
suppressed_sensory = trace_data.get("suppressed_sensory", 0)
|
| 308 |
suppressed_graph = trace_data.get("suppressed_graph", 0)
|
|
@@ -325,7 +377,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
|
|
| 325 |
"suppressed_graph": suppressed_graph,
|
| 326 |
}
|
| 327 |
})
|
| 328 |
-
await asyncio.sleep(0.
|
| 329 |
|
| 330 |
yield sse_event("brain_trace", build_brain_event(
|
| 331 |
"association",
|
|
@@ -339,7 +391,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
|
|
| 339 |
}
|
| 340 |
))
|
| 341 |
yield sse_event("trace", {"phase": "association", "message": assoc_msg, "data": node_output.get("graph_context"), "touched": trace_data.get("touched")})
|
| 342 |
-
await asyncio.sleep(0.
|
| 343 |
|
| 344 |
elif node_name == "call_model":
|
| 345 |
reason_msg = "Synthesizing final response via Cortex Node..."
|
|
@@ -352,7 +404,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
|
|
| 352 |
data={"prediction": prediction["intent"]}
|
| 353 |
))
|
| 354 |
yield sse_event("trace", {"phase": "reasoning", "message": reason_msg})
|
| 355 |
-
await asyncio.sleep(0.
|
| 356 |
|
| 357 |
final_response = node_output.get("response", "")
|
| 358 |
add_message(current_user, "user", request.text)
|
|
@@ -366,6 +418,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
|
|
| 366 |
inputs_used=["response_plan"],
|
| 367 |
data={"response_preview": final_response[:120]}
|
| 368 |
))
|
|
|
|
| 369 |
yield sse_event("final_result", {"response": final_response})
|
| 370 |
|
| 371 |
# Build neural mesh AFTER streaming the response so
|
|
@@ -431,6 +484,31 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
|
|
| 431 |
|
| 432 |
# ── Memory Explorer ─────────────────────────────────────────────
|
| 433 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
@router.get("/memory/sensory")
|
| 435 |
async def get_sensory_memories(current_user: str = Depends(get_current_user)):
|
| 436 |
try:
|
|
@@ -451,6 +529,18 @@ async def get_sensory_memories(current_user: str = Depends(get_current_user)):
|
|
| 451 |
raise HTTPException(status_code=500, detail=str(e))
|
| 452 |
|
| 453 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
# ── History ───────────────────────────────────────────────────────
|
| 455 |
|
| 456 |
@router.get("/history")
|
|
|
|
| 164 |
|
| 165 |
# ── Stream Query ──────────────────────────────────────────────────
|
| 166 |
|
| 167 |
+
@router.post("/analyze")
|
| 168 |
+
async def analyze_text(request: QueryRequest, current_user: str = Depends(get_current_user)):
|
| 169 |
+
"""
|
| 170 |
+
Analyzes text to preview potential semantic links and cognitive metrics.
|
| 171 |
+
Checks existing graph to find potential overlaps.
|
| 172 |
+
"""
|
| 173 |
+
try:
|
| 174 |
+
from langchain_groq import ChatGroq
|
| 175 |
+
from langchain_core.messages import HumanMessage
|
| 176 |
+
from app.core.config import settings
|
| 177 |
+
|
| 178 |
+
api_key = settings.GROQ_API_KEY if settings.GROQ_API_KEY else "dummy_key"
|
| 179 |
+
llm = ChatGroq(model="llama-3.1-8b-instant", api_key=api_key)
|
| 180 |
+
|
| 181 |
+
# 1. Extract potential entities
|
| 182 |
+
prompt = f"Extract 5-8 key entities (names, concepts, places) from this text as a comma-separated list. Return ONLY the list: {request.text}"
|
| 183 |
+
response = await llm.ainvoke([HumanMessage(content=prompt)])
|
| 184 |
+
entities = [e.strip() for e in response.content.split(',') if e.strip()]
|
| 185 |
+
|
| 186 |
+
# 2. Check for existing overlaps in Neo4j
|
| 187 |
+
existing_links = []
|
| 188 |
+
if neo4j_db.driver:
|
| 189 |
+
# Look for entities that already exist for this user
|
| 190 |
+
check_query = """
|
| 191 |
+
MATCH (n:Entity)
|
| 192 |
+
WHERE n.user_id = $user_id AND toLower(n.name) IN $entities
|
| 193 |
+
RETURN n.name AS name, count{(n)--()} AS connections
|
| 194 |
+
"""
|
| 195 |
+
overlaps = neo4j_db.query(check_query, {
|
| 196 |
+
"user_id": current_user,
|
| 197 |
+
"entities": [e.lower() for e in entities]
|
| 198 |
+
}) or []
|
| 199 |
+
existing_links = [{"name": o["name"], "connections": o["connections"]} for o in overlaps]
|
| 200 |
+
|
| 201 |
+
# 3. Calculate metrics
|
| 202 |
+
char_count = len(request.text)
|
| 203 |
+
chunk_count = (char_count // 500) + 1
|
| 204 |
+
|
| 205 |
+
return {
|
| 206 |
+
"entities": entities,
|
| 207 |
+
"existing_links": existing_links,
|
| 208 |
+
"metrics": {
|
| 209 |
+
"density": min(char_count / 2000, 1.0),
|
| 210 |
+
"chunks": chunk_count,
|
| 211 |
+
"estimated_links": len(entities) * 1.5,
|
| 212 |
+
"reinforcement_index": len(existing_links) / max(len(entities), 1)
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
except Exception as e:
|
| 216 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 217 |
+
|
| 218 |
+
|
| 219 |
@router.post("/query/stream")
|
| 220 |
async def process_query_stream(request: QueryRequest, current_user: str = Depends(get_current_user)):
|
| 221 |
async def event_generator():
|
|
|
|
| 244 |
data={"query": request.text}
|
| 245 |
))
|
| 246 |
yield sse_event("trace", {"phase": "perception", "message": perception_msg, "data": {"query": request.text}})
|
| 247 |
+
await asyncio.sleep(0.4)
|
| 248 |
|
| 249 |
yield sse_event("brain_trace", build_brain_event(
|
| 250 |
"attention",
|
|
|
|
| 259 |
"message": f"Attention salience computed at {attention['salience']}%.",
|
| 260 |
"data": attention
|
| 261 |
})
|
| 262 |
+
await asyncio.sleep(0.4)
|
| 263 |
|
| 264 |
if attention["emotional_intensity"] >= 70:
|
| 265 |
yield sse_event("brain_trace", build_brain_event(
|
|
|
|
| 275 |
"message": f"Amygdala analogue flagged {attention['emotion_label']} salience.",
|
| 276 |
"data": {"emotion": attention["emotion_label"]}
|
| 277 |
})
|
| 278 |
+
await asyncio.sleep(0.4)
|
| 279 |
|
| 280 |
yield sse_event("brain_trace", build_brain_event(
|
| 281 |
"routing",
|
|
|
|
| 290 |
"message": f"Routed cognition through {', '.join(routing['regions'])}.",
|
| 291 |
"data": {"regions": routing["regions"]}
|
| 292 |
})
|
| 293 |
+
await asyncio.sleep(0.4)
|
| 294 |
|
| 295 |
yield sse_event("brain_trace", build_brain_event(
|
| 296 |
"prediction",
|
|
|
|
| 305 |
"message": prediction["intent"],
|
| 306 |
"data": prediction
|
| 307 |
})
|
| 308 |
+
await asyncio.sleep(0.4)
|
| 309 |
|
| 310 |
yield sse_event("brain_trace", build_brain_event(
|
| 311 |
"working_memory",
|
|
|
|
| 320 |
"message": f"Loaded {len(history)} recent messages into working memory.",
|
| 321 |
"data": {"history_count": len(history)}
|
| 322 |
})
|
| 323 |
+
await asyncio.sleep(0.4)
|
| 324 |
|
| 325 |
for output in orchestrator.stream(state_input):
|
| 326 |
for node_name, node_output in output.items():
|
|
|
|
| 336 |
data={"reflection": reflection}
|
| 337 |
))
|
| 338 |
yield sse_event("trace", {"phase": "reflection", "message": "Intent map formed.", "data": {"reflection": reflection}})
|
| 339 |
+
await asyncio.sleep(0.4)
|
| 340 |
|
| 341 |
elif node_name == "retrieve":
|
| 342 |
trace_data = node_output.get("trace_data", {})
|
|
|
|
| 354 |
}
|
| 355 |
))
|
| 356 |
yield sse_event("trace", {"phase": "recall", "message": recall_msg, "data": node_output.get("context")})
|
| 357 |
+
await asyncio.sleep(0.4)
|
| 358 |
|
| 359 |
suppressed_sensory = trace_data.get("suppressed_sensory", 0)
|
| 360 |
suppressed_graph = trace_data.get("suppressed_graph", 0)
|
|
|
|
| 377 |
"suppressed_graph": suppressed_graph,
|
| 378 |
}
|
| 379 |
})
|
| 380 |
+
await asyncio.sleep(0.4)
|
| 381 |
|
| 382 |
yield sse_event("brain_trace", build_brain_event(
|
| 383 |
"association",
|
|
|
|
| 391 |
}
|
| 392 |
))
|
| 393 |
yield sse_event("trace", {"phase": "association", "message": assoc_msg, "data": node_output.get("graph_context"), "touched": trace_data.get("touched")})
|
| 394 |
+
await asyncio.sleep(0.4)
|
| 395 |
|
| 396 |
elif node_name == "call_model":
|
| 397 |
reason_msg = "Synthesizing final response via Cortex Node..."
|
|
|
|
| 404 |
data={"prediction": prediction["intent"]}
|
| 405 |
))
|
| 406 |
yield sse_event("trace", {"phase": "reasoning", "message": reason_msg})
|
| 407 |
+
await asyncio.sleep(0.4)
|
| 408 |
|
| 409 |
final_response = node_output.get("response", "")
|
| 410 |
add_message(current_user, "user", request.text)
|
|
|
|
| 418 |
inputs_used=["response_plan"],
|
| 419 |
data={"response_preview": final_response[:120]}
|
| 420 |
))
|
| 421 |
+
yield sse_event("trace", {"phase": "language", "message": "Generating natural language output."})
|
| 422 |
yield sse_event("final_result", {"response": final_response})
|
| 423 |
|
| 424 |
# Build neural mesh AFTER streaming the response so
|
|
|
|
| 484 |
|
| 485 |
# ── Memory Explorer ─────────────────────────────────────────────
|
| 486 |
|
| 487 |
+
@router.get("/memory/search")
|
| 488 |
+
async def process_memory_search(q: str, current_user: str = Depends(get_current_user)):
|
| 489 |
+
try:
|
| 490 |
+
from app.db.chroma import search_memories
|
| 491 |
+
results = search_memories(q, current_user)
|
| 492 |
+
|
| 493 |
+
memories = []
|
| 494 |
+
if results and "documents" in results and results["documents"]:
|
| 495 |
+
docs = results["documents"][0]
|
| 496 |
+
ids = results["ids"][0]
|
| 497 |
+
metadatas = results["metadatas"][0] if results["metadatas"] else []
|
| 498 |
+
distances = results["distances"][0] if results["distances"] else []
|
| 499 |
+
|
| 500 |
+
for i in range(len(docs)):
|
| 501 |
+
memories.append({
|
| 502 |
+
"id": ids[i],
|
| 503 |
+
"content": docs[i],
|
| 504 |
+
"metadata": metadatas[i] if i < len(metadatas) else {},
|
| 505 |
+
"similarity": round(1 - distances[i], 2) if i < len(distances) else 0
|
| 506 |
+
})
|
| 507 |
+
return {"memories": memories}
|
| 508 |
+
except Exception as e:
|
| 509 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 510 |
+
|
| 511 |
+
|
| 512 |
@router.get("/memory/sensory")
|
| 513 |
async def get_sensory_memories(current_user: str = Depends(get_current_user)):
|
| 514 |
try:
|
|
|
|
| 529 |
raise HTTPException(status_code=500, detail=str(e))
|
| 530 |
|
| 531 |
|
| 532 |
+
@router.delete("/memory/{memory_id}")
|
| 533 |
+
async def purge_memory_chunk(memory_id: str, current_user: str = Depends(get_current_user)):
|
| 534 |
+
try:
|
| 535 |
+
from app.db.chroma import delete_vector
|
| 536 |
+
success = delete_vector(memory_id, current_user)
|
| 537 |
+
if not success:
|
| 538 |
+
raise HTTPException(status_code=404, detail="Memory chunk not found or unauthorized.")
|
| 539 |
+
return {"message": "Memory chunk purged successfully."}
|
| 540 |
+
except Exception as e:
|
| 541 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 542 |
+
|
| 543 |
+
|
| 544 |
# ── History ───────────────────────────────────────────────────────
|
| 545 |
|
| 546 |
@router.get("/history")
|
app/db/chroma.py
CHANGED
|
@@ -10,6 +10,21 @@ def get_collection(name: str = "soma_sensory_memory"):
|
|
| 10 |
return client.get_or_create_collection(name=name)
|
| 11 |
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
def clear_user_vectors(user_id: str):
|
| 14 |
"""Delete all ChromaDB documents belonging to a user."""
|
| 15 |
try:
|
|
@@ -19,3 +34,13 @@ def clear_user_vectors(user_id: str):
|
|
| 19 |
collection.delete(ids=results["ids"])
|
| 20 |
except Exception as e:
|
| 21 |
print(f"ChromaDB clear error: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
return client.get_or_create_collection(name=name)
|
| 11 |
|
| 12 |
|
| 13 |
+
def search_memories(query: str, user_id: str, limit: int = 10):
|
| 14 |
+
"""Semantic search for memories belonging to a user."""
|
| 15 |
+
try:
|
| 16 |
+
collection = get_collection()
|
| 17 |
+
results = collection.query(
|
| 18 |
+
query_texts=[query],
|
| 19 |
+
n_results=limit,
|
| 20 |
+
where={"user_id": user_id}
|
| 21 |
+
)
|
| 22 |
+
return results
|
| 23 |
+
except Exception as e:
|
| 24 |
+
print(f"ChromaDB search error: {e}")
|
| 25 |
+
return None
|
| 26 |
+
|
| 27 |
+
|
| 28 |
def clear_user_vectors(user_id: str):
|
| 29 |
"""Delete all ChromaDB documents belonging to a user."""
|
| 30 |
try:
|
|
|
|
| 34 |
collection.delete(ids=results["ids"])
|
| 35 |
except Exception as e:
|
| 36 |
print(f"ChromaDB clear error: {e}")
|
| 37 |
+
|
| 38 |
+
def delete_vector(memory_id: str, user_id: str):
|
| 39 |
+
"""Delete a specific document from ChromaDB."""
|
| 40 |
+
try:
|
| 41 |
+
collection = get_collection()
|
| 42 |
+
collection.delete(ids=[memory_id], where={"user_id": user_id})
|
| 43 |
+
return True
|
| 44 |
+
except Exception as e:
|
| 45 |
+
print(f"ChromaDB delete error: {e}")
|
| 46 |
+
return False
|
app/db/session.py
CHANGED
|
@@ -74,7 +74,7 @@ def init_session_db():
|
|
| 74 |
|
| 75 |
# Test Postgres connection if configured
|
| 76 |
if _db_backend == "postgres" and not _test_postgres_connection():
|
| 77 |
-
print("
|
| 78 |
_db_backend = "sqlite"
|
| 79 |
|
| 80 |
with get_conn() as conn:
|
|
|
|
| 74 |
|
| 75 |
# Test Postgres connection if configured
|
| 76 |
if _db_backend == "postgres" and not _test_postgres_connection():
|
| 77 |
+
print("[!] Postgres connection failed. Falling back to SQLite.")
|
| 78 |
_db_backend = "sqlite"
|
| 79 |
|
| 80 |
with get_conn() as conn:
|
app/main.py
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
print("Main: Imports starting...")
|
| 2 |
from fastapi import FastAPI
|
| 3 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import io
|
| 3 |
+
|
| 4 |
+
# Force UTF-8 encoding for stdout and stderr on Windows to handle emojis
|
| 5 |
+
if sys.platform == "win32":
|
| 6 |
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
| 7 |
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
| 8 |
+
|
| 9 |
print("Main: Imports starting...")
|
| 10 |
from fastapi import FastAPI
|
| 11 |
from fastapi.middleware.cors import CORSMiddleware
|
app/services/brain_trace.py
CHANGED
|
@@ -33,7 +33,7 @@ PHASE_REGIONS = {
|
|
| 33 |
"language": "language_cortex",
|
| 34 |
"memory": "memory_consolidation",
|
| 35 |
"graph": "neocortex",
|
| 36 |
-
"dreaming": "default_mode_network",
|
| 37 |
"sleep": "memory_consolidation",
|
| 38 |
}
|
| 39 |
|
|
|
|
| 33 |
"language": "language_cortex",
|
| 34 |
"memory": "memory_consolidation",
|
| 35 |
"graph": "neocortex",
|
| 36 |
+
"dreaming": "default_mode_network",
|
| 37 |
"sleep": "memory_consolidation",
|
| 38 |
}
|
| 39 |
|
app/services/memory.py
CHANGED
|
@@ -28,18 +28,18 @@ def get_embeddings():
|
|
| 28 |
model_name="all-MiniLM-L6-v2",
|
| 29 |
model_kwargs={"trust_remote_code": True}
|
| 30 |
)
|
| 31 |
-
print("
|
| 32 |
return _embeddings
|
| 33 |
except Exception as e:
|
| 34 |
-
print(f"
|
| 35 |
-
print("
|
| 36 |
_embeddings_failed = True
|
| 37 |
return None
|
| 38 |
|
| 39 |
def ingest_text(text: str, metadata: dict = None, user_id: str = "default_user"):
|
| 40 |
embeddings = get_embeddings()
|
| 41 |
if embeddings is None:
|
| 42 |
-
print(f"
|
| 43 |
return 0
|
| 44 |
|
| 45 |
# Step 1: Chunk the text (Soma's parsing)
|
|
@@ -75,7 +75,7 @@ def ingest_text(text: str, metadata: dict = None, user_id: str = "default_user")
|
|
| 75 |
def retrieve_context(query: str, user_id: str = "default_user", n_results: int = 3):
|
| 76 |
embeddings = get_embeddings()
|
| 77 |
if embeddings is None:
|
| 78 |
-
print(f"
|
| 79 |
return []
|
| 80 |
|
| 81 |
collection = get_collection()
|
|
|
|
| 28 |
model_name="all-MiniLM-L6-v2",
|
| 29 |
model_kwargs={"trust_remote_code": True}
|
| 30 |
)
|
| 31 |
+
print("[OK] Embeddings model loaded successfully")
|
| 32 |
return _embeddings
|
| 33 |
except Exception as e:
|
| 34 |
+
print(f"[!] Failed to load embeddings from HF Hub: {e}")
|
| 35 |
+
print("[!] Continuing without embeddings (sensory memory will be limited)")
|
| 36 |
_embeddings_failed = True
|
| 37 |
return None
|
| 38 |
|
| 39 |
def ingest_text(text: str, metadata: dict = None, user_id: str = "default_user"):
|
| 40 |
embeddings = get_embeddings()
|
| 41 |
if embeddings is None:
|
| 42 |
+
print(f"[!] Skipping sensory memory ingestion (embeddings unavailable)")
|
| 43 |
return 0
|
| 44 |
|
| 45 |
# Step 1: Chunk the text (Soma's parsing)
|
|
|
|
| 75 |
def retrieve_context(query: str, user_id: str = "default_user", n_results: int = 3):
|
| 76 |
embeddings = get_embeddings()
|
| 77 |
if embeddings is None:
|
| 78 |
+
print(f"[!] Cannot retrieve context (embeddings unavailable)")
|
| 79 |
return []
|
| 80 |
|
| 81 |
collection = get_collection()
|
app/services/orchestrator.py
CHANGED
|
@@ -79,40 +79,38 @@ def call_model(state: AgentState):
|
|
| 79 |
history_lines.append(f"{prefix}: {msg['content']}")
|
| 80 |
history_str = "\n".join(history_lines) if history_lines else "No previous conversation."
|
| 81 |
|
| 82 |
-
#
|
| 83 |
context_str = "\n\n".join(state["context"])
|
| 84 |
graph_str = "\n".join(state["graph_context"]) if state["graph_context"] else "No related knowledge graph entities found."
|
| 85 |
-
|
| 86 |
-
has_memories = bool(context_str.strip()) or bool(state["graph_context"])
|
| 87 |
|
| 88 |
-
|
| 89 |
-
You have specialized memory layers that build up as you interact with the user.
|
| 90 |
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
-
###
|
|
|
|
| 96 |
{graph_str}
|
| 97 |
|
| 98 |
-
#### SENSORY MEMORY
|
| 99 |
{context_str}
|
|
|
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
4. As the user talks to you, your memory layers will grow. Acknowledge what you learn from them.
|
| 106 |
-
5. Keep your persona warm, intelligent, and concise. You are a brain that is always learning.
|
| 107 |
-
|
| 108 |
-
USER MESSAGE:
|
| 109 |
-
{state["input"]}"""
|
| 110 |
|
| 111 |
try:
|
| 112 |
-
response = llm.invoke(
|
| 113 |
-
response_text = response.content
|
| 114 |
except Exception as e:
|
| 115 |
-
response_text = f"
|
| 116 |
|
| 117 |
return {"response": response_text}
|
| 118 |
|
|
|
|
| 79 |
history_lines.append(f"{prefix}: {msg['content']}")
|
| 80 |
history_str = "\n".join(history_lines) if history_lines else "No previous conversation."
|
| 81 |
|
| 82 |
+
# Context formatting
|
| 83 |
context_str = "\n\n".join(state["context"])
|
| 84 |
graph_str = "\n".join(state["graph_context"]) if state["graph_context"] else "No related knowledge graph entities found."
|
|
|
|
|
|
|
| 85 |
|
| 86 |
+
from langchain_core.messages import SystemMessage, HumanMessage
|
|
|
|
| 87 |
|
| 88 |
+
system_prompt = f"""You are Soma, a brain-inspired cognitive AI.
|
| 89 |
+
|
| 90 |
+
### OPERATING RULES:
|
| 91 |
+
1. Be a friendly, intelligent, and natural conversationalist.
|
| 92 |
+
2. NO META-COMMENTARY. Never talk about your own "sensing patterns", "internal processes", or "memory layers" unless explicitly asked.
|
| 93 |
+
3. Keep responses concise but human-like. Don't be a robot, but don't write essays either.
|
| 94 |
+
4. Acknowledge user input naturally (e.g., if they introduce themselves, greet them by name).
|
| 95 |
|
| 96 |
+
### COGNITIVE CONTEXT:
|
| 97 |
+
#### SEMANTIC MEMORY:
|
| 98 |
{graph_str}
|
| 99 |
|
| 100 |
+
#### SENSORY MEMORY:
|
| 101 |
{context_str}
|
| 102 |
+
"""
|
| 103 |
|
| 104 |
+
messages = [
|
| 105 |
+
SystemMessage(content=system_prompt),
|
| 106 |
+
HumanMessage(content=f"HISTORY:\n{history_str}\n\nUSER MESSAGE: {state['input']}")
|
| 107 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
try:
|
| 110 |
+
response = llm.invoke(messages)
|
| 111 |
+
response_text = response.content.strip()
|
| 112 |
except Exception as e:
|
| 113 |
+
response_text = f"Cognitive Link Error: {str(e)}"
|
| 114 |
|
| 115 |
return {"response": response_text}
|
| 116 |
|
app/services/sleep_cycle.py
CHANGED
|
@@ -123,7 +123,7 @@ def run_sleep_cycle(keep_recent: int = 10):
|
|
| 123 |
"graph_triples": triples,
|
| 124 |
})
|
| 125 |
|
| 126 |
-
print(f" Sleep Cycle: Session {session_id}
|
| 127 |
|
| 128 |
report["message"] = f"Sleep Cycle complete. Processed {report['sessions_processed']} sessions."
|
| 129 |
return report
|
|
@@ -132,7 +132,7 @@ def run_sleep_cycle(keep_recent: int = 10):
|
|
| 132 |
# Allow running as a standalone script
|
| 133 |
if __name__ == "__main__":
|
| 134 |
print("=" * 60)
|
| 135 |
-
print("Soma Sleep Cycle
|
| 136 |
print("=" * 60)
|
| 137 |
result = run_sleep_cycle()
|
| 138 |
print(f"\n{'=' * 60}")
|
|
|
|
| 123 |
"graph_triples": triples,
|
| 124 |
})
|
| 125 |
|
| 126 |
+
print(f" Sleep Cycle: Session {session_id} - summarized, {triples} triples, pruned {pruned} messages.")
|
| 127 |
|
| 128 |
report["message"] = f"Sleep Cycle complete. Processed {report['sessions_processed']} sessions."
|
| 129 |
return report
|
|
|
|
| 132 |
# Allow running as a standalone script
|
| 133 |
if __name__ == "__main__":
|
| 134 |
print("=" * 60)
|
| 135 |
+
print("Soma Sleep Cycle - Starting consolidation...")
|
| 136 |
print("=" * 60)
|
| 137 |
result = run_sleep_cycle()
|
| 138 |
print(f"\n{'=' * 60}")
|
frontend/package-lock.json
CHANGED
|
@@ -10,6 +10,7 @@
|
|
| 10 |
"dependencies": {
|
| 11 |
"react": "^18.3.1",
|
| 12 |
"react-dom": "^18.3.1",
|
|
|
|
| 13 |
"react-force-graph-3d": "^1.29.1"
|
| 14 |
},
|
| 15 |
"devDependencies": {
|
|
@@ -1598,6 +1599,16 @@
|
|
| 1598 |
"node": ">=6.0.0"
|
| 1599 |
}
|
| 1600 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1601 |
"node_modules/brace-expansion": {
|
| 1602 |
"version": "1.1.12",
|
| 1603 |
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
|
@@ -1675,6 +1686,18 @@
|
|
| 1675 |
],
|
| 1676 |
"license": "CC-BY-4.0"
|
| 1677 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1678 |
"node_modules/chalk": {
|
| 1679 |
"version": "4.1.2",
|
| 1680 |
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
|
@@ -1784,6 +1807,28 @@
|
|
| 1784 |
"node": ">=12"
|
| 1785 |
}
|
| 1786 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1787 |
"node_modules/d3-force-3d": {
|
| 1788 |
"version": "3.0.6",
|
| 1789 |
"resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
|
|
@@ -1870,6 +1915,7 @@
|
|
| 1870 |
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
| 1871 |
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
| 1872 |
"license": "ISC",
|
|
|
|
| 1873 |
"engines": {
|
| 1874 |
"node": ">=12"
|
| 1875 |
}
|
|
@@ -1907,6 +1953,41 @@
|
|
| 1907 |
"node": ">=12"
|
| 1908 |
}
|
| 1909 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1910 |
"node_modules/data-bind-mapper": {
|
| 1911 |
"version": "1.0.3",
|
| 1912 |
"resolved": "https://registry.npmjs.org/data-bind-mapper/-/data-bind-mapper-1.0.3.tgz",
|
|
@@ -2298,6 +2379,32 @@
|
|
| 2298 |
"node": ">=12"
|
| 2299 |
}
|
| 2300 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2301 |
"node_modules/fsevents": {
|
| 2302 |
"version": "2.3.3",
|
| 2303 |
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
|
@@ -2396,6 +2503,15 @@
|
|
| 2396 |
"node": ">=0.8.19"
|
| 2397 |
}
|
| 2398 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2399 |
"node_modules/internmap": {
|
| 2400 |
"version": "2.0.3",
|
| 2401 |
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
|
@@ -2909,6 +3025,23 @@
|
|
| 2909 |
"react": "^18.3.1"
|
| 2910 |
}
|
| 2911 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2912 |
"node_modules/react-force-graph-3d": {
|
| 2913 |
"version": "1.29.1",
|
| 2914 |
"resolved": "https://registry.npmjs.org/react-force-graph-3d/-/react-force-graph-3d-1.29.1.tgz",
|
|
|
|
| 10 |
"dependencies": {
|
| 11 |
"react": "^18.3.1",
|
| 12 |
"react-dom": "^18.3.1",
|
| 13 |
+
"react-force-graph-2d": "^1.29.1",
|
| 14 |
"react-force-graph-3d": "^1.29.1"
|
| 15 |
},
|
| 16 |
"devDependencies": {
|
|
|
|
| 1599 |
"node": ">=6.0.0"
|
| 1600 |
}
|
| 1601 |
},
|
| 1602 |
+
"node_modules/bezier-js": {
|
| 1603 |
+
"version": "6.1.4",
|
| 1604 |
+
"resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz",
|
| 1605 |
+
"integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==",
|
| 1606 |
+
"license": "MIT",
|
| 1607 |
+
"funding": {
|
| 1608 |
+
"type": "individual",
|
| 1609 |
+
"url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md"
|
| 1610 |
+
}
|
| 1611 |
+
},
|
| 1612 |
"node_modules/brace-expansion": {
|
| 1613 |
"version": "1.1.12",
|
| 1614 |
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
|
|
|
| 1686 |
],
|
| 1687 |
"license": "CC-BY-4.0"
|
| 1688 |
},
|
| 1689 |
+
"node_modules/canvas-color-tracker": {
|
| 1690 |
+
"version": "1.3.2",
|
| 1691 |
+
"resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz",
|
| 1692 |
+
"integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==",
|
| 1693 |
+
"license": "MIT",
|
| 1694 |
+
"dependencies": {
|
| 1695 |
+
"tinycolor2": "^1.6.0"
|
| 1696 |
+
},
|
| 1697 |
+
"engines": {
|
| 1698 |
+
"node": ">=12"
|
| 1699 |
+
}
|
| 1700 |
+
},
|
| 1701 |
"node_modules/chalk": {
|
| 1702 |
"version": "4.1.2",
|
| 1703 |
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
|
|
|
| 1807 |
"node": ">=12"
|
| 1808 |
}
|
| 1809 |
},
|
| 1810 |
+
"node_modules/d3-drag": {
|
| 1811 |
+
"version": "3.0.0",
|
| 1812 |
+
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
| 1813 |
+
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
| 1814 |
+
"license": "ISC",
|
| 1815 |
+
"dependencies": {
|
| 1816 |
+
"d3-dispatch": "1 - 3",
|
| 1817 |
+
"d3-selection": "3"
|
| 1818 |
+
},
|
| 1819 |
+
"engines": {
|
| 1820 |
+
"node": ">=12"
|
| 1821 |
+
}
|
| 1822 |
+
},
|
| 1823 |
+
"node_modules/d3-ease": {
|
| 1824 |
+
"version": "3.0.1",
|
| 1825 |
+
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
| 1826 |
+
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
| 1827 |
+
"license": "BSD-3-Clause",
|
| 1828 |
+
"engines": {
|
| 1829 |
+
"node": ">=12"
|
| 1830 |
+
}
|
| 1831 |
+
},
|
| 1832 |
"node_modules/d3-force-3d": {
|
| 1833 |
"version": "3.0.6",
|
| 1834 |
"resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
|
|
|
|
| 1915 |
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
| 1916 |
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
| 1917 |
"license": "ISC",
|
| 1918 |
+
"peer": true,
|
| 1919 |
"engines": {
|
| 1920 |
"node": ">=12"
|
| 1921 |
}
|
|
|
|
| 1953 |
"node": ">=12"
|
| 1954 |
}
|
| 1955 |
},
|
| 1956 |
+
"node_modules/d3-transition": {
|
| 1957 |
+
"version": "3.0.1",
|
| 1958 |
+
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
| 1959 |
+
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
| 1960 |
+
"license": "ISC",
|
| 1961 |
+
"dependencies": {
|
| 1962 |
+
"d3-color": "1 - 3",
|
| 1963 |
+
"d3-dispatch": "1 - 3",
|
| 1964 |
+
"d3-ease": "1 - 3",
|
| 1965 |
+
"d3-interpolate": "1 - 3",
|
| 1966 |
+
"d3-timer": "1 - 3"
|
| 1967 |
+
},
|
| 1968 |
+
"engines": {
|
| 1969 |
+
"node": ">=12"
|
| 1970 |
+
},
|
| 1971 |
+
"peerDependencies": {
|
| 1972 |
+
"d3-selection": "2 - 3"
|
| 1973 |
+
}
|
| 1974 |
+
},
|
| 1975 |
+
"node_modules/d3-zoom": {
|
| 1976 |
+
"version": "3.0.0",
|
| 1977 |
+
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
| 1978 |
+
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
| 1979 |
+
"license": "ISC",
|
| 1980 |
+
"dependencies": {
|
| 1981 |
+
"d3-dispatch": "1 - 3",
|
| 1982 |
+
"d3-drag": "2 - 3",
|
| 1983 |
+
"d3-interpolate": "1 - 3",
|
| 1984 |
+
"d3-selection": "2 - 3",
|
| 1985 |
+
"d3-transition": "2 - 3"
|
| 1986 |
+
},
|
| 1987 |
+
"engines": {
|
| 1988 |
+
"node": ">=12"
|
| 1989 |
+
}
|
| 1990 |
+
},
|
| 1991 |
"node_modules/data-bind-mapper": {
|
| 1992 |
"version": "1.0.3",
|
| 1993 |
"resolved": "https://registry.npmjs.org/data-bind-mapper/-/data-bind-mapper-1.0.3.tgz",
|
|
|
|
| 2379 |
"node": ">=12"
|
| 2380 |
}
|
| 2381 |
},
|
| 2382 |
+
"node_modules/force-graph": {
|
| 2383 |
+
"version": "1.51.4",
|
| 2384 |
+
"resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.4.tgz",
|
| 2385 |
+
"integrity": "sha512-TdJ2KbkoiDQ7NIRx8IPGD0mAXXpLhamS7c+b7W98b0MHG7lphnda1VOQX/98UDTsttIAdH4TcP0l0MauSnLK8w==",
|
| 2386 |
+
"license": "MIT",
|
| 2387 |
+
"dependencies": {
|
| 2388 |
+
"@tweenjs/tween.js": "18 - 25",
|
| 2389 |
+
"accessor-fn": "1",
|
| 2390 |
+
"bezier-js": "3 - 6",
|
| 2391 |
+
"canvas-color-tracker": "^1.3",
|
| 2392 |
+
"d3-array": "1 - 3",
|
| 2393 |
+
"d3-drag": "2 - 3",
|
| 2394 |
+
"d3-force-3d": "2 - 3",
|
| 2395 |
+
"d3-scale": "1 - 4",
|
| 2396 |
+
"d3-scale-chromatic": "1 - 3",
|
| 2397 |
+
"d3-selection": "2 - 3",
|
| 2398 |
+
"d3-zoom": "2 - 3",
|
| 2399 |
+
"float-tooltip": "^1.7",
|
| 2400 |
+
"index-array-by": "1",
|
| 2401 |
+
"kapsule": "^1.16",
|
| 2402 |
+
"lodash-es": "4"
|
| 2403 |
+
},
|
| 2404 |
+
"engines": {
|
| 2405 |
+
"node": ">=12"
|
| 2406 |
+
}
|
| 2407 |
+
},
|
| 2408 |
"node_modules/fsevents": {
|
| 2409 |
"version": "2.3.3",
|
| 2410 |
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
|
|
|
| 2503 |
"node": ">=0.8.19"
|
| 2504 |
}
|
| 2505 |
},
|
| 2506 |
+
"node_modules/index-array-by": {
|
| 2507 |
+
"version": "1.4.2",
|
| 2508 |
+
"resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz",
|
| 2509 |
+
"integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==",
|
| 2510 |
+
"license": "MIT",
|
| 2511 |
+
"engines": {
|
| 2512 |
+
"node": ">=12"
|
| 2513 |
+
}
|
| 2514 |
+
},
|
| 2515 |
"node_modules/internmap": {
|
| 2516 |
"version": "2.0.3",
|
| 2517 |
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
|
|
|
| 3025 |
"react": "^18.3.1"
|
| 3026 |
}
|
| 3027 |
},
|
| 3028 |
+
"node_modules/react-force-graph-2d": {
|
| 3029 |
+
"version": "1.29.1",
|
| 3030 |
+
"resolved": "https://registry.npmjs.org/react-force-graph-2d/-/react-force-graph-2d-1.29.1.tgz",
|
| 3031 |
+
"integrity": "sha512-1Rl/1Z3xy2iTHKj6a0jRXGyiI86xUti81K+jBQZ+Oe46csaMikp47L5AjrzA9hY9fNGD63X8ffrqnvaORukCuQ==",
|
| 3032 |
+
"license": "MIT",
|
| 3033 |
+
"dependencies": {
|
| 3034 |
+
"force-graph": "^1.51",
|
| 3035 |
+
"prop-types": "15",
|
| 3036 |
+
"react-kapsule": "^2.5"
|
| 3037 |
+
},
|
| 3038 |
+
"engines": {
|
| 3039 |
+
"node": ">=12"
|
| 3040 |
+
},
|
| 3041 |
+
"peerDependencies": {
|
| 3042 |
+
"react": "*"
|
| 3043 |
+
}
|
| 3044 |
+
},
|
| 3045 |
"node_modules/react-force-graph-3d": {
|
| 3046 |
"version": "1.29.1",
|
| 3047 |
"resolved": "https://registry.npmjs.org/react-force-graph-3d/-/react-force-graph-3d-1.29.1.tgz",
|
frontend/package.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
| 12 |
"dependencies": {
|
| 13 |
"react": "^18.3.1",
|
| 14 |
"react-dom": "^18.3.1",
|
|
|
|
| 15 |
"react-force-graph-3d": "^1.29.1"
|
| 16 |
},
|
| 17 |
"devDependencies": {
|
|
|
|
| 12 |
"dependencies": {
|
| 13 |
"react": "^18.3.1",
|
| 14 |
"react-dom": "^18.3.1",
|
| 15 |
+
"react-force-graph-2d": "^1.29.1",
|
| 16 |
"react-force-graph-3d": "^1.29.1"
|
| 17 |
},
|
| 18 |
"devDependencies": {
|
frontend/src/App.css
CHANGED
|
@@ -1,20 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
/* App.css - Final Refinements for 8-Page Design */
|
| 2 |
|
| 3 |
.soma-shell {
|
| 4 |
display: flex;
|
| 5 |
height: 100vh;
|
| 6 |
width: 100vw;
|
| 7 |
-
background-color:
|
| 8 |
overflow: hidden;
|
| 9 |
}
|
| 10 |
|
| 11 |
/* ── Sidebar ── */
|
| 12 |
.soma-sidebar {
|
| 13 |
-
width:
|
| 14 |
background-color: transparent;
|
| 15 |
display: flex;
|
| 16 |
flex-direction: column;
|
| 17 |
-
padding: 48px
|
| 18 |
flex-shrink: 0;
|
| 19 |
}
|
| 20 |
|
|
@@ -42,7 +67,7 @@
|
|
| 42 |
|
| 43 |
.brand-copy h1 {
|
| 44 |
font-family: var(--font-display);
|
| 45 |
-
font-size: 1.
|
| 46 |
font-weight: 800;
|
| 47 |
line-height: 1;
|
| 48 |
color: #1a1a1a;
|
|
@@ -50,7 +75,7 @@
|
|
| 50 |
}
|
| 51 |
|
| 52 |
.brand-copy p {
|
| 53 |
-
font-size: 0.
|
| 54 |
color: #888;
|
| 55 |
margin-top: 2px;
|
| 56 |
text-transform: uppercase;
|
|
@@ -74,7 +99,7 @@
|
|
| 74 |
color: #666;
|
| 75 |
text-decoration: none;
|
| 76 |
font-weight: 600;
|
| 77 |
-
font-size: 0.
|
| 78 |
transition: all 0.2s ease;
|
| 79 |
background: transparent;
|
| 80 |
}
|
|
@@ -106,8 +131,8 @@
|
|
| 106 |
.session-card {
|
| 107 |
display: flex;
|
| 108 |
align-items: center;
|
| 109 |
-
gap:
|
| 110 |
-
padding:
|
| 111 |
background-color: rgba(255, 255, 255, 0.6);
|
| 112 |
border-radius: 20px;
|
| 113 |
border: 1px solid rgba(0,0,0,0.05);
|
|
@@ -127,14 +152,15 @@
|
|
| 127 |
}
|
| 128 |
|
| 129 |
.session-copy strong {
|
| 130 |
-
font-size: 0.
|
| 131 |
font-weight: 700;
|
| 132 |
color: #1a1a1a;
|
| 133 |
}
|
| 134 |
|
| 135 |
.session-copy span {
|
| 136 |
-
font-size: 0.
|
| 137 |
color: #999;
|
|
|
|
| 138 |
}
|
| 139 |
|
| 140 |
/* ── Main Content Area ── */
|
|
@@ -146,10 +172,12 @@
|
|
| 146 |
|
| 147 |
.page-canvas {
|
| 148 |
flex: 1;
|
| 149 |
-
background-color:
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
| 153 |
display: flex;
|
| 154 |
flex-direction: column;
|
| 155 |
position: relative;
|
|
@@ -160,12 +188,12 @@
|
|
| 160 |
display: flex;
|
| 161 |
justify-content: space-between;
|
| 162 |
align-items: flex-start;
|
| 163 |
-
margin-bottom:
|
| 164 |
}
|
| 165 |
|
| 166 |
.page-header h2 {
|
| 167 |
font-family: var(--font-display);
|
| 168 |
-
font-size: 2rem;
|
| 169 |
font-weight: 700;
|
| 170 |
color: #1a1a1a;
|
| 171 |
letter-spacing: -0.01em;
|
|
@@ -183,7 +211,7 @@
|
|
| 183 |
}
|
| 184 |
|
| 185 |
.status-pill .label {
|
| 186 |
-
font-size: 0.
|
| 187 |
text-transform: uppercase;
|
| 188 |
letter-spacing: 0.08em;
|
| 189 |
color: #999;
|
|
@@ -194,7 +222,7 @@
|
|
| 194 |
display: flex;
|
| 195 |
align-items: center;
|
| 196 |
gap: 8px;
|
| 197 |
-
font-size: 0.
|
| 198 |
font-weight: 700;
|
| 199 |
color: #1a1a1a;
|
| 200 |
}
|
|
@@ -230,7 +258,7 @@
|
|
| 230 |
}
|
| 231 |
|
| 232 |
.feed-header {
|
| 233 |
-
font-size: 0.
|
| 234 |
text-transform: uppercase;
|
| 235 |
letter-spacing: 0.1em;
|
| 236 |
color: #999;
|
|
@@ -263,7 +291,182 @@
|
|
| 263 |
line-height: 1.4;
|
| 264 |
}
|
| 265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
@keyframes feedItemIn {
|
| 267 |
from { opacity: 0; transform: translateX(20px); }
|
| 268 |
to { opacity: 1; transform: translateX(0); }
|
| 269 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ── Global Scrollbar Styles ── */
|
| 2 |
+
* {
|
| 3 |
+
scrollbar-width: thin;
|
| 4 |
+
scrollbar-color: rgba(0, 0, 0, 0.1) transparent;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
::-webkit-scrollbar {
|
| 8 |
+
width: 5px;
|
| 9 |
+
height: 5px;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
::-webkit-scrollbar-track {
|
| 13 |
+
background: transparent;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
::-webkit-scrollbar-thumb {
|
| 17 |
+
background: rgba(0, 0, 0, 0.05);
|
| 18 |
+
border-radius: 20px;
|
| 19 |
+
transition: background 0.3s ease;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
::-webkit-scrollbar-thumb:hover {
|
| 23 |
+
background: rgba(0, 0, 0, 0.15);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
/* App.css - Final Refinements for 8-Page Design */
|
| 27 |
|
| 28 |
.soma-shell {
|
| 29 |
display: flex;
|
| 30 |
height: 100vh;
|
| 31 |
width: 100vw;
|
| 32 |
+
background-color: transparent; /* Inherits high-fidelity body radial gradients */
|
| 33 |
overflow: hidden;
|
| 34 |
}
|
| 35 |
|
| 36 |
/* ── Sidebar ── */
|
| 37 |
.soma-sidebar {
|
| 38 |
+
width: 200px;
|
| 39 |
background-color: transparent;
|
| 40 |
display: flex;
|
| 41 |
flex-direction: column;
|
| 42 |
+
padding: 48px 20px;
|
| 43 |
flex-shrink: 0;
|
| 44 |
}
|
| 45 |
|
|
|
|
| 67 |
|
| 68 |
.brand-copy h1 {
|
| 69 |
font-family: var(--font-display);
|
| 70 |
+
font-size: 1.8rem;
|
| 71 |
font-weight: 800;
|
| 72 |
line-height: 1;
|
| 73 |
color: #1a1a1a;
|
|
|
|
| 75 |
}
|
| 76 |
|
| 77 |
.brand-copy p {
|
| 78 |
+
font-size: 0.55rem;
|
| 79 |
color: #888;
|
| 80 |
margin-top: 2px;
|
| 81 |
text-transform: uppercase;
|
|
|
|
| 99 |
color: #666;
|
| 100 |
text-decoration: none;
|
| 101 |
font-weight: 600;
|
| 102 |
+
font-size: 0.8rem;
|
| 103 |
transition: all 0.2s ease;
|
| 104 |
background: transparent;
|
| 105 |
}
|
|
|
|
| 131 |
.session-card {
|
| 132 |
display: flex;
|
| 133 |
align-items: center;
|
| 134 |
+
gap: 8px;
|
| 135 |
+
padding: 8px 10px;
|
| 136 |
background-color: rgba(255, 255, 255, 0.6);
|
| 137 |
border-radius: 20px;
|
| 138 |
border: 1px solid rgba(0,0,0,0.05);
|
|
|
|
| 152 |
}
|
| 153 |
|
| 154 |
.session-copy strong {
|
| 155 |
+
font-size: 0.75rem;
|
| 156 |
font-weight: 700;
|
| 157 |
color: #1a1a1a;
|
| 158 |
}
|
| 159 |
|
| 160 |
.session-copy span {
|
| 161 |
+
font-size: 0.55rem;
|
| 162 |
color: #999;
|
| 163 |
+
font-weight: 500;
|
| 164 |
}
|
| 165 |
|
| 166 |
/* ── Main Content Area ── */
|
|
|
|
| 172 |
|
| 173 |
.page-canvas {
|
| 174 |
flex: 1;
|
| 175 |
+
background-color: var(--bg-card);
|
| 176 |
+
backdrop-filter: blur(20px);
|
| 177 |
+
border: var(--border-card);
|
| 178 |
+
border-radius: 48px;
|
| 179 |
+
padding: 40px;
|
| 180 |
+
box-shadow: var(--shadow-premium);
|
| 181 |
display: flex;
|
| 182 |
flex-direction: column;
|
| 183 |
position: relative;
|
|
|
|
| 188 |
display: flex;
|
| 189 |
justify-content: space-between;
|
| 190 |
align-items: flex-start;
|
| 191 |
+
margin-bottom: 40px;
|
| 192 |
}
|
| 193 |
|
| 194 |
.page-header h2 {
|
| 195 |
font-family: var(--font-display);
|
| 196 |
+
font-size: 1.2rem;
|
| 197 |
font-weight: 700;
|
| 198 |
color: #1a1a1a;
|
| 199 |
letter-spacing: -0.01em;
|
|
|
|
| 211 |
}
|
| 212 |
|
| 213 |
.status-pill .label {
|
| 214 |
+
font-size: 0.6rem;
|
| 215 |
text-transform: uppercase;
|
| 216 |
letter-spacing: 0.08em;
|
| 217 |
color: #999;
|
|
|
|
| 222 |
display: flex;
|
| 223 |
align-items: center;
|
| 224 |
gap: 8px;
|
| 225 |
+
font-size: 0.75rem;
|
| 226 |
font-weight: 700;
|
| 227 |
color: #1a1a1a;
|
| 228 |
}
|
|
|
|
| 258 |
}
|
| 259 |
|
| 260 |
.feed-header {
|
| 261 |
+
font-size: 0.85rem;
|
| 262 |
text-transform: uppercase;
|
| 263 |
letter-spacing: 0.1em;
|
| 264 |
color: #999;
|
|
|
|
| 291 |
line-height: 1.4;
|
| 292 |
}
|
| 293 |
|
| 294 |
+
/* ── Telemetry HUD ── */
|
| 295 |
+
.telemetry-trigger {
|
| 296 |
+
position: absolute;
|
| 297 |
+
top: 24px;
|
| 298 |
+
right: 24px;
|
| 299 |
+
z-index: 100;
|
| 300 |
+
width: 42px;
|
| 301 |
+
height: 42px;
|
| 302 |
+
border-radius: 12px;
|
| 303 |
+
background: white;
|
| 304 |
+
border: 1px solid rgba(0,0,0,0.05);
|
| 305 |
+
display: flex;
|
| 306 |
+
align-items: center;
|
| 307 |
+
justify-content: center;
|
| 308 |
+
color: #666;
|
| 309 |
+
cursor: pointer;
|
| 310 |
+
transition: all 0.2s ease;
|
| 311 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.telemetry-trigger:hover {
|
| 315 |
+
background: #fdfdfd;
|
| 316 |
+
color: #ff6b35;
|
| 317 |
+
transform: translateY(-2px);
|
| 318 |
+
box-shadow: 0 6px 15px rgba(0,0,0,0.06);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.telemetry-trigger.active {
|
| 322 |
+
background: #ff6b35;
|
| 323 |
+
color: white;
|
| 324 |
+
border-color: #ff6b35;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
.telemetry-overlay {
|
| 328 |
+
position: fixed;
|
| 329 |
+
top: 0;
|
| 330 |
+
left: 0;
|
| 331 |
+
right: 0;
|
| 332 |
+
bottom: 0;
|
| 333 |
+
background: rgba(255, 255, 255, 0.4);
|
| 334 |
+
backdrop-filter: blur(8px);
|
| 335 |
+
z-index: 2000;
|
| 336 |
+
display: flex;
|
| 337 |
+
align-items: center;
|
| 338 |
+
justify-content: center;
|
| 339 |
+
padding: 40px;
|
| 340 |
+
animation: fadeIn 0.3s ease-out;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.telemetry-modal {
|
| 344 |
+
width: 100%;
|
| 345 |
+
max-width: 620px;
|
| 346 |
+
height: auto;
|
| 347 |
+
max-height: 80vh;
|
| 348 |
+
background: rgba(255, 255, 255, 0.85);
|
| 349 |
+
backdrop-filter: blur(25px);
|
| 350 |
+
border-radius: 32px;
|
| 351 |
+
box-shadow: 0 40px 120px rgba(0,0,0,0.08);
|
| 352 |
+
border: 1px solid white;
|
| 353 |
+
display: flex;
|
| 354 |
+
flex-direction: column;
|
| 355 |
+
overflow: hidden; /* Important: Clip everything to the rounded corners */
|
| 356 |
+
position: relative;
|
| 357 |
+
animation: modalSlide 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
| 358 |
+
color: #1a1a1a;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.modal-header {
|
| 362 |
+
display: flex;
|
| 363 |
+
justify-content: space-between;
|
| 364 |
+
align-items: center;
|
| 365 |
+
padding: 32px 32px 16px 32px;
|
| 366 |
+
background: rgba(255, 255, 255, 0.01);
|
| 367 |
+
z-index: 10;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.modal-scroll-area {
|
| 371 |
+
flex: 1;
|
| 372 |
+
overflow-y: auto;
|
| 373 |
+
padding: 0 32px 32px 32px;
|
| 374 |
+
/* Custom scrollbar for the internal area */
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.modal-scroll-area::-webkit-scrollbar {
|
| 378 |
+
width: 16px; /* Wider track but narrower thumb */
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.modal-scroll-area::-webkit-scrollbar-track {
|
| 382 |
+
background: transparent;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.modal-scroll-area::-webkit-scrollbar-thumb {
|
| 386 |
+
background-color: rgba(0, 0, 0, 0.08);
|
| 387 |
+
border: 5px solid transparent; /* Pushes the thumb 5px away from all edges */
|
| 388 |
+
background-clip: padding-box;
|
| 389 |
+
border-radius: 10px;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
.modal-scroll-area::-webkit-scrollbar-button:single-button {
|
| 393 |
+
background-color: transparent;
|
| 394 |
+
display: block;
|
| 395 |
+
height: 12px;
|
| 396 |
+
width: 16px;
|
| 397 |
+
background-repeat: no-repeat;
|
| 398 |
+
background-position: center;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.modal-scroll-area::-webkit-scrollbar-button:single-button:vertical:decrement {
|
| 402 |
+
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='%23999999'><path d='M7 14l5-5 5 5z'/></svg>");
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
.modal-scroll-area::-webkit-scrollbar-button:single-button:vertical:increment {
|
| 406 |
+
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='%23999999'><path d='M7 10l5 5 5-5z'/></svg>");
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.modal-scroll-area::-webkit-scrollbar-button:hover {
|
| 410 |
+
background-color: rgba(0,0,0,0.02);
|
| 411 |
+
border-radius: 4px;
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
.modal-header h3 {
|
| 415 |
+
font-size: 0.8rem;
|
| 416 |
+
text-transform: uppercase;
|
| 417 |
+
letter-spacing: 0.15em;
|
| 418 |
+
color: #999;
|
| 419 |
+
font-weight: 700;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.modal-header button {
|
| 423 |
+
background: rgba(0,0,0,0.05);
|
| 424 |
+
border: none;
|
| 425 |
+
width: 32px;
|
| 426 |
+
height: 32px;
|
| 427 |
+
border-radius: 50%;
|
| 428 |
+
display: flex;
|
| 429 |
+
align-items: center;
|
| 430 |
+
justify-content: center;
|
| 431 |
+
color: #666;
|
| 432 |
+
cursor: pointer;
|
| 433 |
+
transition: all 0.2s;
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
.modal-header button:hover {
|
| 437 |
+
background: #ff6b35;
|
| 438 |
+
color: white;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
@keyframes modalSlide {
|
| 442 |
+
from { opacity: 0; transform: translateY(40px) scale(0.95); }
|
| 443 |
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
@keyframes feedItemIn {
|
| 447 |
from { opacity: 0; transform: translateX(20px); }
|
| 448 |
to { opacity: 1; transform: translateX(0); }
|
| 449 |
}
|
| 450 |
+
|
| 451 |
+
.timeline-empty {
|
| 452 |
+
display: flex;
|
| 453 |
+
flex-direction: column;
|
| 454 |
+
align-items: center;
|
| 455 |
+
justify-content: center;
|
| 456 |
+
height: 100%;
|
| 457 |
+
padding: 40px 20px 240px 20px;
|
| 458 |
+
text-align: center;
|
| 459 |
+
opacity: 0.4;
|
| 460 |
+
gap: 12px;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
.timeline-empty span {
|
| 464 |
+
font-size: 48px;
|
| 465 |
+
color: #999;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.timeline-empty p {
|
| 469 |
+
font-size: 0.7rem;
|
| 470 |
+
color: #999;
|
| 471 |
+
letter-spacing: 0.02em;
|
| 472 |
+
}
|
frontend/src/App.jsx
CHANGED
|
@@ -6,7 +6,7 @@ import MemoryExplorer from './components/MemoryExplorer';
|
|
| 6 |
import KnowledgeGraph from './components/KnowledgeGraph';
|
| 7 |
import CognitiveDashboard from './components/CognitiveDashboard';
|
| 8 |
import KnowledgeInput from './components/KnowledgeInput';
|
| 9 |
-
import { SleepProgress
|
| 10 |
import AuthScreen from './components/AuthScreen';
|
| 11 |
import { apiFetch } from './api';
|
| 12 |
import './App.css';
|
|
@@ -15,8 +15,8 @@ const NAV_ITEMS = [
|
|
| 15 |
{ id: 'console', label: 'Console', icon: 'chat_bubble_outline' },
|
| 16 |
{ id: 'memory', label: 'Memory', icon: 'layers' },
|
| 17 |
{ id: 'graph', label: 'Graph', icon: 'share' },
|
| 18 |
-
{ id: 'knowledge', label: '
|
| 19 |
-
{ id: '
|
| 20 |
];
|
| 21 |
|
| 22 |
const COGNITIVE_PHASES = {
|
|
@@ -40,6 +40,18 @@ function App() {
|
|
| 40 |
const [knowledgeStatus, setKnowledgeStatus] = useState('');
|
| 41 |
const [sleepPhaseIndex, setSleepPhaseIndex] = useState(0);
|
| 42 |
const [sleepSummary, setSleepSummary] = useState(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
useEffect(() => {
|
| 45 |
if (!username) return;
|
|
@@ -75,21 +87,7 @@ function App() {
|
|
| 75 |
setMessages(prev => [...prev, { role: 'user', content: text, timestamp }]);
|
| 76 |
setTrace([]);
|
| 77 |
|
| 78 |
-
//
|
| 79 |
-
|
| 80 |
-
// Phase A: Perception (~0.4s)
|
| 81 |
-
setCognitiveState(COGNITIVE_PHASES.PERCEPTION);
|
| 82 |
-
setTrace([{ phase: 'Perception', content: 'Parsing input and identifying intent...', time: 'NOW' }]);
|
| 83 |
-
await new Promise(r => setTimeout(r, 400));
|
| 84 |
-
|
| 85 |
-
// Phase B: Attention (~0.6s)
|
| 86 |
-
setCognitiveState(COGNITIVE_PHASES.ATTENTION);
|
| 87 |
-
setTrace(prev => [{ phase: 'Attention', content: 'Focusing on key concepts and relationships...', time: 'NOW' }, ...prev]);
|
| 88 |
-
await new Promise(r => setTimeout(r, 600));
|
| 89 |
-
|
| 90 |
-
// Phase C: Memory Recall (Initiate API)
|
| 91 |
-
setCognitiveState(COGNITIVE_PHASES.RECALL);
|
| 92 |
-
setTrace(prev => [{ phase: 'Recall', content: 'Retrieving related memories and associations...', time: 'NOW' }, ...prev]);
|
| 93 |
|
| 94 |
try {
|
| 95 |
const controller = new AbortController();
|
|
@@ -113,10 +111,6 @@ function App() {
|
|
| 113 |
const decoder = new TextDecoder();
|
| 114 |
let buffer = '';
|
| 115 |
|
| 116 |
-
// Phase D: Reasoning (During Stream Start)
|
| 117 |
-
setCognitiveState(COGNITIVE_PHASES.REASONING);
|
| 118 |
-
setTrace(prev => [{ phase: 'Reasoning', content: 'Synthesizing context and evaluating response...', time: 'NOW' }, ...prev]);
|
| 119 |
-
|
| 120 |
while (true) {
|
| 121 |
const { value, done } = await reader.read();
|
| 122 |
if (done) break;
|
|
@@ -134,7 +128,9 @@ function App() {
|
|
| 134 |
if (data.phase) {
|
| 135 |
// Live trace and state updates from backend
|
| 136 |
setCognitiveState(data.phase);
|
| 137 |
-
|
|
|
|
|
|
|
| 138 |
} else if (data.response) {
|
| 139 |
// Phase E: Response Generation
|
| 140 |
setCognitiveState(COGNITIVE_PHASES.RESPONDING);
|
|
@@ -157,12 +153,12 @@ function App() {
|
|
| 157 |
setTrace(prev => [{ phase: 'Error', content: 'Neural connection interrupted.', time: 'ERROR' }, ...prev]);
|
| 158 |
} finally {
|
| 159 |
// Ensure we always return to idle
|
| 160 |
-
|
| 161 |
}
|
| 162 |
};
|
| 163 |
|
| 164 |
const handleKnowledgeSubmit = async (text) => {
|
| 165 |
-
setKnowledgeStatus('
|
| 166 |
try {
|
| 167 |
const res = await apiFetch('/api/v1/ingest', {
|
| 168 |
method: 'POST',
|
|
@@ -170,11 +166,11 @@ function App() {
|
|
| 170 |
});
|
| 171 |
const data = await res.json();
|
| 172 |
if (!res.ok) throw new Error(data.detail || 'Ingestion failed');
|
| 173 |
-
setKnowledgeStatus('Knowledge stored successfully.');
|
| 174 |
-
setTimeout(() => setKnowledgeStatus(''),
|
| 175 |
fetchVitals();
|
| 176 |
} catch (error) {
|
| 177 |
-
setKnowledgeStatus('
|
| 178 |
}
|
| 179 |
};
|
| 180 |
|
|
@@ -182,22 +178,35 @@ function App() {
|
|
| 182 |
setCognitiveState('consolidating');
|
| 183 |
setActivePage('sleep');
|
| 184 |
setSleepPhaseIndex(0);
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
try {
|
| 189 |
const res = await apiFetch('/api/v1/sleep', { method: 'POST' });
|
| 190 |
const data = await res.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
setSleepSummary({
|
| 192 |
-
linked: data.graph_relations_extracted ||
|
| 193 |
-
consolidated:
|
| 194 |
-
pruned: data.messages_pruned ||
|
| 195 |
-
strengthened: 34
|
| 196 |
});
|
| 197 |
} catch (error) {
|
| 198 |
-
|
|
|
|
|
|
|
| 199 |
} finally {
|
| 200 |
-
|
|
|
|
| 201 |
}
|
| 202 |
};
|
| 203 |
|
|
@@ -211,10 +220,10 @@ function App() {
|
|
| 211 |
if (!username) return <AuthScreen onAuth={setUsername} />;
|
| 212 |
|
| 213 |
const stats = [
|
| 214 |
-
{ label: 'Working Memory', value: vitals?.
|
| 215 |
-
{ label: 'Sensory Memory', value: vitals?.
|
| 216 |
-
{ label: 'Semantic Memory', value: vitals?.
|
| 217 |
-
{ label: 'Neural Sparks', value: vitals?.
|
| 218 |
];
|
| 219 |
|
| 220 |
return (
|
|
@@ -241,6 +250,16 @@ function App() {
|
|
| 241 |
<span>{item.label}</span>
|
| 242 |
</button>
|
| 243 |
))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
</nav>
|
| 245 |
|
| 246 |
<div className="sidebar-footer">
|
|
@@ -250,7 +269,7 @@ function App() {
|
|
| 250 |
</div>
|
| 251 |
<div className="session-copy">
|
| 252 |
<strong>{username}</strong>
|
| 253 |
-
<span>
|
| 254 |
</div>
|
| 255 |
<button style={{marginLeft: 'auto', color: '#999', background: 'transparent'}} onClick={handleLogout}>
|
| 256 |
<span className="material-icons" style={{fontSize: '18px'}}>logout</span>
|
|
@@ -262,26 +281,29 @@ function App() {
|
|
| 262 |
<main className="soma-main-panel">
|
| 263 |
{activePage === 'console' && (
|
| 264 |
<section className="page-canvas fade-in">
|
| 265 |
-
<div className="page-header">
|
| 266 |
-
<h2>
|
| 267 |
-
<
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
</div>
|
| 275 |
-
</div>
|
| 276 |
</div>
|
| 277 |
|
| 278 |
-
<div style={{display: 'flex',
|
| 279 |
-
{/* Interaction Layer (
|
| 280 |
-
<div style={{flex:
|
| 281 |
<ChatPanel
|
| 282 |
messages={messages}
|
| 283 |
onSendMessage={handleSendMessage}
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
onInputStateChange={(isTyping) => {
|
| 286 |
if (cognitiveState === COGNITIVE_PHASES.IDLE && isTyping) setCognitiveState(COGNITIVE_PHASES.LISTENING);
|
| 287 |
if (cognitiveState === COGNITIVE_PHASES.LISTENING && !isTyping) setCognitiveState(COGNITIVE_PHASES.IDLE);
|
|
@@ -290,8 +312,23 @@ function App() {
|
|
| 290 |
</div>
|
| 291 |
|
| 292 |
{/* Cognitive Layer (The Brain) */}
|
| 293 |
-
<div style={{flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
| 294 |
<CognitiveBrainImageScene state={cognitiveState} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
</div>
|
| 296 |
</div>
|
| 297 |
</section>
|
|
@@ -300,7 +337,7 @@ function App() {
|
|
| 300 |
{activePage === 'activity' && (
|
| 301 |
<section className="page-canvas fade-in">
|
| 302 |
<div className="page-header">
|
| 303 |
-
<h2>
|
| 304 |
<button className="sidebar-link" style={{background: 'white'}} onClick={() => setActivePage('console')}>
|
| 305 |
Back to Console
|
| 306 |
</button>
|
|
@@ -317,23 +354,32 @@ function App() {
|
|
| 317 |
</section>
|
| 318 |
)}
|
| 319 |
|
| 320 |
-
{
|
| 321 |
-
<
|
| 322 |
-
<div className="
|
| 323 |
-
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
)}
|
| 326 |
|
| 327 |
{activePage === 'memory' && (
|
| 328 |
<section className="page-canvas fade-in">
|
| 329 |
-
<div className="page-header"><h2>
|
| 330 |
<MemoryExplorer />
|
| 331 |
</section>
|
| 332 |
)}
|
| 333 |
|
| 334 |
{activePage === 'graph' && (
|
| 335 |
<section className="page-canvas fade-in">
|
| 336 |
-
<div className="page-header"><h2>
|
| 337 |
<KnowledgeGraph refreshTick={refreshTick} />
|
| 338 |
</section>
|
| 339 |
)}
|
|
@@ -341,27 +387,26 @@ function App() {
|
|
| 341 |
{activePage === 'knowledge' && (
|
| 342 |
<section className="page-canvas fade-in">
|
| 343 |
<div className="page-header">
|
| 344 |
-
<h2>
|
| 345 |
-
<button className="sidebar-link" style={{background: 'white'}} onClick={handleSleepCycle}>
|
| 346 |
-
Run Sleep Cycle
|
| 347 |
-
</button>
|
| 348 |
</div>
|
| 349 |
<KnowledgeInput onKnowledgeSubmit={handleKnowledgeSubmit} isBusy={knowledgeStatus.includes('Adding')} status={knowledgeStatus} />
|
| 350 |
</section>
|
| 351 |
)}
|
| 352 |
|
| 353 |
{activePage === 'sleep' && (
|
| 354 |
-
<
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
|
|
|
|
|
|
| 365 |
)}
|
| 366 |
</main>
|
| 367 |
</div>
|
|
|
|
| 6 |
import KnowledgeGraph from './components/KnowledgeGraph';
|
| 7 |
import CognitiveDashboard from './components/CognitiveDashboard';
|
| 8 |
import KnowledgeInput from './components/KnowledgeInput';
|
| 9 |
+
import { SleepProgress } from './components/DreamSequence';
|
| 10 |
import AuthScreen from './components/AuthScreen';
|
| 11 |
import { apiFetch } from './api';
|
| 12 |
import './App.css';
|
|
|
|
| 15 |
{ id: 'console', label: 'Console', icon: 'chat_bubble_outline' },
|
| 16 |
{ id: 'memory', label: 'Memory', icon: 'layers' },
|
| 17 |
{ id: 'graph', label: 'Graph', icon: 'share' },
|
| 18 |
+
{ id: 'knowledge', label: 'Inscription', icon: 'auto_awesome' },
|
| 19 |
+
{ id: 'sleep', label: 'Sleep', icon: 'bedtime' },
|
| 20 |
];
|
| 21 |
|
| 22 |
const COGNITIVE_PHASES = {
|
|
|
|
| 40 |
const [knowledgeStatus, setKnowledgeStatus] = useState('');
|
| 41 |
const [sleepPhaseIndex, setSleepPhaseIndex] = useState(0);
|
| 42 |
const [sleepSummary, setSleepSummary] = useState(null);
|
| 43 |
+
const [showStatus, setShowStatus] = useState(false);
|
| 44 |
+
const [darkMode, setDarkMode] = useState(localStorage.getItem('soma_dark') === 'true');
|
| 45 |
+
|
| 46 |
+
useEffect(() => {
|
| 47 |
+
if (darkMode) {
|
| 48 |
+
document.body.classList.add('dark-theme');
|
| 49 |
+
localStorage.setItem('soma_dark', 'true');
|
| 50 |
+
} else {
|
| 51 |
+
document.body.classList.remove('dark-theme');
|
| 52 |
+
localStorage.setItem('soma_dark', 'false');
|
| 53 |
+
}
|
| 54 |
+
}, [darkMode]);
|
| 55 |
|
| 56 |
useEffect(() => {
|
| 57 |
if (!username) return;
|
|
|
|
| 87 |
setMessages(prev => [...prev, { role: 'user', content: text, timestamp }]);
|
| 88 |
setTrace([]);
|
| 89 |
|
| 90 |
+
// Remove artificial frontend phases since backend now streams them.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
try {
|
| 93 |
const controller = new AbortController();
|
|
|
|
| 111 |
const decoder = new TextDecoder();
|
| 112 |
let buffer = '';
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
while (true) {
|
| 115 |
const { value, done } = await reader.read();
|
| 116 |
if (done) break;
|
|
|
|
| 128 |
if (data.phase) {
|
| 129 |
// Live trace and state updates from backend
|
| 130 |
setCognitiveState(data.phase);
|
| 131 |
+
if (data.message) {
|
| 132 |
+
setTrace(prev => [{ time: new Date().toLocaleTimeString([], { hour12: false }), ...data }, ...prev]);
|
| 133 |
+
}
|
| 134 |
} else if (data.response) {
|
| 135 |
// Phase E: Response Generation
|
| 136 |
setCognitiveState(COGNITIVE_PHASES.RESPONDING);
|
|
|
|
| 153 |
setTrace(prev => [{ phase: 'Error', content: 'Neural connection interrupted.', time: 'ERROR' }, ...prev]);
|
| 154 |
} finally {
|
| 155 |
// Ensure we always return to idle
|
| 156 |
+
setCognitiveState(COGNITIVE_PHASES.IDLE);
|
| 157 |
}
|
| 158 |
};
|
| 159 |
|
| 160 |
const handleKnowledgeSubmit = async (text) => {
|
| 161 |
+
setKnowledgeStatus('Integrating knowledge into neural layers...');
|
| 162 |
try {
|
| 163 |
const res = await apiFetch('/api/v1/ingest', {
|
| 164 |
method: 'POST',
|
|
|
|
| 166 |
});
|
| 167 |
const data = await res.json();
|
| 168 |
if (!res.ok) throw new Error(data.detail || 'Ingestion failed');
|
| 169 |
+
setKnowledgeStatus(data.message || 'Knowledge stored successfully.');
|
| 170 |
+
setTimeout(() => setKnowledgeStatus(''), 10000);
|
| 171 |
fetchVitals();
|
| 172 |
} catch (error) {
|
| 173 |
+
setKnowledgeStatus('Neural integration failed: ' + error.message);
|
| 174 |
}
|
| 175 |
};
|
| 176 |
|
|
|
|
| 178 |
setCognitiveState('consolidating');
|
| 179 |
setActivePage('sleep');
|
| 180 |
setSleepPhaseIndex(0);
|
| 181 |
+
setSleepSummary(null);
|
| 182 |
+
|
| 183 |
+
// Animate checklist steps gracefully during the pending API request
|
| 184 |
+
const stepInterval = setInterval(() => {
|
| 185 |
+
setSleepPhaseIndex(prev => {
|
| 186 |
+
if (prev < 2) return prev + 1;
|
| 187 |
+
return prev;
|
| 188 |
+
});
|
| 189 |
+
}, 1000);
|
| 190 |
|
| 191 |
try {
|
| 192 |
const res = await apiFetch('/api/v1/sleep', { method: 'POST' });
|
| 193 |
const data = await res.json();
|
| 194 |
+
|
| 195 |
+
clearInterval(stepInterval);
|
| 196 |
+
setSleepPhaseIndex(2); // Mark all checklists as complete
|
| 197 |
+
|
| 198 |
setSleepSummary({
|
| 199 |
+
linked: data.graph_relations_extracted || 0,
|
| 200 |
+
consolidated: data.summaries_created || 0,
|
| 201 |
+
pruned: data.messages_pruned || 0
|
|
|
|
| 202 |
});
|
| 203 |
} catch (error) {
|
| 204 |
+
clearInterval(stepInterval);
|
| 205 |
+
console.error("Sleep cycle failed:", error);
|
| 206 |
+
setSleepSummary({ linked: 0, consolidated: 0, pruned: 0 });
|
| 207 |
} finally {
|
| 208 |
+
setCognitiveState(COGNITIVE_PHASES.IDLE);
|
| 209 |
+
fetchVitals(); // Instantly update vitals to reflect pruned/cleared working queue
|
| 210 |
}
|
| 211 |
};
|
| 212 |
|
|
|
|
| 220 |
if (!username) return <AuthScreen onAuth={setUsername} />;
|
| 221 |
|
| 222 |
const stats = [
|
| 223 |
+
{ label: 'Working Memory', value: vitals?.working || '0', icon: 'psychology' },
|
| 224 |
+
{ label: 'Sensory Memory', value: vitals?.sensory || '0', icon: 'cloud' },
|
| 225 |
+
{ label: 'Semantic Memory', value: vitals?.semantic?.nodes || '0', icon: 'account_tree' },
|
| 226 |
+
{ label: 'Neural Sparks', value: vitals?.semantic?.edges || '0', icon: 'auto_awesome' },
|
| 227 |
];
|
| 228 |
|
| 229 |
return (
|
|
|
|
| 250 |
<span>{item.label}</span>
|
| 251 |
</button>
|
| 252 |
))}
|
| 253 |
+
|
| 254 |
+
<button
|
| 255 |
+
className="sidebar-link theme-toggle-btn"
|
| 256 |
+
onClick={() => setDarkMode(!darkMode)}
|
| 257 |
+
style={{marginTop: '16px', borderTop: '1px solid rgba(0,0,0,0.03)', paddingTop: '16px'}}
|
| 258 |
+
title="Toggle Dark/Light Mode"
|
| 259 |
+
>
|
| 260 |
+
<span className="material-icons">{darkMode ? 'light_mode' : 'dark_mode'}</span>
|
| 261 |
+
<span>{darkMode ? 'Light UI' : 'Dark UI'}</span>
|
| 262 |
+
</button>
|
| 263 |
</nav>
|
| 264 |
|
| 265 |
<div className="sidebar-footer">
|
|
|
|
| 269 |
</div>
|
| 270 |
<div className="session-copy">
|
| 271 |
<strong>{username}</strong>
|
| 272 |
+
<span>GUEST-7F3A</span>
|
| 273 |
</div>
|
| 274 |
<button style={{marginLeft: 'auto', color: '#999', background: 'transparent'}} onClick={handleLogout}>
|
| 275 |
<span className="material-icons" style={{fontSize: '18px'}}>logout</span>
|
|
|
|
| 281 |
<main className="soma-main-panel">
|
| 282 |
{activePage === 'console' && (
|
| 283 |
<section className="page-canvas fade-in">
|
| 284 |
+
<div className="page-header" style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
| 285 |
+
<h2>Cognitive Console</h2>
|
| 286 |
+
<button
|
| 287 |
+
className={`telemetry-trigger ${showStatus ? 'active' : ''}`}
|
| 288 |
+
onClick={() => setShowStatus(!showStatus)}
|
| 289 |
+
title="Toggle System Telemetry"
|
| 290 |
+
>
|
| 291 |
+
<span className="material-icons">analytics</span>
|
| 292 |
+
</button>
|
|
|
|
|
|
|
| 293 |
</div>
|
| 294 |
|
| 295 |
+
<div className="canvas-body" style={{display: 'flex', flex: 1, gap: '40px', minHeight: 0}}>
|
| 296 |
+
{/* Interaction Layer (The Chat) */}
|
| 297 |
+
<div className="chat-container" style={{flex: 0.7, display: 'flex', flexDirection: 'column'}}>
|
| 298 |
<ChatPanel
|
| 299 |
messages={messages}
|
| 300 |
onSendMessage={handleSendMessage}
|
| 301 |
+
onNewChat={() => {
|
| 302 |
+
setMessages([]);
|
| 303 |
+
setTrace([]);
|
| 304 |
+
}}
|
| 305 |
+
userAvatar={`https://api.dicebear.com/7.x/avataaars/svg?seed=${username}`}
|
| 306 |
+
isTyping={cognitiveState !== COGNITIVE_PHASES.IDLE && cognitiveState !== COGNITIVE_PHASES.LISTENING}
|
| 307 |
onInputStateChange={(isTyping) => {
|
| 308 |
if (cognitiveState === COGNITIVE_PHASES.IDLE && isTyping) setCognitiveState(COGNITIVE_PHASES.LISTENING);
|
| 309 |
if (cognitiveState === COGNITIVE_PHASES.LISTENING && !isTyping) setCognitiveState(COGNITIVE_PHASES.IDLE);
|
|
|
|
| 312 |
</div>
|
| 313 |
|
| 314 |
{/* Cognitive Layer (The Brain) */}
|
| 315 |
+
<div className="cognitive-container" style={{flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', position: 'relative'}}>
|
| 316 |
<CognitiveBrainImageScene state={cognitiveState} />
|
| 317 |
+
<div className="status-pill" style={{marginTop: '32px'}}>
|
| 318 |
+
<span className="label">Status</span>
|
| 319 |
+
<div className="value">
|
| 320 |
+
<div className={`status-dot ${cognitiveState !== COGNITIVE_PHASES.IDLE ? 'pulse' : ''}`} />
|
| 321 |
+
<span style={{textTransform: 'capitalize'}}>{cognitiveState}</span>
|
| 322 |
+
</div>
|
| 323 |
+
</div>
|
| 324 |
+
</div>
|
| 325 |
+
|
| 326 |
+
{/* Activity Layer (The Timeline) */}
|
| 327 |
+
<div className="activity-feed-wrapper" style={{flex: 0.6, maxWidth: '280px', display: 'flex', flexDirection: 'column', padding: '0', margin: '-50px 0 0 0', height: 'calc(100% + 50px)'}}>
|
| 328 |
+
<h3 style={{fontSize: '0.7rem', textTransform: 'uppercase', color: '#999', marginTop: 0, marginBottom: '8px', letterSpacing: '0.1em', fontWeight: 700}}>Activity Feed</h3>
|
| 329 |
+
<div style={{flex: 1, overflowY: 'auto', overflowX: 'hidden', paddingRight: '8px'}}>
|
| 330 |
+
<CognitiveTimeline trace={trace} />
|
| 331 |
+
</div>
|
| 332 |
</div>
|
| 333 |
</div>
|
| 334 |
</section>
|
|
|
|
| 337 |
{activePage === 'activity' && (
|
| 338 |
<section className="page-canvas fade-in">
|
| 339 |
<div className="page-header">
|
| 340 |
+
<h2>Neural Activity</h2>
|
| 341 |
<button className="sidebar-link" style={{background: 'white'}} onClick={() => setActivePage('console')}>
|
| 342 |
Back to Console
|
| 343 |
</button>
|
|
|
|
| 354 |
</section>
|
| 355 |
)}
|
| 356 |
|
| 357 |
+
{showStatus && (
|
| 358 |
+
<div className="telemetry-overlay" onClick={() => setShowStatus(false)}>
|
| 359 |
+
<div className="telemetry-modal" onClick={e => e.stopPropagation()}>
|
| 360 |
+
<div className="modal-header">
|
| 361 |
+
<h3>System Telemetry</h3>
|
| 362 |
+
<button className="modal-close" onClick={() => setShowStatus(false)}>
|
| 363 |
+
<span className="material-icons">close</span>
|
| 364 |
+
</button>
|
| 365 |
+
</div>
|
| 366 |
+
<div className="modal-scroll-area">
|
| 367 |
+
<CognitiveDashboard statusText={cognitiveState} stats={stats} />
|
| 368 |
+
</div>
|
| 369 |
+
</div>
|
| 370 |
+
</div>
|
| 371 |
)}
|
| 372 |
|
| 373 |
{activePage === 'memory' && (
|
| 374 |
<section className="page-canvas fade-in">
|
| 375 |
+
<div className="page-header"><h2>Neural Memory</h2></div>
|
| 376 |
<MemoryExplorer />
|
| 377 |
</section>
|
| 378 |
)}
|
| 379 |
|
| 380 |
{activePage === 'graph' && (
|
| 381 |
<section className="page-canvas fade-in">
|
| 382 |
+
<div className="page-header"><h2>Knowledge Graph</h2></div>
|
| 383 |
<KnowledgeGraph refreshTick={refreshTick} />
|
| 384 |
</section>
|
| 385 |
)}
|
|
|
|
| 387 |
{activePage === 'knowledge' && (
|
| 388 |
<section className="page-canvas fade-in">
|
| 389 |
<div className="page-header">
|
| 390 |
+
<h2>Neural Inscription</h2>
|
|
|
|
|
|
|
|
|
|
| 391 |
</div>
|
| 392 |
<KnowledgeInput onKnowledgeSubmit={handleKnowledgeSubmit} isBusy={knowledgeStatus.includes('Adding')} status={knowledgeStatus} />
|
| 393 |
</section>
|
| 394 |
)}
|
| 395 |
|
| 396 |
{activePage === 'sleep' && (
|
| 397 |
+
<SleepProgress
|
| 398 |
+
phaseIndex={sleepPhaseIndex}
|
| 399 |
+
isConsolidating={cognitiveState === 'consolidating'}
|
| 400 |
+
onStart={handleSleepCycle}
|
| 401 |
+
summary={sleepSummary}
|
| 402 |
+
vitals={vitals}
|
| 403 |
+
onClose={() => {
|
| 404 |
+
setCognitiveState(COGNITIVE_PHASES.IDLE);
|
| 405 |
+
setSleepPhaseIndex(0);
|
| 406 |
+
setSleepSummary(null);
|
| 407 |
+
setActivePage('console');
|
| 408 |
+
}}
|
| 409 |
+
/>
|
| 410 |
)}
|
| 411 |
</main>
|
| 412 |
</div>
|
frontend/src/assets/{hero.png → brain/Brain.png}
RENAMED
|
File without changes
|
frontend/src/assets/brain/Brain_nobg.png
ADDED
|
Git LFS Details
|
frontend/src/assets/brain/sleep.png
ADDED
|
Git LFS Details
|
frontend/src/assets/brain/sleep_nobg.png
ADDED
|
Git LFS Details
|
frontend/src/components/ChatPanel.css
CHANGED
|
@@ -12,22 +12,68 @@
|
|
| 12 |
padding-right: 12px;
|
| 13 |
display: flex;
|
| 14 |
flex-direction: column;
|
| 15 |
-
gap:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
|
| 18 |
.chat-bubble-group {
|
| 19 |
display: flex;
|
| 20 |
gap: 20px;
|
| 21 |
-
max-width:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
|
| 24 |
.avatar-wrap {
|
| 25 |
flex-shrink: 0;
|
| 26 |
}
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
.chat-avatar {
|
| 29 |
-
width:
|
| 30 |
-
height:
|
|
|
|
| 31 |
border-radius: 50%;
|
| 32 |
background-color: #f0f0f0;
|
| 33 |
display: flex;
|
|
@@ -37,14 +83,20 @@
|
|
| 37 |
box-shadow: 0 4px 10px rgba(0,0,0,0.02);
|
| 38 |
}
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
.chat-avatar .material-icons {
|
| 41 |
-
font-size:
|
| 42 |
color: #888;
|
| 43 |
}
|
| 44 |
|
| 45 |
.bubble-body {
|
| 46 |
background-color: white; /* Clearer white for premium look */
|
| 47 |
-
padding:
|
| 48 |
border-radius: 24px;
|
| 49 |
position: relative;
|
| 50 |
box-shadow: 0 4px 20px rgba(0,0,0,0.02);
|
|
@@ -52,50 +104,58 @@
|
|
| 52 |
}
|
| 53 |
|
| 54 |
.bubble-meta {
|
| 55 |
-
margin-bottom:
|
| 56 |
}
|
| 57 |
|
| 58 |
.bubble-meta strong {
|
| 59 |
-
font-size: 0.
|
| 60 |
font-weight: 700;
|
| 61 |
color: #1a1a1a;
|
| 62 |
letter-spacing: -0.01em;
|
| 63 |
}
|
| 64 |
|
| 65 |
.bubble-text {
|
| 66 |
-
font-size:
|
| 67 |
line-height: 1.6;
|
| 68 |
color: #444;
|
|
|
|
| 69 |
}
|
| 70 |
|
| 71 |
.bubble-time {
|
| 72 |
-
margin-top:
|
| 73 |
-
font-size: 0.
|
| 74 |
color: #bbb;
|
| 75 |
font-weight: 500;
|
| 76 |
}
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
/* ── Input Row ── */
|
| 79 |
.chat-input-row {
|
| 80 |
display: flex;
|
| 81 |
-
gap:
|
| 82 |
align-items: center;
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
.input-field-wrap {
|
| 86 |
flex: 1;
|
| 87 |
-
background-color:
|
| 88 |
-
border
|
| 89 |
-
|
|
|
|
|
|
|
| 90 |
display: flex;
|
| 91 |
align-items: center;
|
| 92 |
-
height:
|
| 93 |
transition: all 0.2s;
|
| 94 |
}
|
| 95 |
|
| 96 |
.input-field-wrap:focus-within {
|
| 97 |
-
|
| 98 |
-
box-shadow:
|
| 99 |
}
|
| 100 |
|
| 101 |
.input-field-wrap input {
|
|
@@ -103,31 +163,58 @@
|
|
| 103 |
background: transparent;
|
| 104 |
border: none;
|
| 105 |
outline: none;
|
| 106 |
-
font-size:
|
| 107 |
color: #1a1a1a;
|
| 108 |
font-weight: 500;
|
| 109 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
.input-field-wrap input::placeholder {
|
| 112 |
color: #aaa;
|
| 113 |
}
|
| 114 |
|
| 115 |
.send-btn {
|
| 116 |
-
width:
|
| 117 |
-
height:
|
| 118 |
background-color: #ff6b35;
|
| 119 |
-
border-radius:
|
| 120 |
display: flex;
|
| 121 |
align-items: center;
|
| 122 |
justify-content: center;
|
| 123 |
color: white;
|
| 124 |
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 125 |
-
box-shadow: 0 10px 25px rgba(255, 107, 53, 0.
|
| 126 |
}
|
| 127 |
|
| 128 |
.send-btn:hover:not(:disabled) {
|
| 129 |
transform: translateY(-2px);
|
| 130 |
-
box-shadow: 0 14px 30px rgba(255, 107, 53, 0.
|
| 131 |
}
|
| 132 |
|
| 133 |
.send-btn:active:not(:disabled) {
|
|
@@ -141,7 +228,7 @@
|
|
| 141 |
}
|
| 142 |
|
| 143 |
.send-btn .material-icons {
|
| 144 |
-
font-size:
|
| 145 |
}
|
| 146 |
|
| 147 |
/* ── Typing Indicator ── */
|
|
@@ -166,3 +253,75 @@
|
|
| 166 |
0%, 100% { opacity: 0.3; transform: scale(0.8); }
|
| 167 |
50% { opacity: 1; transform: scale(1.1); }
|
| 168 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
padding-right: 12px;
|
| 13 |
display: flex;
|
| 14 |
flex-direction: column;
|
| 15 |
+
gap: 16px;
|
| 16 |
+
scrollbar-width: thin;
|
| 17 |
+
scrollbar-color: rgba(0,0,0,0.05) transparent;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/* ── Custom Scrollbar (Subtle) ── */
|
| 21 |
+
.chat-messages::-webkit-scrollbar {
|
| 22 |
+
width: 4px;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
.chat-messages::-webkit-scrollbar-track {
|
| 26 |
+
background: transparent;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.chat-messages::-webkit-scrollbar-thumb {
|
| 30 |
+
background-color: rgba(0, 0, 0, 0.05);
|
| 31 |
+
border-radius: 10px;
|
| 32 |
+
transition: background-color 0.3s;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.chat-messages:hover::-webkit-scrollbar-thumb {
|
| 36 |
+
background-color: rgba(0, 0, 0, 0.1);
|
| 37 |
}
|
| 38 |
|
| 39 |
.chat-bubble-group {
|
| 40 |
display: flex;
|
| 41 |
gap: 20px;
|
| 42 |
+
max-width: 85%;
|
| 43 |
+
align-items: flex-start;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.chat-bubble-group.user {
|
| 47 |
+
align-self: flex-end;
|
| 48 |
+
flex-direction: row-reverse;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.chat-bubble-group.soma {
|
| 52 |
+
align-self: flex-start;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.chat-bubble-group.user .bubble-body {
|
| 56 |
+
background-color: #fcfcfc;
|
| 57 |
+
border-bottom-right-radius: 4px;
|
| 58 |
+
text-align: right;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.chat-bubble-group.soma .bubble-body {
|
| 62 |
+
border-bottom-left-radius: 4px;
|
| 63 |
}
|
| 64 |
|
| 65 |
.avatar-wrap {
|
| 66 |
flex-shrink: 0;
|
| 67 |
}
|
| 68 |
|
| 69 |
+
.bubble-header {
|
| 70 |
+
display: none;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
.chat-avatar {
|
| 74 |
+
width: 28px;
|
| 75 |
+
height: 28px;
|
| 76 |
+
flex-shrink: 0;
|
| 77 |
border-radius: 50%;
|
| 78 |
background-color: #f0f0f0;
|
| 79 |
display: flex;
|
|
|
|
| 83 |
box-shadow: 0 4px 10px rgba(0,0,0,0.02);
|
| 84 |
}
|
| 85 |
|
| 86 |
+
.chat-avatar img {
|
| 87 |
+
width: 100%;
|
| 88 |
+
height: 100%;
|
| 89 |
+
object-fit: cover;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
.chat-avatar .material-icons {
|
| 93 |
+
font-size: 16px;
|
| 94 |
color: #888;
|
| 95 |
}
|
| 96 |
|
| 97 |
.bubble-body {
|
| 98 |
background-color: white; /* Clearer white for premium look */
|
| 99 |
+
padding: 16px 20px;
|
| 100 |
border-radius: 24px;
|
| 101 |
position: relative;
|
| 102 |
box-shadow: 0 4px 20px rgba(0,0,0,0.02);
|
|
|
|
| 104 |
}
|
| 105 |
|
| 106 |
.bubble-meta {
|
| 107 |
+
margin-bottom: 4px;
|
| 108 |
}
|
| 109 |
|
| 110 |
.bubble-meta strong {
|
| 111 |
+
font-size: 0.7rem;
|
| 112 |
font-weight: 700;
|
| 113 |
color: #1a1a1a;
|
| 114 |
letter-spacing: -0.01em;
|
| 115 |
}
|
| 116 |
|
| 117 |
.bubble-text {
|
| 118 |
+
font-size: 0.8rem;
|
| 119 |
line-height: 1.6;
|
| 120 |
color: #444;
|
| 121 |
+
white-space: pre-wrap;
|
| 122 |
}
|
| 123 |
|
| 124 |
.bubble-time {
|
| 125 |
+
margin-top: 6px;
|
| 126 |
+
font-size: 0.7rem;
|
| 127 |
color: #bbb;
|
| 128 |
font-weight: 500;
|
| 129 |
}
|
| 130 |
|
| 131 |
+
.chat-bubble-group.user .bubble-time {
|
| 132 |
+
text-align: right;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
/* ── Input Row ── */
|
| 136 |
.chat-input-row {
|
| 137 |
display: flex;
|
| 138 |
+
gap: 10px;
|
| 139 |
align-items: center;
|
| 140 |
+
margin-top: 24px;
|
| 141 |
}
|
| 142 |
|
| 143 |
.input-field-wrap {
|
| 144 |
flex: 1;
|
| 145 |
+
background-color: white;
|
| 146 |
+
border: 1px solid rgba(0,0,0,0.04);
|
| 147 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.02);
|
| 148 |
+
border-radius: 14px;
|
| 149 |
+
padding: 0 16px;
|
| 150 |
display: flex;
|
| 151 |
align-items: center;
|
| 152 |
+
height: 48px;
|
| 153 |
transition: all 0.2s;
|
| 154 |
}
|
| 155 |
|
| 156 |
.input-field-wrap:focus-within {
|
| 157 |
+
border-color: rgba(255, 107, 53, 0.3);
|
| 158 |
+
box-shadow: 0 4px 20px rgba(255, 107, 53, 0.05);
|
| 159 |
}
|
| 160 |
|
| 161 |
.input-field-wrap input {
|
|
|
|
| 163 |
background: transparent;
|
| 164 |
border: none;
|
| 165 |
outline: none;
|
| 166 |
+
font-size: 0.85rem;
|
| 167 |
color: #1a1a1a;
|
| 168 |
font-weight: 500;
|
| 169 |
}
|
| 170 |
+
.new-chat-btn {
|
| 171 |
+
background: white;
|
| 172 |
+
border: 1px solid rgba(0,0,0,0.05);
|
| 173 |
+
width: 48px;
|
| 174 |
+
height: 48px;
|
| 175 |
+
border-radius: 12px;
|
| 176 |
+
display: flex;
|
| 177 |
+
align-items: center;
|
| 178 |
+
justify-content: center;
|
| 179 |
+
cursor: pointer;
|
| 180 |
+
color: #666;
|
| 181 |
+
transition: all 0.3s ease;
|
| 182 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.03);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.new-chat-btn:hover {
|
| 186 |
+
background: #f8f8f8;
|
| 187 |
+
color: #ff6b35;
|
| 188 |
+
transform: translateY(-2px);
|
| 189 |
+
box-shadow: 0 6px 20px rgba(0,0,0,0.06);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.new-chat-btn:disabled {
|
| 193 |
+
opacity: 0.5;
|
| 194 |
+
cursor: not-allowed;
|
| 195 |
+
transform: none;
|
| 196 |
+
}
|
| 197 |
|
| 198 |
.input-field-wrap input::placeholder {
|
| 199 |
color: #aaa;
|
| 200 |
}
|
| 201 |
|
| 202 |
.send-btn {
|
| 203 |
+
width: 48px;
|
| 204 |
+
height: 48px;
|
| 205 |
background-color: #ff6b35;
|
| 206 |
+
border-radius: 12px;
|
| 207 |
display: flex;
|
| 208 |
align-items: center;
|
| 209 |
justify-content: center;
|
| 210 |
color: white;
|
| 211 |
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 212 |
+
box-shadow: 0 10px 25px rgba(255, 107, 53, 0.2);
|
| 213 |
}
|
| 214 |
|
| 215 |
.send-btn:hover:not(:disabled) {
|
| 216 |
transform: translateY(-2px);
|
| 217 |
+
box-shadow: 0 14px 30px rgba(255, 107, 53, 0.3);
|
| 218 |
}
|
| 219 |
|
| 220 |
.send-btn:active:not(:disabled) {
|
|
|
|
| 228 |
}
|
| 229 |
|
| 230 |
.send-btn .material-icons {
|
| 231 |
+
font-size: 24px;
|
| 232 |
}
|
| 233 |
|
| 234 |
/* ── Typing Indicator ── */
|
|
|
|
| 253 |
0%, 100% { opacity: 0.3; transform: scale(0.8); }
|
| 254 |
50% { opacity: 1; transform: scale(1.1); }
|
| 255 |
}
|
| 256 |
+
|
| 257 |
+
/* ── Welcome State ── */
|
| 258 |
+
.welcome-state {
|
| 259 |
+
display: flex;
|
| 260 |
+
flex-direction: column;
|
| 261 |
+
align-items: center;
|
| 262 |
+
justify-content: center;
|
| 263 |
+
height: 100%;
|
| 264 |
+
text-align: center;
|
| 265 |
+
padding: 40px;
|
| 266 |
+
opacity: 0.6;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.welcome-icon {
|
| 270 |
+
width: 64px;
|
| 271 |
+
height: 64px;
|
| 272 |
+
background: white;
|
| 273 |
+
border-radius: 20px;
|
| 274 |
+
display: flex;
|
| 275 |
+
align-items: center;
|
| 276 |
+
justify-content: center;
|
| 277 |
+
margin-bottom: 24px;
|
| 278 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.welcome-icon span {
|
| 282 |
+
font-size: 32px;
|
| 283 |
+
color: #1a1a1a;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.welcome-state h3 {
|
| 287 |
+
font-size: 1rem;
|
| 288 |
+
font-weight: 700;
|
| 289 |
+
color: #1a1a1a;
|
| 290 |
+
margin-bottom: 12px;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.welcome-state p {
|
| 294 |
+
font-size: 0.75rem;
|
| 295 |
+
color: #999;
|
| 296 |
+
max-width: 200px;
|
| 297 |
+
line-height: 1.5;
|
| 298 |
+
margin-bottom: 32px;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.suggested-starters {
|
| 302 |
+
display: flex;
|
| 303 |
+
flex-direction: column;
|
| 304 |
+
gap: 10px;
|
| 305 |
+
width: 100%;
|
| 306 |
+
max-width: 240px;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.suggested-starters button {
|
| 310 |
+
background: white;
|
| 311 |
+
border: 1px solid rgba(0,0,0,0.05);
|
| 312 |
+
padding: 12px 16px;
|
| 313 |
+
border-radius: 12px;
|
| 314 |
+
font-size: 0.7rem;
|
| 315 |
+
color: #555;
|
| 316 |
+
text-align: left;
|
| 317 |
+
transition: all 0.2s;
|
| 318 |
+
cursor: pointer;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.suggested-starters button:hover {
|
| 322 |
+
background: #fdfdfd;
|
| 323 |
+
border-color: rgba(255, 107, 53, 0.2);
|
| 324 |
+
color: #ff6b35;
|
| 325 |
+
transform: translateY(-2px);
|
| 326 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.03);
|
| 327 |
+
}
|
frontend/src/components/ChatPanel.jsx
CHANGED
|
@@ -7,7 +7,7 @@ import './ChatPanel.css';
|
|
| 7 |
* Layer 3: Interaction (Utility Layer)
|
| 8 |
* Handles live conversation and input.
|
| 9 |
*/
|
| 10 |
-
function ChatPanel({ messages, onSendMessage, isTyping, onInputStateChange }) {
|
| 11 |
const [input, setInput] = useState('');
|
| 12 |
const scrollRef = useRef(null);
|
| 13 |
|
|
@@ -37,16 +37,23 @@ function ChatPanel({ messages, onSendMessage, isTyping, onInputStateChange }) {
|
|
| 37 |
return (
|
| 38 |
<div className="chat-interface">
|
| 39 |
<div className="chat-messages" ref={scrollRef}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
{messages.map((msg, i) => (
|
| 41 |
<div key={i} className={`chat-bubble-group ${msg.role}`}>
|
| 42 |
-
<div className="
|
| 43 |
-
|
| 44 |
-
{
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
)}
|
| 49 |
-
</div>
|
| 50 |
</div>
|
| 51 |
<div className="bubble-body">
|
| 52 |
<div className="bubble-meta">
|
|
@@ -72,6 +79,15 @@ function ChatPanel({ messages, onSendMessage, isTyping, onInputStateChange }) {
|
|
| 72 |
</div>
|
| 73 |
|
| 74 |
<form className="chat-input-row" onSubmit={handleSubmit}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
<div className="input-field-wrap">
|
| 76 |
<input
|
| 77 |
type="text"
|
|
|
|
| 7 |
* Layer 3: Interaction (Utility Layer)
|
| 8 |
* Handles live conversation and input.
|
| 9 |
*/
|
| 10 |
+
function ChatPanel({ messages, onSendMessage, onNewChat, userAvatar, isTyping, onInputStateChange }) {
|
| 11 |
const [input, setInput] = useState('');
|
| 12 |
const scrollRef = useRef(null);
|
| 13 |
|
|
|
|
| 37 |
return (
|
| 38 |
<div className="chat-interface">
|
| 39 |
<div className="chat-messages" ref={scrollRef}>
|
| 40 |
+
{messages.length === 0 && (
|
| 41 |
+
<div className="welcome-state">
|
| 42 |
+
<div className="welcome-icon">
|
| 43 |
+
<span className="material-icons">lens_blur</span>
|
| 44 |
+
</div>
|
| 45 |
+
<h3>Neural Core Initialized.</h3>
|
| 46 |
+
<p>Feed me a thought to begin building your neural pathways.</p>
|
| 47 |
+
</div>
|
| 48 |
+
)}
|
| 49 |
{messages.map((msg, i) => (
|
| 50 |
<div key={i} className={`chat-bubble-group ${msg.role}`}>
|
| 51 |
+
<div className="chat-avatar">
|
| 52 |
+
{msg.role === 'user' ? (
|
| 53 |
+
<img src={userAvatar} alt="User" />
|
| 54 |
+
) : (
|
| 55 |
+
<span className="material-icons">lens_blur</span>
|
| 56 |
+
)}
|
|
|
|
|
|
|
| 57 |
</div>
|
| 58 |
<div className="bubble-body">
|
| 59 |
<div className="bubble-meta">
|
|
|
|
| 79 |
</div>
|
| 80 |
|
| 81 |
<form className="chat-input-row" onSubmit={handleSubmit}>
|
| 82 |
+
<button
|
| 83 |
+
className="new-chat-btn"
|
| 84 |
+
type="button"
|
| 85 |
+
onClick={onNewChat}
|
| 86 |
+
title="New Chat"
|
| 87 |
+
disabled={isTyping}
|
| 88 |
+
>
|
| 89 |
+
<span className="material-icons">add</span>
|
| 90 |
+
</button>
|
| 91 |
<div className="input-field-wrap">
|
| 92 |
<input
|
| 93 |
type="text"
|
frontend/src/components/CognitiveBrain3DScene.css
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* CognitiveBrain3DScene.css - Sci-fi Organic 3D Brain simulation styling */
|
| 2 |
+
|
| 3 |
+
.brain-3d-viewport {
|
| 4 |
+
position: relative;
|
| 5 |
+
width: 100%;
|
| 6 |
+
height: 100%;
|
| 7 |
+
background: radial-gradient(circle at center, rgba(15, 23, 42, 0.05) 0%, rgba(10, 10, 15, 0) 70%);
|
| 8 |
+
border-radius: 24px;
|
| 9 |
+
overflow: hidden;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
/* Micro HUD floating overlay */
|
| 13 |
+
.brain-hud-metrics {
|
| 14 |
+
position: absolute;
|
| 15 |
+
bottom: 24px;
|
| 16 |
+
left: 24px;
|
| 17 |
+
right: 24px;
|
| 18 |
+
display: flex;
|
| 19 |
+
justify-content: space-between;
|
| 20 |
+
gap: 16px;
|
| 21 |
+
background: rgba(255, 255, 255, 0.4);
|
| 22 |
+
backdrop-filter: blur(20px);
|
| 23 |
+
border: 1px solid rgba(255, 255, 255, 0.6);
|
| 24 |
+
padding: 14px 20px;
|
| 25 |
+
border-radius: 16px;
|
| 26 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.03);
|
| 27 |
+
pointer-events: none;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.hud-metric-item {
|
| 31 |
+
display: flex;
|
| 32 |
+
flex-direction: column;
|
| 33 |
+
gap: 4px;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.hud-label {
|
| 37 |
+
font-size: 0.6rem;
|
| 38 |
+
font-weight: 700;
|
| 39 |
+
text-transform: uppercase;
|
| 40 |
+
color: #888;
|
| 41 |
+
letter-spacing: 0.05em;
|
| 42 |
+
font-family: var(--font-mono), monospace;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.hud-val {
|
| 46 |
+
font-size: 0.8rem;
|
| 47 |
+
font-weight: 800;
|
| 48 |
+
color: #1a1a1a;
|
| 49 |
+
letter-spacing: -0.01em;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.hud-val.uppercase {
|
| 53 |
+
text-transform: uppercase;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Utility Colors */
|
| 57 |
+
.text-cyan { color: #06b6d4 !important; }
|
| 58 |
+
.text-pink { color: #ec4899 !important; }
|
| 59 |
+
|
| 60 |
+
/* Micro-HUD tooltips */
|
| 61 |
+
.brain-tooltip-hud {
|
| 62 |
+
background: rgba(10, 10, 15, 0.95) !important;
|
| 63 |
+
border: 1px solid rgba(6, 182, 212, 0.3) !important;
|
| 64 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.5) !important;
|
| 65 |
+
border-radius: 8px !important;
|
| 66 |
+
padding: 8px 12px !important;
|
| 67 |
+
color: #fff !important;
|
| 68 |
+
font-family: var(--font-mono), monospace !important;
|
| 69 |
+
font-size: 0.7rem !important;
|
| 70 |
+
pointer-events: none;
|
| 71 |
+
display: flex;
|
| 72 |
+
flex-direction: column;
|
| 73 |
+
gap: 2px;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.brain-tooltip-hud strong {
|
| 77 |
+
color: #06b6d4;
|
| 78 |
+
font-size: 0.75rem;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.hud-category {
|
| 82 |
+
color: #888;
|
| 83 |
+
font-size: 0.6rem;
|
| 84 |
+
text-transform: uppercase;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/* Loading label */
|
| 88 |
+
.brain-loading-label {
|
| 89 |
+
position: absolute;
|
| 90 |
+
top: 24px;
|
| 91 |
+
right: 24px;
|
| 92 |
+
background: rgba(6, 182, 212, 0.08);
|
| 93 |
+
border: 1px solid rgba(6, 182, 212, 0.2);
|
| 94 |
+
color: #06b6d4;
|
| 95 |
+
padding: 6px 14px;
|
| 96 |
+
border-radius: 100px;
|
| 97 |
+
font-size: 0.65rem;
|
| 98 |
+
font-weight: 700;
|
| 99 |
+
font-family: var(--font-mono), monospace;
|
| 100 |
+
text-transform: uppercase;
|
| 101 |
+
letter-spacing: 0.05em;
|
| 102 |
+
pointer-events: none;
|
| 103 |
+
animation: pulse-hud 1.5s infinite ease-in-out;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
@keyframes pulse-hud {
|
| 107 |
+
0% { opacity: 0.8; }
|
| 108 |
+
50% { opacity: 0.4; }
|
| 109 |
+
100% { opacity: 0.8; }
|
| 110 |
+
}
|
frontend/src/components/CognitiveBrain3DScene.jsx
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState, useRef, useMemo } from 'react';
|
| 2 |
+
import ForceGraph3D from 'react-force-graph-3d';
|
| 3 |
+
import { apiFetch } from '../api';
|
| 4 |
+
import './CognitiveBrain3DScene.css';
|
| 5 |
+
|
| 6 |
+
// Fixed Brain Lobes coordinates to anchor the 3D biological silhouette
|
| 7 |
+
const LOBES = [
|
| 8 |
+
{ id: 'LOBE_PREFRONTAL', label: 'Prefrontal Lobe (Reasoning & Language)', fx: 50, fy: 35, fz: 0, type: 'lobe', key: 'prefrontal' },
|
| 9 |
+
{ id: 'LOBE_TEMPORAL', label: 'Temporal Lobe (Memory & Association)', fx: -10, fy: -35, fz: -15, type: 'lobe', key: 'hippocampus' },
|
| 10 |
+
{ id: 'LOBE_PARIETAL', label: 'Parietal Lobe (Sensory & Intake)', fx: -55, fy: 20, fz: 0, type: 'lobe', key: 'sensory' },
|
| 11 |
+
{ id: 'LOBE_SUBCORTICAL', label: 'Subcortical Hub (Attention & Routing)', fx: 0, fy: 10, fz: 15, type: 'lobe', key: 'thalamus' }
|
| 12 |
+
];
|
| 13 |
+
|
| 14 |
+
const PHASE_TO_REGION = {
|
| 15 |
+
perception: 'sensory',
|
| 16 |
+
sensory: 'sensory',
|
| 17 |
+
attention: 'thalamus',
|
| 18 |
+
routing: 'thalamus',
|
| 19 |
+
prediction: 'prefrontal',
|
| 20 |
+
working_memory: 'prefrontal',
|
| 21 |
+
recall: 'hippocampus',
|
| 22 |
+
association: 'hippocampus',
|
| 23 |
+
memory: 'hippocampus',
|
| 24 |
+
emotion: 'thalamus', // Grouped in subcortical hub for positioning
|
| 25 |
+
reasoning: 'prefrontal',
|
| 26 |
+
reflection: 'prefrontal',
|
| 27 |
+
language: 'prefrontal',
|
| 28 |
+
inhibition: 'hippocampus',
|
| 29 |
+
graph: 'hippocampus',
|
| 30 |
+
listening: 'sensory',
|
| 31 |
+
responding: 'prefrontal'
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
// Deterministic hash to anchor learned entities to specific brain lobes organically
|
| 35 |
+
const hashString = (str) => {
|
| 36 |
+
let hash = 0;
|
| 37 |
+
for (let i = 0; i < str.length; i++) {
|
| 38 |
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
| 39 |
+
}
|
| 40 |
+
return Math.abs(hash);
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
function CognitiveBrain3DScene({ state = 'idle', refreshTick }) {
|
| 44 |
+
const [graphData, setGraphData] = useState({ nodes: [], links: [] });
|
| 45 |
+
const [newNodes, setNewNodes] = useState(new Set());
|
| 46 |
+
const [dimensions, setDimensions] = useState({ width: 500, height: 400 });
|
| 47 |
+
const [loading, setLoading] = useState(false);
|
| 48 |
+
|
| 49 |
+
const containerRef = useRef(null);
|
| 50 |
+
const fgRef = useRef();
|
| 51 |
+
|
| 52 |
+
// Highlight the active lobe based on SOMA's cognitive phase
|
| 53 |
+
const activeRegion = PHASE_TO_REGION[state] || null;
|
| 54 |
+
|
| 55 |
+
// Track layout resize
|
| 56 |
+
useEffect(() => {
|
| 57 |
+
if (!containerRef.current) return;
|
| 58 |
+
const resizeObserver = new ResizeObserver((entries) => {
|
| 59 |
+
for (let entry of entries) {
|
| 60 |
+
const { width, height } = entry.contentRect;
|
| 61 |
+
setDimensions({ width: width || 500, height: height || 400 });
|
| 62 |
+
}
|
| 63 |
+
});
|
| 64 |
+
resizeObserver.observe(containerRef.current);
|
| 65 |
+
return () => resizeObserver.disconnect();
|
| 66 |
+
}, []);
|
| 67 |
+
|
| 68 |
+
// Fetch real-time learned knowledge from Neo4j database
|
| 69 |
+
useEffect(() => {
|
| 70 |
+
const fetchBrainData = async () => {
|
| 71 |
+
setLoading(true);
|
| 72 |
+
try {
|
| 73 |
+
const res = await apiFetch('/api/v1/graph');
|
| 74 |
+
if (!res.ok) throw new Error('Failed to fetch graph data');
|
| 75 |
+
const data = await res.json();
|
| 76 |
+
|
| 77 |
+
// 1. Start with the permanent brain structures (The 4 Lobes)
|
| 78 |
+
const nodes = LOBES.map(l => ({ ...l }));
|
| 79 |
+
const links = [];
|
| 80 |
+
|
| 81 |
+
// Track new nodes to trigger the "synaptic spark" animation
|
| 82 |
+
const currentEntityIds = new Set(data.nodes?.map(n => n.id) || []);
|
| 83 |
+
const previouslyKnownIds = new Set(graphData.nodes.filter(n => n.type !== 'lobe').map(n => n.id));
|
| 84 |
+
const newlyDiscovered = new Set([...currentEntityIds].filter(id => !previouslyKnownIds.has(id)));
|
| 85 |
+
|
| 86 |
+
if (newlyDiscovered.size > 0) {
|
| 87 |
+
setNewNodes(newlyDiscovered);
|
| 88 |
+
// Clear spark animation after 4 seconds
|
| 89 |
+
setTimeout(() => setNewNodes(new Set()), 4000);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
if (data.nodes && data.nodes.length > 0) {
|
| 93 |
+
// 2. Map actual database entities into the brain
|
| 94 |
+
data.nodes.forEach(n => {
|
| 95 |
+
const entityId = n.id;
|
| 96 |
+
|
| 97 |
+
// Assign this entity deterministically to a brain lobe based on name hash
|
| 98 |
+
const assignedLobeIndex = hashString(entityId) % LOBES.length;
|
| 99 |
+
const targetLobeId = LOBES[assignedLobeIndex].id;
|
| 100 |
+
|
| 101 |
+
nodes.push({
|
| 102 |
+
id: entityId,
|
| 103 |
+
label: n.label || entityId,
|
| 104 |
+
connections: n.connections || 1,
|
| 105 |
+
type: 'entity'
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
// Structural bio-link to anchor it to its parent lobe
|
| 109 |
+
links.push({
|
| 110 |
+
source: targetLobeId,
|
| 111 |
+
target: entityId,
|
| 112 |
+
type: 'structural'
|
| 113 |
+
});
|
| 114 |
+
});
|
| 115 |
+
|
| 116 |
+
// 3. Map actual semantic links (Komal -> IS_A -> Student)
|
| 117 |
+
if (data.edges) {
|
| 118 |
+
data.edges.forEach(e => {
|
| 119 |
+
links.push({
|
| 120 |
+
source: e.source,
|
| 121 |
+
target: e.target,
|
| 122 |
+
label: e.label || 'ASSOCIATED_WITH',
|
| 123 |
+
type: 'semantic'
|
| 124 |
+
});
|
| 125 |
+
});
|
| 126 |
+
}
|
| 127 |
+
} else {
|
| 128 |
+
// If Neo4j is offline/empty, seed a few highly visual concept cells
|
| 129 |
+
// representing basic thoughts so it never looks completely empty
|
| 130 |
+
const sampleEntities = ['Komal', 'Student', 'SOMA', 'AI Architecture', 'Learning System'];
|
| 131 |
+
sampleEntities.forEach((entityId, index) => {
|
| 132 |
+
const targetLobeId = LOBES[index % LOBES.length].id;
|
| 133 |
+
nodes.push({ id: entityId, label: entityId, connections: 2, type: 'entity' });
|
| 134 |
+
links.push({ source: targetLobeId, target: entityId, type: 'structural' });
|
| 135 |
+
});
|
| 136 |
+
links.push({ source: 'Komal', target: 'Student', label: 'IS_A', type: 'semantic' });
|
| 137 |
+
links.push({ source: 'SOMA', target: 'AI Architecture', label: 'BUILT_ON', type: 'semantic' });
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
setGraphData({ nodes, links });
|
| 141 |
+
} catch (error) {
|
| 142 |
+
console.error('Brain mesh load error', error);
|
| 143 |
+
} finally {
|
| 144 |
+
setLoading(false);
|
| 145 |
+
}
|
| 146 |
+
};
|
| 147 |
+
|
| 148 |
+
fetchBrainData();
|
| 149 |
+
}, [refreshTick]);
|
| 150 |
+
|
| 151 |
+
// Adjust camera to focus on the active lobe during cognitive processing
|
| 152 |
+
useEffect(() => {
|
| 153 |
+
if (activeRegion && fgRef.current) {
|
| 154 |
+
const activeLobe = LOBES.find(l => l.key === activeRegion);
|
| 155 |
+
if (activeLobe) {
|
| 156 |
+
fgRef.current.cameraPosition(
|
| 157 |
+
{ x: activeLobe.fx * 1.5, y: activeLobe.fy * 1.5, z: 180 }, // Move camera closer
|
| 158 |
+
{ x: activeLobe.fx * 0.5, y: activeLobe.fy * 0.5, z: 0 }, // Point at the lobe
|
| 159 |
+
1200
|
| 160 |
+
);
|
| 161 |
+
}
|
| 162 |
+
} else if (fgRef.current) {
|
| 163 |
+
// Zoom out to global brain view when idle
|
| 164 |
+
fgRef.current.cameraPosition({ x: 0, y: 0, z: 240 }, { x: 0, y: 0, z: 0 }, 1500);
|
| 165 |
+
}
|
| 166 |
+
}, [activeRegion]);
|
| 167 |
+
|
| 168 |
+
// Dynamic Node Styling
|
| 169 |
+
const getNodeColor = (node) => {
|
| 170 |
+
if (node.type === 'lobe') {
|
| 171 |
+
// Core Lobes glow when they are actively processing
|
| 172 |
+
if (node.key === activeRegion) {
|
| 173 |
+
return '#ffffff'; // Intense active state (pure white core)
|
| 174 |
+
}
|
| 175 |
+
return 'rgba(6, 182, 212, 0.4)'; // Faint resting lobe core (Cyan outline)
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
// Spark animation for newly learned nodes (Bright yellow-gold)
|
| 179 |
+
if (newNodes.has(node.id)) {
|
| 180 |
+
return '#f59e0b';
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
// Regular synapses colored based on connection degree
|
| 184 |
+
return (node.connections || 1) > 3 ? '#ec4899' : '#06b6d4';
|
| 185 |
+
};
|
| 186 |
+
|
| 187 |
+
const getNodeVal = (node) => {
|
| 188 |
+
if (node.type === 'lobe') {
|
| 189 |
+
// Active processing lobes grow dynamically in physical scale
|
| 190 |
+
return node.key === activeRegion ? 12 : 5;
|
| 191 |
+
}
|
| 192 |
+
// Normal synapses sized by centrality
|
| 193 |
+
return Math.max(1.8, Math.min((node.connections || 1) * 1.2, 6));
|
| 194 |
+
};
|
| 195 |
+
|
| 196 |
+
return (
|
| 197 |
+
<div className="brain-3d-viewport" ref={containerRef}>
|
| 198 |
+
{/* 3D WebGL Learning Brain Simulation */}
|
| 199 |
+
<ForceGraph3D
|
| 200 |
+
ref={fgRef}
|
| 201 |
+
graphData={graphData}
|
| 202 |
+
width={dimensions.width}
|
| 203 |
+
height={dimensions.height}
|
| 204 |
+
backgroundColor="rgba(0,0,0,0)" // Fully transparent to blend with dashboard
|
| 205 |
+
nodeLabel={node => `
|
| 206 |
+
<div class="brain-tooltip-hud">
|
| 207 |
+
<strong>${node.label || node.id}</strong>
|
| 208 |
+
<span class="hud-category">${node.type === 'lobe' ? 'Brain Cortex Lobe' : 'Learned Synaptic Concept'}</span>
|
| 209 |
+
</div>
|
| 210 |
+
`}
|
| 211 |
+
nodeColor={getNodeColor}
|
| 212 |
+
nodeVal={getNodeVal}
|
| 213 |
+
nodeRelSize={3}
|
| 214 |
+
// Link customizations to distinguish biology vs semantic relations
|
| 215 |
+
linkColor={link => {
|
| 216 |
+
if (link.type === 'structural') return 'rgba(255,255,255,0.03)'; // Structural bio-lines are nearly invisible
|
| 217 |
+
return 'rgba(6, 182, 212, 0.25)'; // Real semantic memories are glowing cyan axons
|
| 218 |
+
}}
|
| 219 |
+
linkWidth={link => (link.type === 'structural' ? 0.5 : 1.5)}
|
| 220 |
+
linkDirectionalParticles={link => {
|
| 221 |
+
if (link.type === 'structural') return 0;
|
| 222 |
+
// Newly activated semantic links run rapid data flows
|
| 223 |
+
return link.source.id === activeRegion || link.target.id === activeRegion ? 6 : 2;
|
| 224 |
+
}}
|
| 225 |
+
linkDirectionalParticleSpeed={0.008}
|
| 226 |
+
linkDirectionalParticleWidth={link => (link.type === 'structural' ? 0 : 2.5)}
|
| 227 |
+
linkDirectionalParticleColor={() => '#ec4899'} // Hot pink thought particles flowing through the axons
|
| 228 |
+
showNavInfo={false}
|
| 229 |
+
enablePointerInteraction={true}
|
| 230 |
+
enableNodeDrag={true}
|
| 231 |
+
/>
|
| 232 |
+
|
| 233 |
+
{/* Futuristic status HUD overlays */}
|
| 234 |
+
<div className="brain-hud-metrics">
|
| 235 |
+
<div className="hud-metric-item">
|
| 236 |
+
<span className="hud-label">Cognitive Mode</span>
|
| 237 |
+
<strong className="hud-val uppercase pulse">{state}</strong>
|
| 238 |
+
</div>
|
| 239 |
+
<div className="hud-metric-item">
|
| 240 |
+
<span className="hud-label">Active Lobe</span>
|
| 241 |
+
<strong className="hud-val text-cyan uppercase">{activeRegion || 'STANDBY'}</strong>
|
| 242 |
+
</div>
|
| 243 |
+
<div className="hud-metric-item">
|
| 244 |
+
<span className="hud-label">Learned Synapses</span>
|
| 245 |
+
<strong className="hud-val text-pink">
|
| 246 |
+
{graphData.nodes.filter(n => n.type !== 'lobe').length} nodes
|
| 247 |
+
</strong>
|
| 248 |
+
</div>
|
| 249 |
+
</div>
|
| 250 |
+
|
| 251 |
+
{loading && <div className="brain-loading-label">Sparking neural networks...</div>}
|
| 252 |
+
</div>
|
| 253 |
+
);
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
export default CognitiveBrain3DScene;
|
frontend/src/components/CognitiveBrainImageScene.jsx
CHANGED
|
@@ -1,9 +1,4 @@
|
|
| 1 |
-
import
|
| 2 |
-
import prefrontalImg from '../assets/brain/Prefrontal_Cortex.png';
|
| 3 |
-
import hippocampusImg from '../assets/brain/Hippocampus.png';
|
| 4 |
-
import sensoryImg from '../assets/brain/sensory_cortex.png';
|
| 5 |
-
import thalamusImg from '../assets/brain/thalamus.png';
|
| 6 |
-
import amygdalaImg from '../assets/brain/brain_amygdala.png';
|
| 7 |
import './CognitiveBrainScene.css';
|
| 8 |
|
| 9 |
/**
|
|
@@ -22,9 +17,11 @@ const REGION_MAP = {
|
|
| 22 |
emotion: 'amygdala',
|
| 23 |
reasoning: 'prefrontal',
|
| 24 |
reflection: 'prefrontal',
|
| 25 |
-
language: 'prefrontal',
|
| 26 |
-
inhibition: '
|
| 27 |
-
graph: 'hippocampus'
|
|
|
|
|
|
|
| 28 |
};
|
| 29 |
|
| 30 |
function CognitiveBrainImageScene({ state = 'idle' }) {
|
|
@@ -33,52 +30,55 @@ function CognitiveBrainImageScene({ state = 'idle' }) {
|
|
| 33 |
|
| 34 |
return (
|
| 35 |
<div className={`brain-viewport state-${state}`}>
|
| 36 |
-
{/* Layer
|
| 37 |
-
<div className="
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
<div className=
|
| 41 |
-
<
|
| 42 |
-
<
|
|
|
|
|
|
|
|
|
|
| 43 |
</div>
|
| 44 |
-
</div>
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
| 51 |
</div>
|
| 52 |
-
</div>
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
| 59 |
</div>
|
| 60 |
-
</div>
|
| 61 |
-
</div>
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
<
|
| 71 |
-
<img
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
{/* State
|
| 76 |
<div className={`glow-hub prefrontal-glow ${activeRegion === 'prefrontal' ? 'visible' : ''}`} />
|
| 77 |
<div className={`glow-hub parietal-glow ${activeRegion === 'sensory' ? 'visible' : ''}`} />
|
| 78 |
<div className={`glow-hub temporal-glow ${activeRegion === 'hippocampus' ? 'visible' : ''}`} />
|
| 79 |
<div className={`glow-hub thalamus-glow ${activeRegion === 'thalamus' || activeRegion === 'amygdala' ? 'visible' : ''}`} />
|
| 80 |
|
| 81 |
-
{/*
|
| 82 |
<div className="ambient-breath" />
|
| 83 |
</div>
|
| 84 |
|
|
|
|
| 1 |
+
import brainImg from '../assets/brain/Brain_nobg.png';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import './CognitiveBrainScene.css';
|
| 3 |
|
| 4 |
/**
|
|
|
|
| 17 |
emotion: 'amygdala',
|
| 18 |
reasoning: 'prefrontal',
|
| 19 |
reflection: 'prefrontal',
|
| 20 |
+
language: 'prefrontal',
|
| 21 |
+
inhibition: 'hippocampus', // Moved to Temporal for smoother memory filtering flow
|
| 22 |
+
graph: 'hippocampus',
|
| 23 |
+
listening: 'sensory',
|
| 24 |
+
responding: 'prefrontal'
|
| 25 |
};
|
| 26 |
|
| 27 |
function CognitiveBrainImageScene({ state = 'idle' }) {
|
|
|
|
| 30 |
|
| 31 |
return (
|
| 32 |
<div className={`brain-viewport state-${state}`}>
|
| 33 |
+
{/* Layer 1: Brain Core */}
|
| 34 |
+
<div className="brain-core">
|
| 35 |
+
{/* Layer 2: Cognitive Signals (Floating Ambient Labels) */}
|
| 36 |
+
<div className="neural-signals">
|
| 37 |
+
<div className={`signal-node prefrontal ${activeRegion === 'prefrontal' ? 'active' : ''} ${isState('reasoning') ? 'hot' : ''}`}>
|
| 38 |
+
<div className="signal-dot"></div>
|
| 39 |
+
<div className="signal-copy">
|
| 40 |
+
<strong>Prefrontal</strong>
|
| 41 |
+
<span>Reasoning & Reflection</span>
|
| 42 |
+
</div>
|
| 43 |
</div>
|
|
|
|
| 44 |
|
| 45 |
+
<div className={`signal-node parietal ${activeRegion === 'sensory' ? 'active' : ''}`}>
|
| 46 |
+
<div className="signal-dot"></div>
|
| 47 |
+
<div className="signal-copy">
|
| 48 |
+
<strong>Parietal</strong>
|
| 49 |
+
<span>Perception & Sensory</span>
|
| 50 |
+
</div>
|
| 51 |
</div>
|
|
|
|
| 52 |
|
| 53 |
+
<div className={`signal-node temporal ${activeRegion === 'hippocampus' ? 'active' : ''}`}>
|
| 54 |
+
<div className="signal-dot"></div>
|
| 55 |
+
<div className="signal-copy">
|
| 56 |
+
<strong>Temporal</strong>
|
| 57 |
+
<span>Memory & Association</span>
|
| 58 |
+
</div>
|
| 59 |
</div>
|
|
|
|
|
|
|
| 60 |
|
| 61 |
+
<div className={`signal-node thalamus-label ${activeRegion === 'thalamus' || activeRegion === 'amygdala' ? 'active' : ''}`}>
|
| 62 |
+
<div className="signal-dot"></div>
|
| 63 |
+
<div className="signal-copy">
|
| 64 |
+
<strong>Subcortical</strong>
|
| 65 |
+
<span>Attention & Routing</span>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
<img
|
| 70 |
+
src={brainImg}
|
| 71 |
+
className="brain-layer base active"
|
| 72 |
+
alt="Soma Brain"
|
| 73 |
+
/>
|
| 74 |
|
| 75 |
+
{/* State Glow Hubs (Subtle Ambient Backglow) */}
|
| 76 |
<div className={`glow-hub prefrontal-glow ${activeRegion === 'prefrontal' ? 'visible' : ''}`} />
|
| 77 |
<div className={`glow-hub parietal-glow ${activeRegion === 'sensory' ? 'visible' : ''}`} />
|
| 78 |
<div className={`glow-hub temporal-glow ${activeRegion === 'hippocampus' ? 'visible' : ''}`} />
|
| 79 |
<div className={`glow-hub thalamus-glow ${activeRegion === 'thalamus' || activeRegion === 'amygdala' ? 'visible' : ''}`} />
|
| 80 |
|
| 81 |
+
{/* Interaction Particles */}
|
| 82 |
<div className="ambient-breath" />
|
| 83 |
</div>
|
| 84 |
|
frontend/src/components/CognitiveBrainScene.css
CHANGED
|
@@ -2,59 +2,81 @@
|
|
| 2 |
|
| 3 |
.brain-viewport {
|
| 4 |
width: 100%;
|
| 5 |
-
height:
|
| 6 |
position: relative;
|
| 7 |
display: flex;
|
| 8 |
align-items: center;
|
| 9 |
justify-content: center;
|
| 10 |
-
perspective:
|
| 11 |
}
|
| 12 |
|
| 13 |
.brain-core {
|
| 14 |
position: relative;
|
| 15 |
-
width:
|
| 16 |
-
height:
|
| 17 |
z-index: 10;
|
|
|
|
| 18 |
}
|
| 19 |
|
| 20 |
-
/* ── Layer Stacking ── */
|
| 21 |
.brain-layer {
|
| 22 |
position: absolute;
|
| 23 |
top: 0;
|
| 24 |
left: 0;
|
| 25 |
width: 100%;
|
| 26 |
-
height:
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
-
|
|
|
|
| 31 |
z-index: 1;
|
| 32 |
-
filter: contrast(1.1) brightness(0.9);
|
| 33 |
}
|
| 34 |
|
| 35 |
-
.brain-layer.region {
|
| 36 |
z-index: 2;
|
| 37 |
-
opacity:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
.brain-layer.region.active {
|
| 41 |
-
|
| 42 |
-
filter: brightness(1.2) drop-shadow(0 0 15px rgba(255, 107, 53, 0.4));
|
| 43 |
}
|
| 44 |
|
| 45 |
/* ── Layer 1: Ambient Motion (Breathing) ── */
|
| 46 |
.ambient-breath {
|
| 47 |
position: absolute;
|
| 48 |
inset: 0;
|
| 49 |
-
background: radial-gradient(circle, rgba(255, 107, 53, 0.
|
| 50 |
animation: breathing 8s infinite ease-in-out;
|
| 51 |
pointer-events: none;
|
| 52 |
z-index: -1;
|
| 53 |
}
|
| 54 |
|
| 55 |
@keyframes breathing {
|
| 56 |
-
0%, 100% { transform: scale(0.
|
| 57 |
-
50% { transform: scale(1.
|
| 58 |
}
|
| 59 |
|
| 60 |
/* ── Layer 2: Cognitive Signals (Floating Labels) ── */
|
|
@@ -71,14 +93,15 @@
|
|
| 71 |
align-items: center;
|
| 72 |
gap: 16px;
|
| 73 |
opacity: 0.2;
|
| 74 |
-
transition: all 0.
|
| 75 |
-
filter: grayscale(1) blur(
|
| 76 |
}
|
| 77 |
|
| 78 |
.signal-node.active {
|
| 79 |
opacity: 1;
|
| 80 |
filter: grayscale(0) blur(0);
|
| 81 |
-
transform: scale(1.
|
|
|
|
| 82 |
}
|
| 83 |
|
| 84 |
.signal-node.hot {
|
|
@@ -88,74 +111,96 @@
|
|
| 88 |
}
|
| 89 |
|
| 90 |
@keyframes signal-hot-throb {
|
| 91 |
-
0%, 100% { transform: scale(1.
|
| 92 |
-
50% { transform: scale(1.
|
| 93 |
}
|
| 94 |
|
| 95 |
.signal-dot {
|
| 96 |
width: 10px;
|
| 97 |
height: 10px;
|
| 98 |
-
background-color: #
|
| 99 |
border-radius: 50%;
|
| 100 |
-
box-shadow:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
}
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
.signal-copy {
|
| 104 |
display: flex;
|
| 105 |
flex-direction: column;
|
| 106 |
}
|
| 107 |
|
| 108 |
.signal-copy strong {
|
| 109 |
-
font-size: 0.
|
| 110 |
font-weight: 800;
|
| 111 |
color: #1a1a1a;
|
| 112 |
line-height: 1.1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
}
|
| 114 |
|
| 115 |
.signal-copy span {
|
| 116 |
-
font-size: 0.
|
| 117 |
color: #999;
|
| 118 |
text-transform: uppercase;
|
| 119 |
letter-spacing: 0.08em;
|
| 120 |
font-weight: 600;
|
| 121 |
}
|
| 122 |
|
| 123 |
-
.prefrontal { top:
|
| 124 |
-
.parietal { top:
|
| 125 |
-
.temporal { bottom:
|
|
|
|
| 126 |
|
| 127 |
-
/* ── State Glow Hubs (Subtle Backglow) ── */
|
| 128 |
.glow-hub {
|
| 129 |
position: absolute;
|
| 130 |
-
width:
|
| 131 |
-
height:
|
| 132 |
border-radius: 50%;
|
| 133 |
-
filter: blur(
|
| 134 |
opacity: 0;
|
| 135 |
-
transition:
|
| 136 |
pointer-events: none;
|
| 137 |
z-index: 0;
|
| 138 |
}
|
| 139 |
|
| 140 |
.glow-hub.visible {
|
| 141 |
-
opacity: 0.
|
| 142 |
}
|
| 143 |
|
| 144 |
-
.prefrontal-glow { top:
|
| 145 |
-
.parietal-glow { top:
|
| 146 |
-
.temporal-glow { bottom:
|
| 147 |
-
.thalamus-glow { top:
|
| 148 |
|
| 149 |
/* ── Listening/Responding Shimmer ── */
|
| 150 |
.listening-particles {
|
| 151 |
position: absolute;
|
| 152 |
inset: 0;
|
| 153 |
-
background: radial-gradient(circle, rgba(255, 107, 53, 0.
|
| 154 |
-
animation: brain-shimmer
|
| 155 |
z-index: 5;
|
| 156 |
}
|
| 157 |
|
| 158 |
@keyframes brain-shimmer {
|
| 159 |
-
from { opacity: 0.
|
| 160 |
-
to { opacity: 0.
|
| 161 |
}
|
|
|
|
| 2 |
|
| 3 |
.brain-viewport {
|
| 4 |
width: 100%;
|
| 5 |
+
height: 100%;
|
| 6 |
position: relative;
|
| 7 |
display: flex;
|
| 8 |
align-items: center;
|
| 9 |
justify-content: center;
|
| 10 |
+
perspective: 1100px;
|
| 11 |
}
|
| 12 |
|
| 13 |
.brain-core {
|
| 14 |
position: relative;
|
| 15 |
+
width: 380px;
|
| 16 |
+
height: 380px;
|
| 17 |
z-index: 10;
|
| 18 |
+
transition: transform 0.5s ease;
|
| 19 |
}
|
| 20 |
|
| 21 |
+
/* ── Exclusive Layer Stacking ── */
|
| 22 |
.brain-layer {
|
| 23 |
position: absolute;
|
| 24 |
top: 0;
|
| 25 |
left: 0;
|
| 26 |
width: 100%;
|
| 27 |
+
height: 100%;
|
| 28 |
+
object-fit: contain;
|
| 29 |
+
opacity: 0;
|
| 30 |
+
transform: scale(0.98);
|
| 31 |
+
transition:
|
| 32 |
+
opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
|
| 33 |
+
transform 0.8s cubic-bezier(0.16, 1, 0.3, 1),
|
| 34 |
+
filter 0.6s ease;
|
| 35 |
+
pointer-events: none;
|
| 36 |
+
filter: contrast(1.1);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.brain-layer.active {
|
| 40 |
+
opacity: 1;
|
| 41 |
+
transform: scale(1);
|
| 42 |
}
|
| 43 |
|
| 44 |
+
/* Ensure only ONE layer is ever visible (The Swap) */
|
| 45 |
+
.brain-layer.base.active {
|
| 46 |
z-index: 1;
|
|
|
|
| 47 |
}
|
| 48 |
|
| 49 |
+
.brain-layer.region.active {
|
| 50 |
z-index: 2;
|
| 51 |
+
opacity: 1 !important;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* If a region is active, hide the base entirely to avoid overlapping/ghosting */
|
| 55 |
+
.brain-viewport:has(.brain-layer.region.active) .brain-layer.base {
|
| 56 |
+
opacity: 0 !important;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.brain-layer.base.active {
|
| 60 |
+
/* Clean slate for multiply blend */
|
| 61 |
}
|
| 62 |
|
| 63 |
.brain-layer.region.active {
|
| 64 |
+
/* Clean slate for multiply blend */
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
/* ── Layer 1: Ambient Motion (Breathing) ── */
|
| 68 |
.ambient-breath {
|
| 69 |
position: absolute;
|
| 70 |
inset: 0;
|
| 71 |
+
background: radial-gradient(circle, rgba(255, 107, 53, 0.04) 0%, transparent 75%);
|
| 72 |
animation: breathing 8s infinite ease-in-out;
|
| 73 |
pointer-events: none;
|
| 74 |
z-index: -1;
|
| 75 |
}
|
| 76 |
|
| 77 |
@keyframes breathing {
|
| 78 |
+
0%, 100% { transform: scale(0.96); opacity: 0.3; }
|
| 79 |
+
50% { transform: scale(1.04); opacity: 0.5; }
|
| 80 |
}
|
| 81 |
|
| 82 |
/* ── Layer 2: Cognitive Signals (Floating Labels) ── */
|
|
|
|
| 93 |
align-items: center;
|
| 94 |
gap: 16px;
|
| 95 |
opacity: 0.2;
|
| 96 |
+
transition: all 0.6s ease;
|
| 97 |
+
filter: grayscale(1) blur(0.5px);
|
| 98 |
}
|
| 99 |
|
| 100 |
.signal-node.active {
|
| 101 |
opacity: 1;
|
| 102 |
filter: grayscale(0) blur(0);
|
| 103 |
+
transform: scale(1.1) translateX(10px);
|
| 104 |
+
transition: all 0.1s ease-out;
|
| 105 |
}
|
| 106 |
|
| 107 |
.signal-node.hot {
|
|
|
|
| 111 |
}
|
| 112 |
|
| 113 |
@keyframes signal-hot-throb {
|
| 114 |
+
0%, 100% { transform: scale(1.1) translateX(10px); filter: brightness(1); }
|
| 115 |
+
50% { transform: scale(1.15) translateX(15px); filter: brightness(1.3); }
|
| 116 |
}
|
| 117 |
|
| 118 |
.signal-dot {
|
| 119 |
width: 10px;
|
| 120 |
height: 10px;
|
| 121 |
+
background-color: #d1d5db;
|
| 122 |
border-radius: 50%;
|
| 123 |
+
box-shadow: none;
|
| 124 |
+
transition: all 0.6s ease;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.signal-node.active .signal-dot {
|
| 128 |
+
transition: all 0.1s ease-out;
|
| 129 |
}
|
| 130 |
|
| 131 |
+
/* ── Active Color Mapping ── */
|
| 132 |
+
.signal-node.active.prefrontal .signal-dot { background-color: #f59e0b; box-shadow: 0 0 15px rgba(245, 158, 11, 0.8); }
|
| 133 |
+
.signal-node.active.parietal .signal-dot { background-color: #ff6b35; box-shadow: 0 0 15px rgba(255, 107, 53, 0.8); }
|
| 134 |
+
.signal-node.active.temporal .signal-dot { background-color: #10b981; box-shadow: 0 0 15px rgba(16, 185, 129, 0.8); }
|
| 135 |
+
.signal-node.active.thalamus-label .signal-dot { background-color: #3b82f6; box-shadow: 0 0 15px rgba(59, 130, 246, 0.8); }
|
| 136 |
+
|
| 137 |
+
.signal-node.active.prefrontal strong { color: #f59e0b; }
|
| 138 |
+
.signal-node.active.parietal strong { color: #ff6b35; }
|
| 139 |
+
.signal-node.active.temporal strong { color: #10b981; }
|
| 140 |
+
.signal-node.active.thalamus-label strong { color: #3b82f6; }
|
| 141 |
+
|
| 142 |
.signal-copy {
|
| 143 |
display: flex;
|
| 144 |
flex-direction: column;
|
| 145 |
}
|
| 146 |
|
| 147 |
.signal-copy strong {
|
| 148 |
+
font-size: 0.7rem;
|
| 149 |
font-weight: 800;
|
| 150 |
color: #1a1a1a;
|
| 151 |
line-height: 1.1;
|
| 152 |
+
transition: color 0.6s ease;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.signal-node.active strong {
|
| 156 |
+
transition: color 0.1s ease-out;
|
| 157 |
}
|
| 158 |
|
| 159 |
.signal-copy span {
|
| 160 |
+
font-size: 0.6rem;
|
| 161 |
color: #999;
|
| 162 |
text-transform: uppercase;
|
| 163 |
letter-spacing: 0.08em;
|
| 164 |
font-weight: 600;
|
| 165 |
}
|
| 166 |
|
| 167 |
+
.prefrontal { top: 5%; left: -60px; }
|
| 168 |
+
.parietal { top: 5%; right: -60px; flex-direction: row-reverse; text-align: right; }
|
| 169 |
+
.temporal { bottom: 10%; left: -60px; }
|
| 170 |
+
.thalamus-label { bottom: 10%; right: -60px; flex-direction: row-reverse; text-align: right; }
|
| 171 |
|
| 172 |
+
/* ── State Glow Hubs (Subtle Ambient Backglow) ── */
|
| 173 |
.glow-hub {
|
| 174 |
position: absolute;
|
| 175 |
+
width: 400px;
|
| 176 |
+
height: 400px;
|
| 177 |
border-radius: 50%;
|
| 178 |
+
filter: blur(100px);
|
| 179 |
opacity: 0;
|
| 180 |
+
transition: opacity 1.2s ease;
|
| 181 |
pointer-events: none;
|
| 182 |
z-index: 0;
|
| 183 |
}
|
| 184 |
|
| 185 |
.glow-hub.visible {
|
| 186 |
+
opacity: 0.25;
|
| 187 |
}
|
| 188 |
|
| 189 |
+
.prefrontal-glow { top: 0%; left: 5%; background: radial-gradient(circle, rgba(255, 107, 53, 0.3) 0%, transparent 70%); }
|
| 190 |
+
.parietal-glow { top: 0%; right: 5%; background: radial-gradient(circle, rgba(59, 130, 246, 0.2) 0%, transparent 70%); }
|
| 191 |
+
.temporal-glow { bottom: 5%; left: 15%; background: radial-gradient(circle, rgba(16, 185, 129, 0.2) 0%, transparent 70%); }
|
| 192 |
+
.thalamus-glow { top: 15%; left: 25%; background: radial-gradient(circle, rgba(245, 158, 11, 0.3) 0%, transparent 70%); }
|
| 193 |
|
| 194 |
/* ── Listening/Responding Shimmer ── */
|
| 195 |
.listening-particles {
|
| 196 |
position: absolute;
|
| 197 |
inset: 0;
|
| 198 |
+
background: radial-gradient(circle, rgba(255, 107, 53, 0.03) 0%, transparent 85%);
|
| 199 |
+
animation: brain-shimmer 3s infinite alternate;
|
| 200 |
z-index: 5;
|
| 201 |
}
|
| 202 |
|
| 203 |
@keyframes brain-shimmer {
|
| 204 |
+
from { opacity: 0.1; transform: scale(1); }
|
| 205 |
+
to { opacity: 0.3; transform: scale(1.05); }
|
| 206 |
}
|
frontend/src/components/CognitiveDashboard.css
CHANGED
|
@@ -1,66 +1,65 @@
|
|
| 1 |
/* CognitiveDashboard.css - Status Hub Refinements */
|
| 2 |
|
| 3 |
.status-layout {
|
| 4 |
-
display: grid;
|
| 5 |
-
grid-template-columns: 1fr 1fr;
|
| 6 |
-
gap: 64px;
|
| 7 |
-
height: 100%;
|
| 8 |
-
align-items: center;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
.status-visual {
|
| 12 |
display: flex;
|
| 13 |
-
|
| 14 |
-
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
-
/* ── Status Cards ── */
|
| 18 |
.status-sidebar {
|
| 19 |
display: flex;
|
| 20 |
flex-direction: column;
|
| 21 |
-
gap:
|
| 22 |
}
|
| 23 |
|
| 24 |
.status-card {
|
| 25 |
-
background
|
| 26 |
-
border-radius:
|
| 27 |
-
padding:
|
| 28 |
-
|
| 29 |
-
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
.status-card h3 {
|
| 33 |
-
font-size: 0.
|
| 34 |
text-transform: uppercase;
|
| 35 |
-
letter-spacing: 0.
|
| 36 |
color: #999;
|
| 37 |
-
margin-bottom:
|
| 38 |
-
font-weight:
|
| 39 |
}
|
| 40 |
|
| 41 |
-
/* ── Waves & Charts ── */
|
| 42 |
.system-chart {
|
| 43 |
-
margin-bottom:
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
height: 100px;
|
| 50 |
}
|
| 51 |
|
| 52 |
.system-chart-svg path {
|
| 53 |
fill: none;
|
| 54 |
stroke: #ff6b35;
|
| 55 |
-
stroke-width: 2
|
| 56 |
stroke-linecap: round;
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
.system-chart-svg path.secondary {
|
| 61 |
stroke: #3b82f6;
|
| 62 |
stroke-width: 1.5;
|
| 63 |
-
opacity: 0.
|
| 64 |
}
|
| 65 |
|
| 66 |
.chart-labels {
|
|
@@ -71,84 +70,87 @@
|
|
| 71 |
}
|
| 72 |
|
| 73 |
.chart-labels span {
|
| 74 |
-
font-
|
|
|
|
| 75 |
color: #bbb;
|
| 76 |
-
font-weight: 500;
|
| 77 |
}
|
| 78 |
|
| 79 |
.current-state {
|
| 80 |
display: flex;
|
| 81 |
justify-content: space-between;
|
| 82 |
align-items: center;
|
| 83 |
-
padding-top:
|
| 84 |
-
|
| 85 |
}
|
| 86 |
|
| 87 |
.current-state span {
|
| 88 |
-
font-size: 0.
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
| 91 |
}
|
| 92 |
|
| 93 |
.current-state strong {
|
| 94 |
-
font-size: 1.
|
| 95 |
color: #ff6b35;
|
| 96 |
-
font-weight:
|
| 97 |
text-transform: capitalize;
|
| 98 |
}
|
| 99 |
|
| 100 |
-
/* ── Metrics ── */
|
| 101 |
.metric-grid {
|
| 102 |
display: grid;
|
| 103 |
grid-template-columns: 1fr 1fr;
|
| 104 |
-
gap:
|
| 105 |
}
|
| 106 |
|
| 107 |
.metric-item {
|
| 108 |
display: flex;
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
background-color: #f9f9f9;
|
| 113 |
border-radius: 20px;
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
.metric-item:hover {
|
| 118 |
-
transform: translateY(-4px);
|
| 119 |
}
|
| 120 |
|
| 121 |
.metric-icon-wrap {
|
| 122 |
-
width:
|
| 123 |
-
height:
|
|
|
|
| 124 |
border-radius: 12px;
|
| 125 |
-
background
|
| 126 |
display: flex;
|
| 127 |
align-items: center;
|
| 128 |
justify-content: center;
|
| 129 |
color: #ff6b35;
|
| 130 |
-
box-shadow: 0 4px 10px rgba(0,0,0,0.03);
|
| 131 |
}
|
| 132 |
|
| 133 |
.metric-icon-wrap .material-icons {
|
| 134 |
-
font-size:
|
| 135 |
}
|
| 136 |
|
| 137 |
.metric-info {
|
| 138 |
display: flex;
|
| 139 |
flex-direction: column;
|
|
|
|
| 140 |
}
|
| 141 |
|
| 142 |
.metric-info label {
|
| 143 |
-
font-size: 0.
|
|
|
|
|
|
|
| 144 |
color: #999;
|
| 145 |
-
font-weight:
|
| 146 |
-
|
| 147 |
}
|
| 148 |
|
| 149 |
-
.metric-
|
| 150 |
-
font-size: 1.
|
| 151 |
color: #1a1a1a;
|
| 152 |
-
font-
|
| 153 |
-
letter-spacing: -0.
|
|
|
|
| 154 |
}
|
|
|
|
| 1 |
/* CognitiveDashboard.css - Status Hub Refinements */
|
| 2 |
|
| 3 |
.status-layout {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
display: flex;
|
| 5 |
+
flex-direction: column;
|
| 6 |
+
gap: 16px;
|
| 7 |
+
width: 100%;
|
| 8 |
}
|
| 9 |
|
| 10 |
+
/* ── Status Cards Redesign ── */
|
| 11 |
.status-sidebar {
|
| 12 |
display: flex;
|
| 13 |
flex-direction: column;
|
| 14 |
+
gap: 16px;
|
| 15 |
}
|
| 16 |
|
| 17 |
.status-card {
|
| 18 |
+
background: rgba(0, 0, 0, 0.02);
|
| 19 |
+
border-radius: 20px;
|
| 20 |
+
padding: 20px;
|
| 21 |
+
border: 1px solid rgba(0, 0, 0, 0.03);
|
| 22 |
+
position: relative;
|
| 23 |
+
overflow: hidden;
|
| 24 |
}
|
| 25 |
|
| 26 |
.status-card h3 {
|
| 27 |
+
font-size: 0.6rem;
|
| 28 |
text-transform: uppercase;
|
| 29 |
+
letter-spacing: 0.15em;
|
| 30 |
color: #999;
|
| 31 |
+
margin-bottom: 12px;
|
| 32 |
+
font-weight: 800;
|
| 33 |
}
|
| 34 |
|
| 35 |
+
/* ── Waves & HUD Charts ── */
|
| 36 |
.system-chart {
|
| 37 |
+
margin-bottom: 16px;
|
| 38 |
+
padding: 12px 16px;
|
| 39 |
+
background: white;
|
| 40 |
+
border-radius: 12px;
|
| 41 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.02);
|
| 42 |
+
border: 1px solid rgba(0,0,0,0.02);
|
|
|
|
| 43 |
}
|
| 44 |
|
| 45 |
.system-chart-svg path {
|
| 46 |
fill: none;
|
| 47 |
stroke: #ff6b35;
|
| 48 |
+
stroke-width: 2;
|
| 49 |
stroke-linecap: round;
|
| 50 |
+
filter: drop-shadow(0 0 4px rgba(255, 107, 53, 0.2));
|
| 51 |
+
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.intensity-high .system-chart-svg path {
|
| 55 |
+
stroke-width: 4;
|
| 56 |
+
stroke: #ff3d00;
|
| 57 |
}
|
| 58 |
|
| 59 |
.system-chart-svg path.secondary {
|
| 60 |
stroke: #3b82f6;
|
| 61 |
stroke-width: 1.5;
|
| 62 |
+
opacity: 0.15;
|
| 63 |
}
|
| 64 |
|
| 65 |
.chart-labels {
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
.chart-labels span {
|
| 73 |
+
font-family: 'Roboto Mono', monospace;
|
| 74 |
+
font-size: 0.55rem;
|
| 75 |
color: #bbb;
|
|
|
|
| 76 |
}
|
| 77 |
|
| 78 |
.current-state {
|
| 79 |
display: flex;
|
| 80 |
justify-content: space-between;
|
| 81 |
align-items: center;
|
| 82 |
+
padding-top: 12px;
|
| 83 |
+
gap: 32px;
|
| 84 |
}
|
| 85 |
|
| 86 |
.current-state span {
|
| 87 |
+
font-size: 0.65rem;
|
| 88 |
+
text-transform: uppercase;
|
| 89 |
+
letter-spacing: 0.15em;
|
| 90 |
+
color: #999;
|
| 91 |
+
font-weight: 700;
|
| 92 |
}
|
| 93 |
|
| 94 |
.current-state strong {
|
| 95 |
+
font-size: 1.6rem;
|
| 96 |
color: #ff6b35;
|
| 97 |
+
font-weight: 800;
|
| 98 |
text-transform: capitalize;
|
| 99 |
}
|
| 100 |
|
| 101 |
+
/* ── Metrics Grid ── */
|
| 102 |
.metric-grid {
|
| 103 |
display: grid;
|
| 104 |
grid-template-columns: 1fr 1fr;
|
| 105 |
+
gap: 16px;
|
| 106 |
}
|
| 107 |
|
| 108 |
.metric-item {
|
| 109 |
display: flex;
|
| 110 |
+
align-items: center;
|
| 111 |
+
padding: 20px 24px;
|
| 112 |
+
background: white;
|
|
|
|
| 113 |
border-radius: 20px;
|
| 114 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.02);
|
| 115 |
+
border: 1px solid rgba(0,0,0,0.03);
|
| 116 |
+
gap: 16px;
|
|
|
|
|
|
|
| 117 |
}
|
| 118 |
|
| 119 |
.metric-icon-wrap {
|
| 120 |
+
width: 42px;
|
| 121 |
+
height: 42px;
|
| 122 |
+
min-width: 42px;
|
| 123 |
border-radius: 12px;
|
| 124 |
+
background: rgba(255, 107, 53, 0.05);
|
| 125 |
display: flex;
|
| 126 |
align-items: center;
|
| 127 |
justify-content: center;
|
| 128 |
color: #ff6b35;
|
|
|
|
| 129 |
}
|
| 130 |
|
| 131 |
.metric-icon-wrap .material-icons {
|
| 132 |
+
font-size: 22px;
|
| 133 |
}
|
| 134 |
|
| 135 |
.metric-info {
|
| 136 |
display: flex;
|
| 137 |
flex-direction: column;
|
| 138 |
+
flex: 1;
|
| 139 |
}
|
| 140 |
|
| 141 |
.metric-info label {
|
| 142 |
+
font-size: 0.6rem;
|
| 143 |
+
text-transform: uppercase;
|
| 144 |
+
letter-spacing: 0.1em;
|
| 145 |
color: #999;
|
| 146 |
+
font-weight: 700;
|
| 147 |
+
white-space: nowrap;
|
| 148 |
}
|
| 149 |
|
| 150 |
+
.metric-item strong {
|
| 151 |
+
font-size: 1.5rem;
|
| 152 |
color: #1a1a1a;
|
| 153 |
+
font-family: 'Roboto Mono', monospace;
|
| 154 |
+
letter-spacing: -0.05em;
|
| 155 |
+
margin-left: auto;
|
| 156 |
}
|
frontend/src/components/CognitiveDashboard.jsx
CHANGED
|
@@ -1,19 +1,15 @@
|
|
| 1 |
import './CognitiveDashboard.css';
|
| 2 |
-
import
|
| 3 |
|
| 4 |
function CognitiveDashboard({ statusText, stats }) {
|
| 5 |
return (
|
| 6 |
<div className="status-layout fade-in">
|
| 7 |
-
<div className="status-visual">
|
| 8 |
-
<CognitiveBrainImageScene state="reasoning" />
|
| 9 |
-
</div>
|
| 10 |
-
|
| 11 |
<div className="status-sidebar">
|
| 12 |
-
<div className="status-card">
|
| 13 |
<h3>Neural Vitals</h3>
|
| 14 |
-
<SystemStatusChart />
|
| 15 |
<div className="current-state">
|
| 16 |
-
<span>
|
| 17 |
<strong>{statusText}</strong>
|
| 18 |
</div>
|
| 19 |
</div>
|
|
@@ -39,17 +35,62 @@ function CognitiveDashboard({ statusText, stats }) {
|
|
| 39 |
);
|
| 40 |
}
|
| 41 |
|
| 42 |
-
function SystemStatusChart() {
|
| 43 |
-
|
| 44 |
-
const
|
| 45 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
const labels = ['ALPHA', 'BETA', 'GAMMA', 'DELTA', 'THETA'];
|
| 47 |
|
| 48 |
return (
|
| 49 |
-
<div className=
|
| 50 |
<svg viewBox="0 0 300 80" className="system-chart-svg">
|
| 51 |
-
<path d={
|
| 52 |
-
<path d={
|
| 53 |
</svg>
|
| 54 |
<div className="chart-labels">
|
| 55 |
{labels.map(l => <span key={l}>{l}</span>)}
|
|
|
|
| 1 |
import './CognitiveDashboard.css';
|
| 2 |
+
import { useState, useEffect, useRef } from 'react';
|
| 3 |
|
| 4 |
function CognitiveDashboard({ statusText, stats }) {
|
| 5 |
return (
|
| 6 |
<div className="status-layout fade-in">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
<div className="status-sidebar">
|
| 8 |
+
<div className="status-card highlight">
|
| 9 |
<h3>Neural Vitals</h3>
|
| 10 |
+
<SystemStatusChart state={statusText} />
|
| 11 |
<div className="current-state">
|
| 12 |
+
<span>Neural Frequency</span>
|
| 13 |
<strong>{statusText}</strong>
|
| 14 |
</div>
|
| 15 |
</div>
|
|
|
|
| 35 |
);
|
| 36 |
}
|
| 37 |
|
| 38 |
+
function SystemStatusChart({ state }) {
|
| 39 |
+
const [path1, setPath1] = useState("");
|
| 40 |
+
const [path2, setPath2] = useState("");
|
| 41 |
+
const frameRef = useRef(0);
|
| 42 |
+
|
| 43 |
+
const intensity = ['reasoning', 'language', 'reflection'].includes(state) ? 'high' :
|
| 44 |
+
['recall', 'association', 'attention'].includes(state) ? 'medium' : 'low';
|
| 45 |
+
|
| 46 |
+
useEffect(() => {
|
| 47 |
+
let animationFrame;
|
| 48 |
+
|
| 49 |
+
const generateWave = (time, freq, amp, jitter, complexity = 1) => {
|
| 50 |
+
let d = "M0,40 ";
|
| 51 |
+
const step = complexity > 1 ? 5 : 10;
|
| 52 |
+
for (let x = 0; x <= 300; x += step) {
|
| 53 |
+
// Complex brainwave math: Base Sine + Harmonics for interference
|
| 54 |
+
let yBase = Math.sin(x * freq + time) * amp;
|
| 55 |
+
if (complexity > 1) {
|
| 56 |
+
yBase += Math.sin(x * freq * 2.3 + time * 1.4) * (amp * 0.4);
|
| 57 |
+
yBase += Math.sin(x * freq * 3.9 + time * 2.1) * (amp * 0.2);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
const noise = (Math.random() - 0.5) * jitter;
|
| 61 |
+
const y = 40 + yBase + noise;
|
| 62 |
+
d += `L${x},${y} `;
|
| 63 |
+
}
|
| 64 |
+
return d;
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
const animate = () => {
|
| 68 |
+
frameRef.current += intensity === 'high' ? 0.28 : intensity === 'medium' ? 0.14 : 0.06;
|
| 69 |
+
const t = frameRef.current;
|
| 70 |
+
|
| 71 |
+
const config = {
|
| 72 |
+
high: { freq: 0.12, amp: 26, jitter: 14, comp: 3 },
|
| 73 |
+
medium: { freq: 0.08, amp: 18, jitter: 4, comp: 1 },
|
| 74 |
+
low: { freq: 0.03, amp: 10, jitter: 1, comp: 1 }
|
| 75 |
+
}[intensity];
|
| 76 |
+
|
| 77 |
+
setPath1(generateWave(t, config.freq, config.amp, config.jitter, config.comp));
|
| 78 |
+
setPath2(generateWave(t * 0.8, config.freq * 0.6, config.amp * 0.5, config.jitter * 0.4, 1));
|
| 79 |
+
|
| 80 |
+
animationFrame = requestAnimationFrame(animate);
|
| 81 |
+
};
|
| 82 |
+
|
| 83 |
+
animate();
|
| 84 |
+
return () => cancelAnimationFrame(animationFrame);
|
| 85 |
+
}, [intensity]);
|
| 86 |
+
|
| 87 |
const labels = ['ALPHA', 'BETA', 'GAMMA', 'DELTA', 'THETA'];
|
| 88 |
|
| 89 |
return (
|
| 90 |
+
<div className={`system-chart intensity-${intensity}`}>
|
| 91 |
<svg viewBox="0 0 300 80" className="system-chart-svg">
|
| 92 |
+
<path d={path2} className="secondary" />
|
| 93 |
+
<path d={path1} />
|
| 94 |
</svg>
|
| 95 |
<div className="chart-labels">
|
| 96 |
{labels.map(l => <span key={l}>{l}</span>)}
|
frontend/src/components/CognitiveTimeline.css
CHANGED
|
@@ -3,8 +3,8 @@
|
|
| 3 |
.timeline-container {
|
| 4 |
display: flex;
|
| 5 |
flex-direction: column;
|
| 6 |
-
gap:
|
| 7 |
-
padding:
|
| 8 |
}
|
| 9 |
|
| 10 |
.timeline-row {
|
|
@@ -19,7 +19,7 @@
|
|
| 19 |
position: absolute;
|
| 20 |
left: 65px; /* Alignment with dot */
|
| 21 |
top: 12px;
|
| 22 |
-
bottom: -
|
| 23 |
width: 1px;
|
| 24 |
background-color: #eee;
|
| 25 |
z-index: 1;
|
|
@@ -31,7 +31,7 @@
|
|
| 31 |
|
| 32 |
.timeline-time {
|
| 33 |
width: 55px;
|
| 34 |
-
font-size: 0.
|
| 35 |
color: #bbb;
|
| 36 |
font-family: var(--font-mono);
|
| 37 |
padding-top: 4px;
|
|
@@ -57,11 +57,7 @@
|
|
| 57 |
|
| 58 |
.timeline-content {
|
| 59 |
flex: 1;
|
| 60 |
-
|
| 61 |
-
padding: 20px 24px;
|
| 62 |
-
border-radius: 20px;
|
| 63 |
-
box-shadow: 0 4px 12px rgba(0,0,0,0.02);
|
| 64 |
-
border: 1px solid rgba(0,0,0,0.03);
|
| 65 |
transition: transform 0.3s ease;
|
| 66 |
}
|
| 67 |
|
|
@@ -89,14 +85,14 @@
|
|
| 89 |
}
|
| 90 |
|
| 91 |
.phase-title {
|
| 92 |
-
font-size: 0.
|
| 93 |
font-weight: 700;
|
| 94 |
color: #1a1a1a;
|
| 95 |
letter-spacing: -0.01em;
|
| 96 |
}
|
| 97 |
|
| 98 |
.phase-desc {
|
| 99 |
-
font-size: 0.
|
| 100 |
color: #666;
|
| 101 |
line-height: 1.5;
|
| 102 |
}
|
|
|
|
| 3 |
.timeline-container {
|
| 4 |
display: flex;
|
| 5 |
flex-direction: column;
|
| 6 |
+
gap: 16px;
|
| 7 |
+
padding: 0;
|
| 8 |
}
|
| 9 |
|
| 10 |
.timeline-row {
|
|
|
|
| 19 |
position: absolute;
|
| 20 |
left: 65px; /* Alignment with dot */
|
| 21 |
top: 12px;
|
| 22 |
+
bottom: -24px;
|
| 23 |
width: 1px;
|
| 24 |
background-color: #eee;
|
| 25 |
z-index: 1;
|
|
|
|
| 31 |
|
| 32 |
.timeline-time {
|
| 33 |
width: 55px;
|
| 34 |
+
font-size: 0.65rem;
|
| 35 |
color: #bbb;
|
| 36 |
font-family: var(--font-mono);
|
| 37 |
padding-top: 4px;
|
|
|
|
| 57 |
|
| 58 |
.timeline-content {
|
| 59 |
flex: 1;
|
| 60 |
+
padding: 0 0 16px 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
transition: transform 0.3s ease;
|
| 62 |
}
|
| 63 |
|
|
|
|
| 85 |
}
|
| 86 |
|
| 87 |
.phase-title {
|
| 88 |
+
font-size: 0.7rem;
|
| 89 |
font-weight: 700;
|
| 90 |
color: #1a1a1a;
|
| 91 |
letter-spacing: -0.01em;
|
| 92 |
}
|
| 93 |
|
| 94 |
.phase-desc {
|
| 95 |
+
font-size: 0.8rem;
|
| 96 |
color: #666;
|
| 97 |
line-height: 1.5;
|
| 98 |
}
|
frontend/src/components/CognitiveTimeline.jsx
CHANGED
|
@@ -3,23 +3,50 @@ import './CognitiveTimeline.css';
|
|
| 3 |
const PHASE_CONFIG = {
|
| 4 |
perception: { icon: 'visibility', color: '#ff6b35', label: 'Perception' },
|
| 5 |
attention: { icon: 'track_changes', color: '#3b82f6', label: 'Attention' },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
recall: { icon: 'psychology', color: '#10b981', label: 'Recall' },
|
|
|
|
|
|
|
| 7 |
reasoning: { icon: 'hub', color: '#ff6b35', label: 'Reasoning' },
|
| 8 |
-
|
|
|
|
|
|
|
| 9 |
};
|
| 10 |
|
| 11 |
function CognitiveTimeline({ trace }) {
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
return (
|
| 14 |
<div className="timeline-empty">
|
| 15 |
-
|
|
|
|
| 16 |
</div>
|
| 17 |
);
|
| 18 |
}
|
| 19 |
|
| 20 |
return (
|
| 21 |
<div className="timeline-container fade-in">
|
| 22 |
-
{
|
| 23 |
const config = PHASE_CONFIG[item.phase.toLowerCase()] || { icon: 'circle', color: '#ccc', label: item.phase };
|
| 24 |
|
| 25 |
return (
|
|
@@ -36,7 +63,7 @@ function CognitiveTimeline({ trace }) {
|
|
| 36 |
<div className="phase-title">{config.label}</div>
|
| 37 |
</div>
|
| 38 |
<div className="phase-desc">
|
| 39 |
-
{item.content || item.desc || 'Processing information...'}
|
| 40 |
</div>
|
| 41 |
</div>
|
| 42 |
</div>
|
|
|
|
| 3 |
const PHASE_CONFIG = {
|
| 4 |
perception: { icon: 'visibility', color: '#ff6b35', label: 'Perception' },
|
| 5 |
attention: { icon: 'track_changes', color: '#3b82f6', label: 'Attention' },
|
| 6 |
+
emotion: { icon: 'favorite', color: '#e11d48', label: 'Emotion' },
|
| 7 |
+
routing: { icon: 'route', color: '#f59e0b', label: 'Routing' },
|
| 8 |
+
prediction: { icon: 'online_prediction', color: '#8b5cf6', label: 'Prediction' },
|
| 9 |
+
working_memory: { icon: 'memory', color: '#06b6d4', label: 'Working Memory' },
|
| 10 |
+
reflection: { icon: 'lightbulb', color: '#f59e0b', label: 'Reflection' },
|
| 11 |
recall: { icon: 'psychology', color: '#10b981', label: 'Recall' },
|
| 12 |
+
inhibition: { icon: 'block', color: '#ef4444', label: 'Inhibition' },
|
| 13 |
+
association: { icon: 'device_hub', color: '#3b82f6', label: 'Association' },
|
| 14 |
reasoning: { icon: 'hub', color: '#ff6b35', label: 'Reasoning' },
|
| 15 |
+
language: { icon: 'chat_bubble', color: '#8ab892', label: 'Language' },
|
| 16 |
+
memory: { icon: 'save', color: '#10b981', label: 'Consolidation' },
|
| 17 |
+
graph: { icon: 'share', color: '#3b82f6', label: 'Graph Update' },
|
| 18 |
};
|
| 19 |
|
| 20 |
function CognitiveTimeline({ trace }) {
|
| 21 |
+
const rawTrace = trace
|
| 22 |
+
.filter(item => item.phase)
|
| 23 |
+
.reverse(); // Chronological order
|
| 24 |
+
|
| 25 |
+
const consolidatedTrace = [];
|
| 26 |
+
let currentPhase = null;
|
| 27 |
+
|
| 28 |
+
rawTrace.forEach(item => {
|
| 29 |
+
if (currentPhase && currentPhase.phase === item.phase) {
|
| 30 |
+
currentPhase.message = item.message || item.content || item.desc;
|
| 31 |
+
currentPhase.time = item.time;
|
| 32 |
+
} else {
|
| 33 |
+
currentPhase = { ...item };
|
| 34 |
+
consolidatedTrace.push(currentPhase);
|
| 35 |
+
}
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
if (!consolidatedTrace || consolidatedTrace.length === 0) {
|
| 39 |
return (
|
| 40 |
<div className="timeline-empty">
|
| 41 |
+
<span className="material-icons">sensors</span>
|
| 42 |
+
<p>Awaiting sensory stimuli...</p>
|
| 43 |
</div>
|
| 44 |
);
|
| 45 |
}
|
| 46 |
|
| 47 |
return (
|
| 48 |
<div className="timeline-container fade-in">
|
| 49 |
+
{consolidatedTrace.map((item, index) => {
|
| 50 |
const config = PHASE_CONFIG[item.phase.toLowerCase()] || { icon: 'circle', color: '#ccc', label: item.phase };
|
| 51 |
|
| 52 |
return (
|
|
|
|
| 63 |
<div className="phase-title">{config.label}</div>
|
| 64 |
</div>
|
| 65 |
<div className="phase-desc">
|
| 66 |
+
{item.message || item.content || item.desc || 'Processing information...'}
|
| 67 |
</div>
|
| 68 |
</div>
|
| 69 |
</div>
|
frontend/src/components/DreamSequence.css
CHANGED
|
@@ -1,220 +1,515 @@
|
|
| 1 |
-
/*
|
| 2 |
|
| 3 |
-
.sleep-
|
| 4 |
display: flex;
|
| 5 |
flex-direction: column;
|
| 6 |
align-items: center;
|
| 7 |
justify-content: center;
|
|
|
|
|
|
|
|
|
|
| 8 |
height: 100%;
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
| 10 |
}
|
| 11 |
|
| 12 |
-
.sleep-
|
| 13 |
-
position:
|
| 14 |
-
|
| 15 |
-
|
| 16 |
}
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
font-
|
| 21 |
-
font-size: 2.2rem;
|
| 22 |
font-weight: 700;
|
| 23 |
-
margin-bottom: 12px;
|
| 24 |
color: #1a1a1a;
|
|
|
|
| 25 |
letter-spacing: -0.01em;
|
| 26 |
}
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
-
.sleep-
|
|
|
|
| 35 |
display: flex;
|
| 36 |
-
gap: 80px;
|
| 37 |
justify-content: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
width: 100%;
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
}
|
| 43 |
|
| 44 |
-
.sleep-step {
|
| 45 |
display: flex;
|
| 46 |
align-items: center;
|
| 47 |
gap: 16px;
|
|
|
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
-
.sleep-step-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
border-radius: 50%;
|
| 54 |
-
border: 2px solid #ddd;
|
| 55 |
display: flex;
|
| 56 |
align-items: center;
|
| 57 |
justify-content: center;
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
transition: all 0.3s;
|
| 62 |
}
|
| 63 |
|
| 64 |
-
.
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
}
|
| 69 |
|
| 70 |
-
.sleep-step-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
background-color: transparent;
|
| 74 |
}
|
| 75 |
|
| 76 |
-
.
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
font-weight: 600;
|
| 80 |
}
|
| 81 |
|
| 82 |
-
.sleep-step-
|
| 83 |
color: #1a1a1a;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
}
|
| 85 |
|
| 86 |
-
/* ── Summary
|
| 87 |
-
.summary-overlay {
|
| 88 |
position: absolute;
|
| 89 |
top: 0;
|
| 90 |
left: 0;
|
| 91 |
right: 0;
|
| 92 |
bottom: 0;
|
| 93 |
-
background-color:
|
| 94 |
display: flex;
|
|
|
|
| 95 |
align-items: center;
|
| 96 |
justify-content: center;
|
| 97 |
z-index: 100;
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
}
|
| 100 |
|
| 101 |
-
.summary-card
|
|
|
|
| 102 |
width: 100%;
|
| 103 |
-
max-width:
|
| 104 |
-
background-color:
|
| 105 |
-
border-radius:
|
| 106 |
-
padding:
|
| 107 |
-
box-shadow: 0
|
| 108 |
position: relative;
|
| 109 |
text-align: left;
|
| 110 |
-
border: 1px solid rgba(0,0,0,0.02);
|
|
|
|
| 111 |
}
|
| 112 |
|
| 113 |
-
.summary-close {
|
| 114 |
position: absolute;
|
| 115 |
-
top:
|
| 116 |
-
right:
|
| 117 |
-
color: #
|
| 118 |
-
font-size: 1.8rem;
|
| 119 |
background: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
}
|
| 121 |
|
| 122 |
-
.summary-close
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
color: #1a1a1a;
|
| 124 |
}
|
| 125 |
|
| 126 |
-
.summary-header {
|
| 127 |
display: flex;
|
| 128 |
-
gap:
|
| 129 |
-
margin-bottom:
|
|
|
|
| 130 |
}
|
| 131 |
|
| 132 |
-
.summary-header-icon {
|
| 133 |
-
width:
|
| 134 |
-
height:
|
| 135 |
-
background-color:
|
| 136 |
-
border-radius:
|
| 137 |
display: flex;
|
| 138 |
align-items: center;
|
| 139 |
justify-content: center;
|
| 140 |
-
color: #
|
| 141 |
}
|
| 142 |
|
| 143 |
-
.summary-header-icon .material-icons {
|
| 144 |
-
font-size:
|
| 145 |
}
|
| 146 |
|
| 147 |
-
.summary-header-copy h3 {
|
| 148 |
-
font-family: var(--font-display);
|
| 149 |
-
font-size: 1.
|
| 150 |
font-weight: 700;
|
| 151 |
-
margin-bottom: 4px;
|
| 152 |
color: #1a1a1a;
|
|
|
|
|
|
|
| 153 |
}
|
| 154 |
|
| 155 |
-
.summary-header-copy p {
|
| 156 |
-
font-size:
|
| 157 |
-
color: #
|
|
|
|
|
|
|
| 158 |
}
|
| 159 |
|
| 160 |
-
.summary-list {
|
| 161 |
display: flex;
|
| 162 |
flex-direction: column;
|
| 163 |
-
gap:
|
| 164 |
-
margin-bottom:
|
| 165 |
}
|
| 166 |
|
| 167 |
-
.summary-item {
|
| 168 |
display: flex;
|
| 169 |
align-items: center;
|
| 170 |
-
gap:
|
| 171 |
-
padding:
|
| 172 |
-
border-radius:
|
| 173 |
-
background-color:
|
| 174 |
-
border: 1px solid rgba(0,0,0,0.
|
| 175 |
}
|
| 176 |
|
| 177 |
-
.summary-item-icon {
|
| 178 |
-
width:
|
| 179 |
-
height:
|
| 180 |
-
border-radius:
|
| 181 |
display: flex;
|
| 182 |
align-items: center;
|
| 183 |
justify-content: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
}
|
| 185 |
|
| 186 |
-
.summary-item-copy strong {
|
| 187 |
display: block;
|
| 188 |
-
font-size:
|
| 189 |
font-weight: 700;
|
| 190 |
color: #1a1a1a;
|
| 191 |
-
margin-bottom:
|
| 192 |
}
|
| 193 |
|
| 194 |
-
.summary-item-copy span {
|
| 195 |
-
|
| 196 |
-
|
|
|
|
| 197 |
font-weight: 500;
|
| 198 |
}
|
| 199 |
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
width: 100%;
|
| 202 |
-
height:
|
| 203 |
background-color: #fff1eb;
|
| 204 |
color: #ff6b35;
|
| 205 |
-
border-radius:
|
|
|
|
| 206 |
font-weight: 700;
|
| 207 |
-
font-size:
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
}
|
| 210 |
|
| 211 |
-
.summary-view-btn:hover {
|
| 212 |
background-color: #ffe4d9;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
transform: translateY(-2px);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
}
|
| 215 |
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ── Sleep & Consolidation Page Styles (Split Layout) ── */
|
| 2 |
|
| 3 |
+
.sleep-page-container {
|
| 4 |
display: flex;
|
| 5 |
flex-direction: column;
|
| 6 |
align-items: center;
|
| 7 |
justify-content: center;
|
| 8 |
+
min-height: 600px;
|
| 9 |
+
background-color: #eaeaea; /* Exact light gray background as in Image 1 */
|
| 10 |
+
width: 100%;
|
| 11 |
height: 100%;
|
| 12 |
+
padding: 40px;
|
| 13 |
+
box-sizing: border-box;
|
| 14 |
+
position: relative;
|
| 15 |
+
border-radius: 24px;
|
| 16 |
}
|
| 17 |
|
| 18 |
+
.sleep-page-header {
|
| 19 |
+
position: absolute;
|
| 20 |
+
top: 40px;
|
| 21 |
+
left: 40px;
|
| 22 |
}
|
| 23 |
|
| 24 |
+
.sleep-page-header h2 {
|
| 25 |
+
font-family: var(--font-display), 'Inter', sans-serif;
|
| 26 |
+
font-size: 1.2rem; /* Exact page header font-size as Console */
|
|
|
|
| 27 |
font-weight: 700;
|
|
|
|
| 28 |
color: #1a1a1a;
|
| 29 |
+
margin: 0;
|
| 30 |
letter-spacing: -0.01em;
|
| 31 |
}
|
| 32 |
|
| 33 |
+
/* ── Split Layout Grid ── */
|
| 34 |
+
.sleep-split-layout {
|
| 35 |
+
display: grid;
|
| 36 |
+
grid-template-columns: 1.1fr 1fr;
|
| 37 |
+
gap: 60px;
|
| 38 |
+
width: 100%;
|
| 39 |
+
max-width: 960px;
|
| 40 |
+
align-items: center;
|
| 41 |
+
justify-content: center;
|
| 42 |
+
margin-top: 40px;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.sleep-left-panel {
|
| 46 |
+
display: flex;
|
| 47 |
+
justify-content: center;
|
| 48 |
+
align-items: center;
|
| 49 |
}
|
| 50 |
|
| 51 |
+
.sleep-brain-wrap-combined {
|
| 52 |
+
position: relative;
|
| 53 |
display: flex;
|
|
|
|
| 54 |
justify-content: center;
|
| 55 |
+
align-items: center;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.sleep-brain-image-asset {
|
| 59 |
+
max-width: 380px;
|
| 60 |
+
height: auto;
|
| 61 |
+
display: block;
|
| 62 |
+
mix-blend-mode: normal; /* Blends perfectly because background colors match exactly */
|
| 63 |
+
opacity: 0.95;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.sleep-right-panel {
|
| 67 |
+
display: flex;
|
| 68 |
+
flex-direction: column;
|
| 69 |
+
align-items: flex-start;
|
| 70 |
+
text-align: left;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.sleep-copy-combined {
|
| 74 |
+
text-align: left;
|
| 75 |
+
margin-bottom: 40px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.sleep-copy-combined h3 {
|
| 79 |
+
font-family: var(--font-display), 'Inter', sans-serif;
|
| 80 |
+
font-size: 1.15rem; /* Exact block header font-size as Console */
|
| 81 |
+
font-weight: 700;
|
| 82 |
+
color: #1a1a1a;
|
| 83 |
+
margin: 0 0 8px 0;
|
| 84 |
+
letter-spacing: -0.01em;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.sleep-copy-combined p {
|
| 88 |
+
font-size: 0.75rem; /* Exact body font-size as Console */
|
| 89 |
+
color: #666;
|
| 90 |
+
margin: 0;
|
| 91 |
+
font-weight: 500;
|
| 92 |
+
line-height: 1.45;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/* ── Vertical Progress Steps (Optimized for Split Panel) ── */
|
| 96 |
+
.sleep-steps-vertical {
|
| 97 |
+
display: flex;
|
| 98 |
+
flex-direction: column;
|
| 99 |
width: 100%;
|
| 100 |
+
gap: 28px;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.sleep-step-item-wrap-vertical {
|
| 104 |
+
position: relative;
|
| 105 |
+
display: flex;
|
| 106 |
+
flex-direction: column;
|
| 107 |
}
|
| 108 |
|
| 109 |
+
.sleep-step-item-vertical {
|
| 110 |
display: flex;
|
| 111 |
align-items: center;
|
| 112 |
gap: 16px;
|
| 113 |
+
opacity: 0.5;
|
| 114 |
+
transition: opacity 0.3s ease;
|
| 115 |
+
z-index: 2;
|
| 116 |
}
|
| 117 |
|
| 118 |
+
.sleep-step-item-vertical.active {
|
| 119 |
+
opacity: 1;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.sleep-step-circle {
|
| 123 |
+
width: 24px;
|
| 124 |
+
height: 24px;
|
| 125 |
border-radius: 50%;
|
|
|
|
| 126 |
display: flex;
|
| 127 |
align-items: center;
|
| 128 |
justify-content: center;
|
| 129 |
+
box-sizing: border-box;
|
| 130 |
+
background-color: #eaeaea; /* matches parent background to blend connector lines */
|
| 131 |
+
transition: all 0.3s ease;
|
|
|
|
| 132 |
}
|
| 133 |
|
| 134 |
+
/* 1. Completed step (Green Checkmark) */
|
| 135 |
+
.sleep-step-circle.completed {
|
| 136 |
+
border: 2px solid #10b981;
|
| 137 |
+
color: #10b981;
|
| 138 |
}
|
| 139 |
|
| 140 |
+
.sleep-step-circle.completed .material-icons {
|
| 141 |
+
font-size: 16px;
|
| 142 |
+
font-weight: 900;
|
|
|
|
| 143 |
}
|
| 144 |
|
| 145 |
+
/* 2. Active step (Orange Ring with Dot) */
|
| 146 |
+
.sleep-step-circle.active {
|
| 147 |
+
border: 2px solid #ff6b35;
|
| 148 |
+
position: relative;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.active-dot-inner {
|
| 152 |
+
width: 8px;
|
| 153 |
+
height: 8px;
|
| 154 |
+
background-color: #ff6b35;
|
| 155 |
+
border-radius: 50%;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
/* 3. Pending step (Grey Ring) */
|
| 159 |
+
.sleep-step-circle.pending {
|
| 160 |
+
border: 2px solid #cbd5e1;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.sleep-step-text {
|
| 164 |
+
font-family: var(--font-display), 'Inter', sans-serif;
|
| 165 |
+
font-size: 0.8rem; /* Exact list font-size as Console activity items */
|
| 166 |
+
color: #444;
|
| 167 |
font-weight: 600;
|
| 168 |
}
|
| 169 |
|
| 170 |
+
.sleep-step-item-vertical.active .sleep-step-text {
|
| 171 |
color: #1a1a1a;
|
| 172 |
+
font-weight: 700;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/* Vertical connector lines */
|
| 176 |
+
.sleep-step-connector-vertical {
|
| 177 |
+
position: absolute;
|
| 178 |
+
left: 11px;
|
| 179 |
+
top: -28px;
|
| 180 |
+
height: 28px;
|
| 181 |
+
width: 2px;
|
| 182 |
+
z-index: 1;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.connector-line {
|
| 186 |
+
width: 100%;
|
| 187 |
+
height: 100%;
|
| 188 |
+
background-color: #cbd5e1;
|
| 189 |
}
|
| 190 |
|
| 191 |
+
/* ── Consolidation Summary Overlay (Image 2) ── */
|
| 192 |
+
.summary-overlay-combined {
|
| 193 |
position: absolute;
|
| 194 |
top: 0;
|
| 195 |
left: 0;
|
| 196 |
right: 0;
|
| 197 |
bottom: 0;
|
| 198 |
+
background-color: #eaeaea; /* Exact solid color to overlay completely as shown in Image 2 */
|
| 199 |
display: flex;
|
| 200 |
+
flex-direction: column;
|
| 201 |
align-items: center;
|
| 202 |
justify-content: center;
|
| 203 |
z-index: 100;
|
| 204 |
+
padding: 40px;
|
| 205 |
+
box-sizing: border-box;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.sleep-page-header-summary {
|
| 209 |
+
position: absolute;
|
| 210 |
+
top: 40px;
|
| 211 |
+
left: 40px;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.sleep-page-header-summary h2 {
|
| 215 |
+
font-family: var(--font-display), 'Inter', sans-serif;
|
| 216 |
+
font-size: 1.2rem; /* Matches unified page header size */
|
| 217 |
+
font-weight: 700;
|
| 218 |
+
color: #1a1a1a;
|
| 219 |
+
margin: 0;
|
| 220 |
}
|
| 221 |
|
| 222 |
+
.summary-card-combined,
|
| 223 |
+
.summary-card-inline {
|
| 224 |
width: 100%;
|
| 225 |
+
max-width: 380px; /* Slimmer card width for better alignment */
|
| 226 |
+
background-color: #f3f3f3;
|
| 227 |
+
border-radius: 24px;
|
| 228 |
+
padding: 22px; /* Denser, high-fidelity margins */
|
| 229 |
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.05);
|
| 230 |
position: relative;
|
| 231 |
text-align: left;
|
| 232 |
+
border: 1px solid rgba(0, 0, 0, 0.02);
|
| 233 |
+
box-sizing: border-box;
|
| 234 |
}
|
| 235 |
|
| 236 |
+
.summary-close-combined {
|
| 237 |
position: absolute;
|
| 238 |
+
top: 24px;
|
| 239 |
+
right: 24px;
|
| 240 |
+
color: #888;
|
|
|
|
| 241 |
background: none;
|
| 242 |
+
border: none;
|
| 243 |
+
cursor: pointer;
|
| 244 |
+
display: flex;
|
| 245 |
+
align-items: center;
|
| 246 |
+
justify-content: center;
|
| 247 |
}
|
| 248 |
|
| 249 |
+
.summary-close-combined .material-icons {
|
| 250 |
+
font-size: 20px;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.summary-close-combined:hover {
|
| 254 |
color: #1a1a1a;
|
| 255 |
}
|
| 256 |
|
| 257 |
+
.summary-header-combined {
|
| 258 |
display: flex;
|
| 259 |
+
gap: 12px;
|
| 260 |
+
margin-bottom: 18px;
|
| 261 |
+
align-items: center;
|
| 262 |
}
|
| 263 |
|
| 264 |
+
.summary-header-icon-combined {
|
| 265 |
+
width: 36px;
|
| 266 |
+
height: 36px;
|
| 267 |
+
background-color: transparent;
|
| 268 |
+
border-radius: 50%;
|
| 269 |
display: flex;
|
| 270 |
align-items: center;
|
| 271 |
justify-content: center;
|
| 272 |
+
color: #475569;
|
| 273 |
}
|
| 274 |
|
| 275 |
+
.summary-header-icon-combined .material-icons {
|
| 276 |
+
font-size: 24px;
|
| 277 |
}
|
| 278 |
|
| 279 |
+
.summary-header-copy-combined h3 {
|
| 280 |
+
font-family: var(--font-display), 'Inter', sans-serif;
|
| 281 |
+
font-size: 1.05rem; /* Balanced, high-density title */
|
| 282 |
font-weight: 700;
|
|
|
|
| 283 |
color: #1a1a1a;
|
| 284 |
+
margin: 0 0 2px 0;
|
| 285 |
+
letter-spacing: -0.01em;
|
| 286 |
}
|
| 287 |
|
| 288 |
+
.summary-header-copy-combined p {
|
| 289 |
+
font-size: 0.72rem; /* Matches unified small captions */
|
| 290 |
+
color: #666;
|
| 291 |
+
margin: 0;
|
| 292 |
+
font-weight: 500;
|
| 293 |
}
|
| 294 |
|
| 295 |
+
.summary-list-combined {
|
| 296 |
display: flex;
|
| 297 |
flex-direction: column;
|
| 298 |
+
gap: 12px;
|
| 299 |
+
margin-bottom: 28px;
|
| 300 |
}
|
| 301 |
|
| 302 |
+
.summary-item-combined {
|
| 303 |
display: flex;
|
| 304 |
align-items: center;
|
| 305 |
+
gap: 12px; /* Denser item gap */
|
| 306 |
+
padding: 10px 14px; /* Highly compact padding */
|
| 307 |
+
border-radius: 14px;
|
| 308 |
+
background-color: rgba(0, 0, 0, 0.025);
|
| 309 |
+
border: 1px solid rgba(0, 0, 0, 0.01);
|
| 310 |
}
|
| 311 |
|
| 312 |
+
.summary-item-icon-combined {
|
| 313 |
+
width: 30px;
|
| 314 |
+
height: 30px;
|
| 315 |
+
border-radius: 10px;
|
| 316 |
display: flex;
|
| 317 |
align-items: center;
|
| 318 |
justify-content: center;
|
| 319 |
+
flex-shrink: 0;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.summary-item-icon-combined .material-icons {
|
| 323 |
+
font-size: 16px; /* Compact action icons */
|
| 324 |
}
|
| 325 |
|
| 326 |
+
.summary-item-copy-combined strong {
|
| 327 |
display: block;
|
| 328 |
+
font-size: 0.78rem; /* Matches premium compact metadata headers */
|
| 329 |
font-weight: 700;
|
| 330 |
color: #1a1a1a;
|
| 331 |
+
margin-bottom: 1px;
|
| 332 |
}
|
| 333 |
|
| 334 |
+
.summary-item-copy-combined span {
|
| 335 |
+
display: block;
|
| 336 |
+
font-size: 0.68rem; /* Highly readable compact value subtext */
|
| 337 |
+
color: #666;
|
| 338 |
font-weight: 500;
|
| 339 |
}
|
| 340 |
|
| 341 |
+
/* Tone styling matching pastel buttons */
|
| 342 |
+
.summary-item-icon-combined.green { color: #10b981; background-color: rgba(16, 185, 129, 0.08); }
|
| 343 |
+
.summary-item-icon-combined.teal { color: #0d9488; background-color: rgba(13, 148, 136, 0.08); }
|
| 344 |
+
.summary-item-icon-combined.orange { color: #ff6b35; background-color: rgba(255, 107, 53, 0.08); }
|
| 345 |
+
.summary-item-icon-combined.amber { color: #f59e0b; background-color: rgba(245, 158, 11, 0.08); }
|
| 346 |
+
|
| 347 |
+
.summary-view-btn-combined {
|
| 348 |
width: 100%;
|
| 349 |
+
height: 40px; /* Slim, premium button height */
|
| 350 |
background-color: #fff1eb;
|
| 351 |
color: #ff6b35;
|
| 352 |
+
border-radius: 14px;
|
| 353 |
+
border: none;
|
| 354 |
font-weight: 700;
|
| 355 |
+
font-size: 0.8rem; /* Compact label size matching Console buttons */
|
| 356 |
+
cursor: pointer;
|
| 357 |
+
transition: all 0.2s ease;
|
| 358 |
+
display: flex;
|
| 359 |
+
align-items: center;
|
| 360 |
+
justify-content: center;
|
| 361 |
}
|
| 362 |
|
| 363 |
+
.summary-view-btn-combined:hover {
|
| 364 |
background-color: #ffe4d9;
|
| 365 |
+
transform: translateY(-1px);
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
/* ── Sleep Trigger Landing State Styles ── */
|
| 369 |
+
.sleep-landing-combined {
|
| 370 |
+
display: flex;
|
| 371 |
+
flex-direction: column;
|
| 372 |
+
align-items: flex-start;
|
| 373 |
+
width: 100%;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.sleep-trigger-btn {
|
| 377 |
+
display: flex;
|
| 378 |
+
align-items: center;
|
| 379 |
+
justify-content: center;
|
| 380 |
+
gap: 10px;
|
| 381 |
+
background: linear-gradient(135deg, #ff6b35, #ff5714);
|
| 382 |
+
color: white;
|
| 383 |
+
border: none;
|
| 384 |
+
padding: 12px 30px; /* Highly compact padding */
|
| 385 |
+
border-radius: 16px;
|
| 386 |
+
font-family: var(--font-display), 'Inter', sans-serif;
|
| 387 |
+
font-size: 0.8rem; /* Exact button text size as Console inputs */
|
| 388 |
+
font-weight: 700;
|
| 389 |
+
cursor: pointer;
|
| 390 |
+
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
| 391 |
+
box-shadow: 0 10px 30px rgba(255, 107, 53, 0.25);
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.sleep-trigger-btn:hover {
|
| 395 |
transform: translateY(-2px);
|
| 396 |
+
box-shadow: 0 15px 40px rgba(255, 107, 53, 0.4);
|
| 397 |
+
background: linear-gradient(135deg, #ff5714, #e04400);
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.sleep-trigger-btn:active {
|
| 401 |
+
transform: translateY(0);
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
.sleep-trigger-btn .material-icons {
|
| 405 |
+
font-size: 22px;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
/* ── Sleep Telemetry Metric Dashboard Styles ── */
|
| 409 |
+
.sleep-telemetry-grid {
|
| 410 |
+
display: flex;
|
| 411 |
+
width: 100%;
|
| 412 |
+
max-width: 900px;
|
| 413 |
+
gap: 24px;
|
| 414 |
+
margin-top: 40px;
|
| 415 |
+
animation: fadeInUp 0.8s ease-out;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.sleep-telemetry-card {
|
| 419 |
+
flex: 1;
|
| 420 |
+
background-color: rgba(255, 255, 255, 0.45);
|
| 421 |
+
border: 1px solid rgba(0, 0, 0, 0.04);
|
| 422 |
+
border-radius: 20px;
|
| 423 |
+
padding: 20px;
|
| 424 |
+
text-align: left;
|
| 425 |
+
box-sizing: border-box;
|
| 426 |
+
transition: all 0.3s ease;
|
| 427 |
+
display: flex;
|
| 428 |
+
flex-direction: column;
|
| 429 |
+
justify-content: space-between;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
.sleep-telemetry-card:hover {
|
| 433 |
+
transform: translateY(-3px);
|
| 434 |
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.02);
|
| 435 |
+
background-color: rgba(255, 255, 255, 0.65);
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
.telemetry-card-header {
|
| 439 |
+
display: flex;
|
| 440 |
+
justify-content: space-between;
|
| 441 |
+
align-items: center;
|
| 442 |
+
margin-bottom: 12px;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
.telemetry-card-header h4 {
|
| 446 |
+
margin: 0;
|
| 447 |
+
font-size: 0.72rem;
|
| 448 |
+
text-transform: uppercase;
|
| 449 |
+
letter-spacing: 0.08em;
|
| 450 |
+
color: #666;
|
| 451 |
+
font-weight: 700;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.telemetry-card-header .material-icons {
|
| 455 |
+
font-size: 16px;
|
| 456 |
+
color: #999;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.telemetry-card-value {
|
| 460 |
+
font-size: 1.15rem; /* Compact instrument value font-size */
|
| 461 |
+
font-weight: 700;
|
| 462 |
+
color: #1a1a1a;
|
| 463 |
+
margin-bottom: 6px;
|
| 464 |
+
display: flex;
|
| 465 |
+
align-items: center;
|
| 466 |
+
gap: 8px;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
.telemetry-card-subtext {
|
| 470 |
+
font-size: 0.68rem; /* Highly compact detail text */
|
| 471 |
+
color: #888;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
/* Adenosine Progress Bar */
|
| 475 |
+
.adenosine-progress-container {
|
| 476 |
+
width: 100%;
|
| 477 |
+
height: 6px;
|
| 478 |
+
background-color: rgba(0, 0, 0, 0.05);
|
| 479 |
+
border-radius: 3px;
|
| 480 |
+
overflow: hidden;
|
| 481 |
+
margin-top: 8px;
|
| 482 |
}
|
| 483 |
|
| 484 |
+
.adenosine-progress-bar {
|
| 485 |
+
height: 100%;
|
| 486 |
+
border-radius: 3px;
|
| 487 |
+
transition: width 1s ease-in-out;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
/* Status Badge */
|
| 491 |
+
.telemetry-status-badge {
|
| 492 |
+
padding: 4px 10px;
|
| 493 |
+
border-radius: 12px;
|
| 494 |
+
font-size: 0.7rem;
|
| 495 |
+
font-weight: 700;
|
| 496 |
+
letter-spacing: 0.05em;
|
| 497 |
+
display: inline-block;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
.telemetry-status-badge.optimal {
|
| 501 |
+
background-color: #e3f9e5;
|
| 502 |
+
color: #1e7e34;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
.telemetry-status-badge.ready {
|
| 506 |
+
background-color: #fff3cd;
|
| 507 |
+
color: #856404;
|
| 508 |
+
animation: pulseOrange 2s infinite;
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
@keyframes pulseOrange {
|
| 512 |
+
0% { opacity: 0.8; }
|
| 513 |
+
50% { opacity: 1; transform: scale(1.02); }
|
| 514 |
+
100% { opacity: 0.8; }
|
| 515 |
+
}
|
frontend/src/components/DreamSequence.jsx
CHANGED
|
@@ -1,76 +1,178 @@
|
|
| 1 |
-
import
|
| 2 |
import './DreamSequence.css';
|
| 3 |
|
| 4 |
-
export function SleepProgress({ phaseIndex }) {
|
| 5 |
const steps = ['Analyzing Memories', 'Linking Concepts', 'Pruning Redundancies'];
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
return (
|
| 8 |
-
<div className="sleep-
|
| 9 |
-
<div className="sleep-
|
| 10 |
-
<
|
| 11 |
-
</div>
|
| 12 |
-
<div className="sleep-copy">
|
| 13 |
-
<h3>Soma is consolidating memories...</h3>
|
| 14 |
-
<p>Cleaning, linking and strengthening knowledge.</p>
|
| 15 |
</div>
|
| 16 |
-
<div className="sleep-steps">
|
| 17 |
-
{steps.map((step, index) => {
|
| 18 |
-
const completed = index < phaseIndex;
|
| 19 |
-
const active = index === phaseIndex;
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
</div>
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
</div>
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
{ label: 'Consolidated', value: `${data.consolidated} memories merged`, icon: 'inventory_2', tone: 'teal' },
|
| 40 |
-
{ label: 'Pruned', value: `${data.pruned} redundant items removed`, icon: 'filter_alt', tone: 'orange' },
|
| 41 |
-
{ label: 'Strengthened', value: `${data.strengthened} memory traces updated`, icon: 'auto_awesome', tone: 'amber' },
|
| 42 |
-
];
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
</div>
|
| 53 |
-
<div className="
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
</div>
|
| 57 |
</div>
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
| 71 |
</div>
|
| 72 |
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
</div>
|
| 75 |
</div>
|
| 76 |
);
|
|
|
|
| 1 |
+
import sleepImg from '../assets/brain/sleep_nobg.png';
|
| 2 |
import './DreamSequence.css';
|
| 3 |
|
| 4 |
+
export function SleepProgress({ phaseIndex, isConsolidating, onStart, summary, vitals, onClose }) {
|
| 5 |
const steps = ['Analyzing Memories', 'Linking Concepts', 'Pruning Redundancies'];
|
| 6 |
|
| 7 |
+
// Parse message count in working memory
|
| 8 |
+
const workingCount = parseInt(vitals?.working || 0);
|
| 9 |
+
const adenosineScore = Math.min(workingCount * 12.5, 100);
|
| 10 |
+
const isOptimal = workingCount >= 4;
|
| 11 |
+
|
| 12 |
return (
|
| 13 |
+
<div className="sleep-page-container fade-in">
|
| 14 |
+
<div className="sleep-page-header">
|
| 15 |
+
<h2>7. Sleep (Consolidation)</h2>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
<div className="sleep-split-layout">
|
| 19 |
+
{/* Left Column: Brain Image (ALWAYS VISIBLE!) */}
|
| 20 |
+
<div className="sleep-left-panel">
|
| 21 |
+
<div className="sleep-brain-wrap-combined">
|
| 22 |
+
<img src={sleepImg} className="sleep-brain-image-asset" alt="Soma Sleeping Brain" />
|
| 23 |
+
</div>
|
| 24 |
+
</div>
|
| 25 |
+
|
| 26 |
+
{/* Right Column: Dynamic State Panel */}
|
| 27 |
+
<div className="sleep-right-panel">
|
| 28 |
+
{summary ? (
|
| 29 |
+
/* State 1: Consolidation Summary Card (Directly inline on the right side!) */
|
| 30 |
+
<div className="summary-card-inline fade-in">
|
| 31 |
+
<button className="summary-close-combined" onClick={onClose}>
|
| 32 |
+
<span className="material-icons">close</span>
|
| 33 |
+
</button>
|
| 34 |
+
|
| 35 |
+
<div className="summary-header-combined">
|
| 36 |
+
<div className="summary-header-icon-combined">
|
| 37 |
+
<span className="material-icons">bedtime</span>
|
| 38 |
+
</div>
|
| 39 |
+
<div className="summary-header-copy-combined">
|
| 40 |
+
<h3>Sleep Cycle Complete</h3>
|
| 41 |
+
<p>Memory consolidation finished</p>
|
| 42 |
+
</div>
|
| 43 |
</div>
|
| 44 |
+
|
| 45 |
+
<div className="summary-list-combined">
|
| 46 |
+
{[
|
| 47 |
+
{ label: 'Linked Together', value: `${summary.linked} connections created`, icon: 'hub', tone: 'green' },
|
| 48 |
+
{ label: 'Consolidated', value: `${summary.consolidated} summaries saved`, icon: 'inventory_2', tone: 'teal' },
|
| 49 |
+
{ label: 'Pruned', value: `${summary.pruned} raw logs pruned`, icon: 'filter_alt', tone: 'orange' },
|
| 50 |
+
].map((item) => (
|
| 51 |
+
<div key={item.label} className="summary-item-combined">
|
| 52 |
+
<div className={`summary-item-icon-combined ${item.tone}`}>
|
| 53 |
+
<span className="material-icons">{item.icon}</span>
|
| 54 |
+
</div>
|
| 55 |
+
<div className="summary-item-copy-combined">
|
| 56 |
+
<strong>{item.label}</strong>
|
| 57 |
+
<span>{item.value}</span>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
))}
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<button className="summary-view-btn-combined" onClick={onClose}>Return to Console</button>
|
| 64 |
</div>
|
| 65 |
+
) : isConsolidating ? (
|
| 66 |
+
/* State 2: Active Consolidation Checklist Steps */
|
| 67 |
+
<div className="sleep-progress-combined fade-in">
|
| 68 |
+
<div className="sleep-copy-combined">
|
| 69 |
+
<h3>Soma is consolidating memories...</h3>
|
| 70 |
+
<p>Cleaning, linking and strengthening knowledge.</p>
|
| 71 |
+
</div>
|
| 72 |
|
| 73 |
+
<div className="sleep-steps-vertical">
|
| 74 |
+
{steps.map((step, index) => {
|
| 75 |
+
const completed = index < phaseIndex;
|
| 76 |
+
const active = index === phaseIndex;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
+
return (
|
| 79 |
+
<div key={step} className="sleep-step-item-wrap-vertical">
|
| 80 |
+
{index > 0 && (
|
| 81 |
+
<div className="sleep-step-connector-vertical">
|
| 82 |
+
<div className="connector-line" />
|
| 83 |
+
</div>
|
| 84 |
+
)}
|
| 85 |
+
<div className={`sleep-step-item-vertical ${active ? 'active' : ''}`}>
|
| 86 |
+
<div className={`sleep-step-circle ${completed ? 'completed' : active ? 'active' : 'pending'}`}>
|
| 87 |
+
{completed && <span className="material-icons">check</span>}
|
| 88 |
+
{active && <div className="active-dot-inner" />}
|
| 89 |
+
</div>
|
| 90 |
+
<span className="sleep-step-text">{step}</span>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
);
|
| 94 |
+
})}
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
) : (
|
| 98 |
+
/* State 3: Landing / Idle State with Sleep button */
|
| 99 |
+
<div className="sleep-landing-combined fade-in">
|
| 100 |
+
<div className="sleep-copy-combined">
|
| 101 |
+
<h3>Reorganize Neural Structures</h3>
|
| 102 |
+
<p>
|
| 103 |
+
Consolidate recent transactional chat logs into the ChromaDB sensory database and extract semantic facts to the Neo4j Knowledge Graph.
|
| 104 |
+
</p>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<button className="sleep-trigger-btn" onClick={onStart}>
|
| 108 |
+
<span className="material-icons">bedtime</span>
|
| 109 |
+
<span>Sleep</span>
|
| 110 |
+
</button>
|
| 111 |
+
</div>
|
| 112 |
+
)}
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
{/* Dynamic Telemetry Metric Dashboard */}
|
| 117 |
+
<div className="sleep-telemetry-grid">
|
| 118 |
+
{/* Card 1: Sleep Pressure / Adenosine */}
|
| 119 |
+
<div className="sleep-telemetry-card">
|
| 120 |
+
<div className="telemetry-card-header">
|
| 121 |
+
<h4>Sleep Pressure (Adenosine)</h4>
|
| 122 |
+
<span className="material-icons">hourglass_empty</span>
|
| 123 |
</div>
|
| 124 |
+
<div className="telemetry-card-value">
|
| 125 |
+
{adenosineScore.toFixed(0)}%
|
| 126 |
+
</div>
|
| 127 |
+
<div className="telemetry-card-subtext">
|
| 128 |
+
{isOptimal ? 'Sleep pressure high. Consolidation critical.' : 'Accumulating data. Low neural pressure.'}
|
| 129 |
+
</div>
|
| 130 |
+
<div className="adenosine-progress-container">
|
| 131 |
+
<div
|
| 132 |
+
className="adenosine-progress-bar"
|
| 133 |
+
style={{
|
| 134 |
+
width: `${adenosineScore}%`,
|
| 135 |
+
background: isOptimal ? 'linear-gradient(90deg, #ff8c00, #ff4500)' : 'linear-gradient(90deg, #3b82f6, #60a5fa)'
|
| 136 |
+
}}
|
| 137 |
+
/>
|
| 138 |
</div>
|
| 139 |
</div>
|
| 140 |
|
| 141 |
+
{/* Card 2: Synaptic Queue Density */}
|
| 142 |
+
<div className="sleep-telemetry-card">
|
| 143 |
+
<div className="telemetry-card-header">
|
| 144 |
+
<h4>Working Synapses</h4>
|
| 145 |
+
<span className="material-icons">psychology</span>
|
| 146 |
+
</div>
|
| 147 |
+
<div className="telemetry-card-value">
|
| 148 |
+
{workingCount} <span style={{ fontSize: '0.9rem', fontWeight: 500, color: '#666', marginLeft: '4px' }}>exchanges</span>
|
| 149 |
+
</div>
|
| 150 |
+
<div className="telemetry-card-subtext">
|
| 151 |
+
{workingCount > 0
|
| 152 |
+
? `${workingCount} raw dialogic traces stored in pre-consolidated state.`
|
| 153 |
+
: 'Working memory fully cleared. No traces queued.'}
|
| 154 |
+
</div>
|
| 155 |
</div>
|
| 156 |
|
| 157 |
+
{/* Card 3: Consolidation Readiness */}
|
| 158 |
+
<div className="sleep-telemetry-card">
|
| 159 |
+
<div className="telemetry-card-header">
|
| 160 |
+
<h4>System Readiness</h4>
|
| 161 |
+
<span className="material-icons">bolt</span>
|
| 162 |
+
</div>
|
| 163 |
+
<div className="telemetry-card-value">
|
| 164 |
+
{isOptimal ? (
|
| 165 |
+
<span className="telemetry-status-badge ready">READY TO SLEEP</span>
|
| 166 |
+
) : (
|
| 167 |
+
<span className="telemetry-status-badge optimal" style={{ background: '#dbeafe', color: '#1e40af' }}>ACCUMULATING</span>
|
| 168 |
+
)}
|
| 169 |
+
</div>
|
| 170 |
+
<div className="telemetry-card-subtext">
|
| 171 |
+
{isOptimal
|
| 172 |
+
? 'Meets minimum requirement (≥ 4 messages) for factual extraction.'
|
| 173 |
+
: 'Needs at least 4 chat messages in working memory to trigger.'}
|
| 174 |
+
</div>
|
| 175 |
+
</div>
|
| 176 |
</div>
|
| 177 |
</div>
|
| 178 |
);
|
frontend/src/components/KnowledgeGraph.css
CHANGED
|
@@ -1,183 +1,390 @@
|
|
| 1 |
-
/* KnowledgeGraph.css -
|
| 2 |
|
| 3 |
.graph-stage {
|
| 4 |
display: flex;
|
| 5 |
flex-direction: column;
|
| 6 |
height: 100%;
|
| 7 |
position: relative;
|
|
|
|
|
|
|
|
|
|
| 8 |
}
|
| 9 |
|
| 10 |
.graph-toolbar {
|
| 11 |
display: flex;
|
| 12 |
justify-content: space-between;
|
| 13 |
-
|
|
|
|
| 14 |
}
|
| 15 |
|
| 16 |
-
.graph-
|
| 17 |
-
background-color: #eee;
|
| 18 |
-
border-radius: 18px;
|
| 19 |
-
padding: 0 20px;
|
| 20 |
-
height: 56px;
|
| 21 |
display: flex;
|
| 22 |
align-items: center;
|
| 23 |
gap: 12px;
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
color: #1a1a1a;
|
| 26 |
-
font-weight:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
}
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
.graph-actions {
|
| 30 |
display: flex;
|
| 31 |
-
gap:
|
| 32 |
}
|
| 33 |
|
| 34 |
.graph-icon-button {
|
| 35 |
-
width:
|
| 36 |
-
height:
|
| 37 |
-
background
|
| 38 |
-
|
|
|
|
|
|
|
| 39 |
display: flex;
|
| 40 |
align-items: center;
|
| 41 |
justify-content: center;
|
| 42 |
-
color: #
|
| 43 |
transition: all 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
.graph-icon-button:hover {
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
| 49 |
}
|
| 50 |
|
| 51 |
-
/* ── Network ── */
|
| 52 |
-
.graph-network {
|
| 53 |
flex: 1;
|
| 54 |
position: relative;
|
| 55 |
-
|
| 56 |
-
align-items: center;
|
| 57 |
-
justify-content: center;
|
| 58 |
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
-
.
|
| 62 |
position: absolute;
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
z-index: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
pointer-events: none;
|
| 67 |
}
|
| 68 |
|
| 69 |
-
.graph-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
|
| 75 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
position: absolute;
|
|
|
|
|
|
|
|
|
|
| 77 |
display: flex;
|
| 78 |
flex-direction: column;
|
| 79 |
align-items: center;
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
-
.graph-
|
| 86 |
-
|
| 87 |
-
top: 50%;
|
| 88 |
}
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
| 95 |
display: flex;
|
| 96 |
-
|
| 97 |
justify-content: center;
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
| 103 |
}
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
}
|
| 109 |
|
| 110 |
-
.
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
border-radius: 50%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
display: flex;
|
| 116 |
align-items: center;
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
}
|
| 122 |
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
50% { transform: translateY(-10px); }
|
| 126 |
}
|
| 127 |
|
| 128 |
-
.
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
font-size: 0.85rem;
|
| 134 |
-
color: #1a1a1a;
|
| 135 |
-
font-weight: 700;
|
| 136 |
-
letter-spacing: -0.01em;
|
| 137 |
}
|
| 138 |
|
| 139 |
-
.
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
border-radius: 50%;
|
| 143 |
-
background-color: #ff6b35;
|
| 144 |
-
box-shadow: 0 0 12px rgba(255, 107, 53, 0.3);
|
| 145 |
}
|
| 146 |
|
| 147 |
-
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
display: flex;
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
margin-top: 48px;
|
| 153 |
-
background-color: white;
|
| 154 |
-
padding: 16px 32px;
|
| 155 |
-
border-radius: 99px;
|
| 156 |
-
box-shadow: 0 4px 15px rgba(0,0,0,0.02);
|
| 157 |
-
width: fit-content;
|
| 158 |
-
align-self: center;
|
| 159 |
}
|
| 160 |
|
| 161 |
-
.
|
| 162 |
display: flex;
|
| 163 |
align-items: center;
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
| 167 |
font-weight: 700;
|
| 168 |
-
|
| 169 |
-
letter-spacing: 0.05em;
|
| 170 |
}
|
| 171 |
|
| 172 |
-
.
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
}
|
| 178 |
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* KnowledgeGraph.css - Premium Sci-Fi 3D Semantic Mesh styling */
|
| 2 |
|
| 3 |
.graph-stage {
|
| 4 |
display: flex;
|
| 5 |
flex-direction: column;
|
| 6 |
height: 100%;
|
| 7 |
position: relative;
|
| 8 |
+
gap: 10px;
|
| 9 |
+
padding: 0 16px 16px 16px;
|
| 10 |
+
overflow: hidden;
|
| 11 |
}
|
| 12 |
|
| 13 |
.graph-toolbar {
|
| 14 |
display: flex;
|
| 15 |
justify-content: space-between;
|
| 16 |
+
align-items: center;
|
| 17 |
+
gap: 12px;
|
| 18 |
}
|
| 19 |
|
| 20 |
+
.graph-title-block {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
display: flex;
|
| 22 |
align-items: center;
|
| 23 |
gap: 12px;
|
| 24 |
+
flex-wrap: wrap;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.graph-select {
|
| 28 |
+
background: rgba(255, 255, 255, 0.6);
|
| 29 |
+
backdrop-filter: blur(20px);
|
| 30 |
+
border: 1px solid rgba(255, 255, 255, 0.8);
|
| 31 |
+
border-radius: 100px;
|
| 32 |
+
padding: 0 14px;
|
| 33 |
+
height: 32px;
|
| 34 |
+
display: flex;
|
| 35 |
+
align-items: center;
|
| 36 |
+
gap: 6px;
|
| 37 |
+
font-size: 0.75rem;
|
| 38 |
color: #1a1a1a;
|
| 39 |
+
font-weight: 700;
|
| 40 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.02);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.graph-select .material-icons {
|
| 44 |
+
color: #06b6d4;
|
| 45 |
+
font-size: 16px;
|
| 46 |
}
|
| 47 |
|
| 48 |
+
/* DB Status Badges */
|
| 49 |
+
.db-status-badge {
|
| 50 |
+
display: inline-flex;
|
| 51 |
+
align-items: center;
|
| 52 |
+
gap: 8px;
|
| 53 |
+
padding: 6px 14px;
|
| 54 |
+
border-radius: 100px;
|
| 55 |
+
font-size: 0.7rem;
|
| 56 |
+
font-weight: 700;
|
| 57 |
+
text-transform: uppercase;
|
| 58 |
+
letter-spacing: 0.05em;
|
| 59 |
+
border: 1px solid transparent;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.db-status-badge.warning {
|
| 63 |
+
background: rgba(245, 158, 11, 0.08);
|
| 64 |
+
color: #d97706;
|
| 65 |
+
border-color: rgba(245, 158, 11, 0.2);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.db-status-badge.success {
|
| 69 |
+
background: rgba(16, 185, 129, 0.08);
|
| 70 |
+
color: #059669;
|
| 71 |
+
border-color: rgba(16, 185, 129, 0.2);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.db-status-badge .dot {
|
| 75 |
+
width: 6px;
|
| 76 |
+
height: 6px;
|
| 77 |
+
border-radius: 50%;
|
| 78 |
+
background-color: currentColor;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/* Actions */
|
| 82 |
.graph-actions {
|
| 83 |
display: flex;
|
| 84 |
+
gap: 10px;
|
| 85 |
}
|
| 86 |
|
| 87 |
.graph-icon-button {
|
| 88 |
+
width: 32px;
|
| 89 |
+
height: 32px;
|
| 90 |
+
background: rgba(255, 255, 255, 0.7);
|
| 91 |
+
backdrop-filter: blur(20px);
|
| 92 |
+
border: 1px solid rgba(255, 255, 255, 0.9);
|
| 93 |
+
border-radius: 10px;
|
| 94 |
display: flex;
|
| 95 |
align-items: center;
|
| 96 |
justify-content: center;
|
| 97 |
+
color: #555;
|
| 98 |
transition: all 0.2s;
|
| 99 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.02);
|
| 100 |
+
cursor: pointer;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.graph-icon-button .material-icons {
|
| 104 |
+
font-size: 16px;
|
| 105 |
}
|
| 106 |
|
| 107 |
.graph-icon-button:hover {
|
| 108 |
+
transform: translateY(-1px);
|
| 109 |
+
background: white;
|
| 110 |
+
color: #4f46e5;
|
| 111 |
+
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15);
|
| 112 |
}
|
| 113 |
|
| 114 |
+
/* ── Network Container ── */
|
| 115 |
+
.graph-network-container {
|
| 116 |
flex: 1;
|
| 117 |
position: relative;
|
| 118 |
+
border-radius: 24px;
|
|
|
|
|
|
|
| 119 |
overflow: hidden;
|
| 120 |
+
box-shadow: var(--shadow-md), inset 0 0 80px rgba(255, 255, 255, 0.3);
|
| 121 |
+
border: 1px solid rgba(255, 255, 255, 0.8);
|
| 122 |
+
background: rgba(255, 255, 255, 0.35);
|
| 123 |
+
backdrop-filter: blur(25px);
|
| 124 |
+
padding-left: 160px; /* Shifted slightly left compared to before to balance the canvas center */
|
| 125 |
+
box-sizing: border-box;
|
| 126 |
}
|
| 127 |
|
| 128 |
+
.brain-silhouette {
|
| 129 |
position: absolute;
|
| 130 |
+
top: 50%;
|
| 131 |
+
left: 50%;
|
| 132 |
+
transform: translate(-50%, -50%);
|
| 133 |
+
width: min(85vw, 550px);
|
| 134 |
+
height: min(85vw, 550px);
|
| 135 |
+
pointer-events: none;
|
| 136 |
z-index: 0;
|
| 137 |
+
display: flex;
|
| 138 |
+
justify-content: center;
|
| 139 |
+
align-items: center;
|
| 140 |
+
opacity: 0.85;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/* Tooltip micro-HUD */
|
| 144 |
+
.graph-tooltip {
|
| 145 |
+
background: rgba(255, 255, 255, 0.96) !important;
|
| 146 |
+
border: 1px solid rgba(99, 102, 241, 0.2) !important;
|
| 147 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05) !important;
|
| 148 |
+
border-radius: 10px !important;
|
| 149 |
+
padding: 10px 14px !important;
|
| 150 |
+
color: #1e293b !important;
|
| 151 |
+
font-family: var(--font-mono), monospace !important;
|
| 152 |
+
font-size: 0.75rem !important;
|
| 153 |
pointer-events: none;
|
| 154 |
}
|
| 155 |
|
| 156 |
+
.graph-tooltip strong {
|
| 157 |
+
display: block;
|
| 158 |
+
font-size: 0.8rem;
|
| 159 |
+
color: #4f46e5;
|
| 160 |
+
margin-bottom: 6px;
|
| 161 |
+
border-bottom: 1px solid rgba(0,0,0,0.06);
|
| 162 |
+
padding-bottom: 4px;
|
| 163 |
}
|
| 164 |
|
| 165 |
+
.tooltip-sub {
|
| 166 |
+
color: #64748b;
|
| 167 |
+
margin-top: 2px;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/* Loading HUD overlay */
|
| 171 |
+
.graph-loading {
|
| 172 |
position: absolute;
|
| 173 |
+
top: 50%;
|
| 174 |
+
left: 50%;
|
| 175 |
+
transform: translate(-50%, -50%);
|
| 176 |
display: flex;
|
| 177 |
flex-direction: column;
|
| 178 |
align-items: center;
|
| 179 |
+
gap: 16px;
|
| 180 |
+
color: #4f46e5;
|
| 181 |
+
font-family: var(--font-mono), monospace;
|
| 182 |
+
font-size: 0.8rem;
|
| 183 |
+
text-transform: uppercase;
|
| 184 |
+
letter-spacing: 0.1em;
|
| 185 |
+
z-index: 10;
|
| 186 |
+
pointer-events: none;
|
| 187 |
+
background: rgba(255, 255, 255, 0.85);
|
| 188 |
+
padding: 24px 36px;
|
| 189 |
+
border-radius: 16px;
|
| 190 |
+
border: 1px solid rgba(99, 102, 241, 0.2);
|
| 191 |
+
backdrop-filter: blur(10px);
|
| 192 |
}
|
| 193 |
|
| 194 |
+
.graph-loading .material-icons {
|
| 195 |
+
font-size: 32px;
|
|
|
|
| 196 |
}
|
| 197 |
|
| 198 |
+
/* Legend - Floating Absolutely in the 3D Container */
|
| 199 |
+
.graph-legend {
|
| 200 |
+
position: absolute;
|
| 201 |
+
bottom: 16px;
|
| 202 |
+
left: calc(50% + 80px); /* Re-centered perfectly relative to the 160px shift */
|
| 203 |
+
transform: translateX(-50%);
|
| 204 |
display: flex;
|
| 205 |
+
gap: 20px;
|
| 206 |
justify-content: center;
|
| 207 |
+
background: rgba(255, 255, 255, 0.75);
|
| 208 |
+
backdrop-filter: blur(25px);
|
| 209 |
+
border: 1px solid rgba(255, 255, 255, 0.8);
|
| 210 |
+
padding: 8px 24px;
|
| 211 |
+
border-radius: 100px;
|
| 212 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.03);
|
| 213 |
+
z-index: 100;
|
| 214 |
+
pointer-events: auto;
|
| 215 |
}
|
| 216 |
|
| 217 |
+
.legend-item {
|
| 218 |
+
display: flex;
|
| 219 |
+
align-items: center;
|
| 220 |
+
gap: 6px;
|
| 221 |
+
font-size: 0.65rem;
|
| 222 |
+
color: #555;
|
| 223 |
+
font-weight: 800;
|
| 224 |
+
text-transform: uppercase;
|
| 225 |
+
letter-spacing: 0.05em;
|
| 226 |
}
|
| 227 |
|
| 228 |
+
.legend-item::before {
|
| 229 |
+
content: '';
|
| 230 |
+
width: 6px;
|
| 231 |
+
height: 6px;
|
| 232 |
border-radius: 50%;
|
| 233 |
+
box-shadow: 0 0 8px currentColor;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.legend-item.core { color: #6366f1; }
|
| 237 |
+
.legend-item.core::before { background-color: #6366f1; }
|
| 238 |
+
|
| 239 |
+
.legend-item.high { color: #ec4899; }
|
| 240 |
+
.legend-item.high::before { background-color: #ec4899; }
|
| 241 |
+
|
| 242 |
+
.legend-item.medium { color: #f59e0b; }
|
| 243 |
+
.legend-item.medium::before { background-color: #f59e0b; }
|
| 244 |
+
|
| 245 |
+
.legend-item.entity { color: #0891b2; }
|
| 246 |
+
.legend-item.entity::before { background-color: #0891b2; }
|
| 247 |
+
|
| 248 |
+
/* ── Live Telemetry HUD Overlay ── */
|
| 249 |
+
.graph-hud-overlay {
|
| 250 |
+
position: absolute;
|
| 251 |
+
top: 16px;
|
| 252 |
+
left: 16px;
|
| 253 |
+
display: flex;
|
| 254 |
+
flex-direction: column;
|
| 255 |
+
gap: 12px;
|
| 256 |
+
z-index: 100;
|
| 257 |
+
pointer-events: none; /* Let clicks pass through to 3D graph */
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.hud-panel {
|
| 261 |
+
background: rgba(255, 255, 255, 0.75);
|
| 262 |
+
backdrop-filter: blur(25px);
|
| 263 |
+
border: 1px solid rgba(255, 255, 255, 0.8);
|
| 264 |
+
border-radius: 16px;
|
| 265 |
+
padding: 12px 14px;
|
| 266 |
+
width: 190px;
|
| 267 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.03);
|
| 268 |
+
pointer-events: auto; /* Re-enable clicks for panel contents */
|
| 269 |
+
display: flex;
|
| 270 |
+
flex-direction: column;
|
| 271 |
+
gap: 8px;
|
| 272 |
+
animation: fadeIn 0.4s ease-out;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.hud-header {
|
| 276 |
display: flex;
|
| 277 |
align-items: center;
|
| 278 |
+
gap: 6px;
|
| 279 |
+
font-size: 0.7rem;
|
| 280 |
+
font-weight: 800;
|
| 281 |
+
text-transform: uppercase;
|
| 282 |
+
letter-spacing: 0.08em;
|
| 283 |
+
color: #4f46e5;
|
| 284 |
+
border-bottom: 1px solid rgba(0,0,0,0.04);
|
| 285 |
+
padding-bottom: 6px;
|
| 286 |
}
|
| 287 |
|
| 288 |
+
.hud-header .material-icons {
|
| 289 |
+
font-size: 14px;
|
|
|
|
| 290 |
}
|
| 291 |
|
| 292 |
+
.hud-row {
|
| 293 |
+
display: flex;
|
| 294 |
+
justify-content: space-between;
|
| 295 |
+
align-items: center;
|
| 296 |
+
font-size: 0.7rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
}
|
| 298 |
|
| 299 |
+
.hud-label {
|
| 300 |
+
color: #64748b;
|
| 301 |
+
font-weight: 600;
|
|
|
|
|
|
|
|
|
|
| 302 |
}
|
| 303 |
|
| 304 |
+
.hud-value {
|
| 305 |
+
color: #1e293b;
|
| 306 |
+
font-weight: 800;
|
| 307 |
+
font-family: var(--font-mono), monospace;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.hud-value.status-glow {
|
| 311 |
+
color: #10b981;
|
| 312 |
+
background: rgba(16, 185, 129, 0.1);
|
| 313 |
+
padding: 2px 6px;
|
| 314 |
+
border-radius: 6px;
|
| 315 |
+
font-weight: 800;
|
| 316 |
+
text-shadow: 0 0 8px rgba(16, 185, 129, 0.2);
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.hud-entity-list {
|
| 320 |
display: flex;
|
| 321 |
+
flex-direction: column;
|
| 322 |
+
gap: 6px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
}
|
| 324 |
|
| 325 |
+
.hud-entity-row {
|
| 326 |
display: flex;
|
| 327 |
align-items: center;
|
| 328 |
+
font-size: 0.65rem;
|
| 329 |
+
gap: 4px;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.entity-rank {
|
| 333 |
+
color: #94a3b8;
|
| 334 |
font-weight: 700;
|
| 335 |
+
width: 18px;
|
|
|
|
| 336 |
}
|
| 337 |
|
| 338 |
+
.entity-name {
|
| 339 |
+
color: #1e293b;
|
| 340 |
+
font-weight: 700;
|
| 341 |
+
flex: 1;
|
| 342 |
+
white-space: nowrap;
|
| 343 |
+
overflow: hidden;
|
| 344 |
+
text-overflow: ellipsis;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.entity-connections {
|
| 348 |
+
color: #6366f1;
|
| 349 |
+
font-weight: 800;
|
| 350 |
+
font-family: var(--font-mono), monospace;
|
| 351 |
+
background: rgba(99, 102, 241, 0.08);
|
| 352 |
+
padding: 1px 4px;
|
| 353 |
+
border-radius: 4px;
|
| 354 |
}
|
| 355 |
|
| 356 |
+
/* Pulse animation utility */
|
| 357 |
+
@keyframes pulse-light {
|
| 358 |
+
0% { opacity: 1; }
|
| 359 |
+
50% { opacity: 0.4; }
|
| 360 |
+
100% { opacity: 1; }
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.pulse {
|
| 364 |
+
animation: pulse-light 1.5s infinite ease-in-out;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
/* ── Holographic Scene Tooltips (Hover Outlined Style) ── */
|
| 368 |
+
.graph-tooltip {
|
| 369 |
+
background: transparent !important;
|
| 370 |
+
border: none !important;
|
| 371 |
+
box-shadow: none !important;
|
| 372 |
+
font-family: 'Inter', sans-serif !important;
|
| 373 |
+
font-weight: 800 !important;
|
| 374 |
+
text-transform: uppercase !important;
|
| 375 |
+
pointer-events: none !important;
|
| 376 |
+
text-shadow:
|
| 377 |
+
-3px -3px 0 #fff,
|
| 378 |
+
3px -3px 0 #fff,
|
| 379 |
+
-3px 3px 0 #fff,
|
| 380 |
+
3px 3px 0 #fff,
|
| 381 |
+
-3px 0px 0 #fff,
|
| 382 |
+
3px 0px 0 #fff,
|
| 383 |
+
0px -3px 0 #fff,
|
| 384 |
+
0px 3px 0 #fff !important;
|
| 385 |
+
padding: 0 !important;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.graph-tooltip strong {
|
| 389 |
+
font-weight: 800 !important;
|
| 390 |
+
}
|
frontend/src/components/KnowledgeGraph.jsx
CHANGED
|
@@ -1,115 +1,371 @@
|
|
| 1 |
-
import { useEffect, useState } from 'react';
|
|
|
|
|
|
|
| 2 |
import { apiFetch } from '../api';
|
| 3 |
import './KnowledgeGraph.css';
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
nodes: [
|
| 8 |
-
{ label: '
|
| 9 |
-
{ label: '
|
| 10 |
-
{ label: '
|
| 11 |
-
{ label: '
|
| 12 |
-
{
|
| 13 |
-
{ label: '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
]
|
| 15 |
};
|
| 16 |
|
| 17 |
-
const POSITIONS = [
|
| 18 |
-
{ x: 50, y: 15 }, // Top
|
| 19 |
-
{ x: 80, y: 35 }, // Top Right
|
| 20 |
-
{ x: 80, y: 65 }, // Bottom Right
|
| 21 |
-
{ x: 50, y: 85 }, // Bottom
|
| 22 |
-
{ x: 20, y: 65 }, // Bottom Left
|
| 23 |
-
{ x: 20, y: 35 }, // Top Left
|
| 24 |
-
];
|
| 25 |
-
|
| 26 |
function KnowledgeGraph({ refreshTick }) {
|
| 27 |
-
const [
|
| 28 |
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
|
|
|
| 30 |
useEffect(() => {
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
const
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
const
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
-
} catch (error) {
|
| 52 |
-
console.error('Graph fetch failed', error);
|
| 53 |
-
} finally {
|
| 54 |
-
setLoading(false);
|
| 55 |
}
|
| 56 |
-
}
|
|
|
|
|
|
|
|
|
|
| 57 |
|
|
|
|
| 58 |
fetchGraph();
|
|
|
|
| 59 |
}, [refreshTick]);
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
return (
|
| 62 |
<div className="graph-stage fade-in">
|
| 63 |
<div className="graph-toolbar">
|
| 64 |
-
<
|
| 65 |
-
<
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
<div className="graph-actions">
|
| 69 |
-
<button
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
</div>
|
| 72 |
</div>
|
| 73 |
|
| 74 |
-
<div className="graph-network">
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
</div>
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
-
{
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
</div>
|
| 114 |
</div>
|
| 115 |
);
|
|
|
|
| 1 |
+
import { useEffect, useState, useRef } from 'react';
|
| 2 |
+
import ForceGraph3D from 'react-force-graph-3d';
|
| 3 |
+
import * as THREE from 'three';
|
| 4 |
import { apiFetch } from '../api';
|
| 5 |
import './KnowledgeGraph.css';
|
| 6 |
|
| 7 |
+
// Stunning conceptual mock graph representing SOMA's cognitive architecture
|
| 8 |
+
const MOCK_GRAPH = {
|
| 9 |
nodes: [
|
| 10 |
+
{ id: 'SOMA', label: 'SOMA (Core)', connections: 10, type: 'core' },
|
| 11 |
+
{ id: 'Cortex', label: 'Cortex Layer', connections: 8, type: 'core' },
|
| 12 |
+
{ id: 'Thalamus', label: 'Thalamus (Routing)', connections: 6, type: 'entity' },
|
| 13 |
+
{ id: 'Hippocampus', label: 'Hippocampus', connections: 7, type: 'entity' },
|
| 14 |
+
{ id: 'Neocortex', label: 'Neocortex', connections: 6, type: 'entity' },
|
| 15 |
+
{ id: 'Sensory Cortex', label: 'Sensory Cortex', connections: 4, type: 'concept' },
|
| 16 |
+
{ id: 'Working Memory', label: 'Working Memory', connections: 5, type: 'concept' },
|
| 17 |
+
{ id: 'Episodic Memory', label: 'Episodic Memory', connections: 4, type: 'concept' },
|
| 18 |
+
{ id: 'Sleep Cycle', label: 'Sleep Cycle', connections: 3, type: 'method' },
|
| 19 |
+
{ id: 'Neural Inscription', label: 'Inscription Layer', connections: 3, type: 'method' },
|
| 20 |
+
{ id: 'Llama 3.1', label: 'Llama 3.1', connections: 3, type: 'metric' },
|
| 21 |
+
{ id: 'Groq API', label: 'Groq API', connections: 2, type: 'metric' }
|
| 22 |
+
],
|
| 23 |
+
links: [
|
| 24 |
+
{ source: 'SOMA', target: 'Cortex', label: 'ORCHESTRATES' },
|
| 25 |
+
{ source: 'SOMA', target: 'Neural Inscription', label: 'ACCEPTS' },
|
| 26 |
+
{ source: 'Cortex', target: 'Thalamus', label: 'ROUTES_BY' },
|
| 27 |
+
{ source: 'Cortex', target: 'Hippocampus', label: 'CONSOLIDATES' },
|
| 28 |
+
{ source: 'Cortex', target: 'Neocortex', label: 'STORES_IN' },
|
| 29 |
+
{ source: 'Sensory Cortex', target: 'Hippocampus', label: 'WRITES_TO' },
|
| 30 |
+
{ source: 'Working Memory', target: 'Thalamus', label: 'SYNCS_WITH' },
|
| 31 |
+
{ source: 'Episodic Memory', target: 'Hippocampus', label: 'LOGS_IN' },
|
| 32 |
+
{ source: 'Sleep Cycle', target: 'Hippocampus', label: 'OPTIMIZES' },
|
| 33 |
+
{ source: 'Sleep Cycle', target: 'Neocortex', label: 'REINFORCES' },
|
| 34 |
+
{ source: 'Cortex', target: 'Llama 3.1', label: 'COMPUTES' },
|
| 35 |
+
{ source: 'Llama 3.1', target: 'Groq API', label: 'HOSTED_ON' }
|
| 36 |
]
|
| 37 |
};
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
function KnowledgeGraph({ refreshTick }) {
|
| 40 |
+
const [graphData, setGraphData] = useState({ nodes: [], links: [] });
|
| 41 |
const [loading, setLoading] = useState(true);
|
| 42 |
+
const [dbStatus, setDbStatus] = useState('connecting');
|
| 43 |
+
const [dimensions, setDimensions] = useState({ width: 800, height: 500 });
|
| 44 |
+
const [stats, setStats] = useState({ node_count: 0, edge_count: 0, top_entities: [] });
|
| 45 |
+
const [physicsActive, setPhysicsActive] = useState(true);
|
| 46 |
+
|
| 47 |
+
const containerRef = useRef(null);
|
| 48 |
+
const fgRef = useRef();
|
| 49 |
|
| 50 |
+
// Measure container dimensions to auto-resize the canvas
|
| 51 |
useEffect(() => {
|
| 52 |
+
if (!containerRef.current) return;
|
| 53 |
+
const resizeObserver = new ResizeObserver((entries) => {
|
| 54 |
+
for (let entry of entries) {
|
| 55 |
+
const { width, height } = entry.contentRect;
|
| 56 |
+
setDimensions({ width: width || 800, height: height || 500 });
|
| 57 |
+
}
|
| 58 |
+
});
|
| 59 |
+
resizeObserver.observe(containerRef.current);
|
| 60 |
+
return () => resizeObserver.disconnect();
|
| 61 |
+
}, []);
|
| 62 |
+
|
| 63 |
+
// Fetch graph database from backend
|
| 64 |
+
const fetchGraph = async () => {
|
| 65 |
+
setLoading(true);
|
| 66 |
+
try {
|
| 67 |
+
const res = await apiFetch('/api/v1/graph');
|
| 68 |
+
if (!res.ok) {
|
| 69 |
+
throw new Error('Graph fetch returned unhealthy status');
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
const data = await res.json();
|
| 73 |
+
|
| 74 |
+
if (data.status === 'offline' || data.status === 'error' || !data.nodes || data.nodes.length === 0) {
|
| 75 |
+
setDbStatus(data.status || 'offline');
|
| 76 |
+
const nodes = MOCK_GRAPH.nodes.map(n => ({ ...n }));
|
| 77 |
+
const links = MOCK_GRAPH.links.map(l => ({ ...l }));
|
| 78 |
+
setGraphData({ nodes, links });
|
| 79 |
+
} else {
|
| 80 |
+
setDbStatus('online');
|
| 81 |
+
const nodes = data.nodes.map(n => ({
|
| 82 |
+
id: n.id,
|
| 83 |
+
label: n.label || n.id,
|
| 84 |
+
connections: n.connections || 1,
|
| 85 |
+
type: 'entity'
|
| 86 |
+
}));
|
| 87 |
|
| 88 |
+
const links = data.edges.map(e => ({
|
| 89 |
+
source: e.source,
|
| 90 |
+
target: e.target,
|
| 91 |
+
label: e.label || 'RELATED_TO'
|
| 92 |
+
}));
|
| 93 |
|
| 94 |
+
setGraphData({ nodes, links });
|
| 95 |
+
}
|
| 96 |
+
} catch (error) {
|
| 97 |
+
console.error('Graph fetch failed', error);
|
| 98 |
+
setDbStatus('offline');
|
| 99 |
+
setGraphData({
|
| 100 |
+
nodes: MOCK_GRAPH.nodes.map(n => ({ ...n })),
|
| 101 |
+
links: MOCK_GRAPH.links.map(l => ({ ...l }))
|
| 102 |
+
});
|
| 103 |
+
} finally {
|
| 104 |
+
setLoading(false);
|
| 105 |
+
}
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
const fetchStats = async () => {
|
| 109 |
+
try {
|
| 110 |
+
const res = await apiFetch('/api/v1/graph/stats');
|
| 111 |
+
if (res.ok) {
|
| 112 |
+
const data = await res.json();
|
| 113 |
+
if (data.status === 'online') {
|
| 114 |
+
setStats(data);
|
| 115 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
+
} catch (err) {
|
| 118 |
+
console.error('Stats fetch failed', err);
|
| 119 |
+
}
|
| 120 |
+
};
|
| 121 |
|
| 122 |
+
useEffect(() => {
|
| 123 |
fetchGraph();
|
| 124 |
+
fetchStats();
|
| 125 |
}, [refreshTick]);
|
| 126 |
|
| 127 |
+
// Physics force configurations to pull the neural cluster into a tight, brain-like shape
|
| 128 |
+
useEffect(() => {
|
| 129 |
+
if (fgRef.current) {
|
| 130 |
+
const d3Force = fgRef.current.d3Force;
|
| 131 |
+
if (d3Force) {
|
| 132 |
+
d3Force('charge').strength(-50); // Gentle repulsion for an ultra-compact molecular look
|
| 133 |
+
d3Force('link').distance(30); // Extremely tight connection paths to pull nodes together
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// Auto-fit camera with very tight padding to zoom in close and eliminate empty workspace
|
| 137 |
+
setTimeout(() => {
|
| 138 |
+
fgRef.current.zoomToFit(800, 20);
|
| 139 |
+
}, 600);
|
| 140 |
+
}
|
| 141 |
+
}, [graphData]);
|
| 142 |
+
|
| 143 |
+
// Calculate max connections dynamically to scale color thresholds
|
| 144 |
+
const maxConnections = graphData.nodes && graphData.nodes.length > 0
|
| 145 |
+
? Math.max(...graphData.nodes.map(n => n.connections || 1), 1)
|
| 146 |
+
: 1;
|
| 147 |
+
|
| 148 |
+
// Color mapper based on node type & degree
|
| 149 |
+
const getNodeColor = (node) => {
|
| 150 |
+
if (node.id === 'SOMA') return '#6366f1'; // Core Hub (Deep Indigo) is always SOMA!
|
| 151 |
+
|
| 152 |
+
// For offline/mock mode, retain the pre-assigned structural category colors
|
| 153 |
+
if (dbStatus !== 'online') {
|
| 154 |
+
if (node.type === 'core') return '#6366f1';
|
| 155 |
+
if (node.type === 'method') return '#10b981'; // Green
|
| 156 |
+
if (node.type === 'concept') return '#3b82f6'; // Blue
|
| 157 |
+
if (node.type === 'metric') return '#f59e0b'; // Amber
|
| 158 |
+
return '#0891b2'; // Cyan
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
// For live Neo4j data, dynamically scale colors based on relative synaptic density!
|
| 162 |
+
const connections = node.connections || 1;
|
| 163 |
+
const ratio = connections / maxConnections;
|
| 164 |
+
|
| 165 |
+
if (ratio >= 0.8) return '#ec4899'; // High centrality (Hot pink)
|
| 166 |
+
if (ratio >= 0.4) return '#f59e0b'; // Medium centrality (Orange)
|
| 167 |
+
return '#0891b2'; // Low centrality (Cyan)
|
| 168 |
+
};
|
| 169 |
+
|
| 170 |
return (
|
| 171 |
<div className="graph-stage fade-in">
|
| 172 |
<div className="graph-toolbar">
|
| 173 |
+
<div className="graph-title-block">
|
| 174 |
+
<button className="graph-select">
|
| 175 |
+
<span className="material-icons">hub</span>
|
| 176 |
+
<span>{dbStatus === 'online' ? 'Real-time 3D Neo4j Graph' : '3D SOMA Cognitive Architecture Model'}</span>
|
| 177 |
+
</button>
|
| 178 |
+
{dbStatus !== 'online' && (
|
| 179 |
+
<span className="db-status-badge warning pulse">
|
| 180 |
+
<span className="dot" />
|
| 181 |
+
Neo4j Offline - Displaying Interactive System Concept
|
| 182 |
+
</span>
|
| 183 |
+
)}
|
| 184 |
+
{dbStatus === 'online' && (
|
| 185 |
+
<span className="db-status-badge success">
|
| 186 |
+
<span className="dot" />
|
| 187 |
+
Neo4j Synchronized (Live)
|
| 188 |
+
</span>
|
| 189 |
+
)}
|
| 190 |
+
</div>
|
| 191 |
+
|
| 192 |
<div className="graph-actions">
|
| 193 |
+
<button
|
| 194 |
+
className="graph-icon-button"
|
| 195 |
+
onClick={() => {
|
| 196 |
+
if (fgRef.current) fgRef.current.zoomToFit(600, 20);
|
| 197 |
+
}}
|
| 198 |
+
title="Recenter Camera"
|
| 199 |
+
>
|
| 200 |
+
<span className="material-icons">zoom_out_map</span>
|
| 201 |
+
</button>
|
| 202 |
+
<button
|
| 203 |
+
className="graph-icon-button"
|
| 204 |
+
onClick={() => {
|
| 205 |
+
if (fgRef.current) {
|
| 206 |
+
const currentPos = fgRef.current.cameraPosition();
|
| 207 |
+
fgRef.current.cameraPosition(
|
| 208 |
+
{ x: currentPos.x * 0.75, y: currentPos.y * 0.75, z: currentPos.z * 0.75 },
|
| 209 |
+
null,
|
| 210 |
+
300
|
| 211 |
+
);
|
| 212 |
+
}
|
| 213 |
+
}}
|
| 214 |
+
title="Zoom In"
|
| 215 |
+
>
|
| 216 |
+
<span className="material-icons">zoom_in</span>
|
| 217 |
+
</button>
|
| 218 |
+
<button
|
| 219 |
+
className="graph-icon-button"
|
| 220 |
+
onClick={() => {
|
| 221 |
+
if (fgRef.current) {
|
| 222 |
+
const currentPos = fgRef.current.cameraPosition();
|
| 223 |
+
fgRef.current.cameraPosition(
|
| 224 |
+
{ x: currentPos.x * 1.35, y: currentPos.y * 1.35, z: currentPos.z * 1.35 },
|
| 225 |
+
null,
|
| 226 |
+
300
|
| 227 |
+
);
|
| 228 |
+
}
|
| 229 |
+
}}
|
| 230 |
+
title="Zoom Out"
|
| 231 |
+
>
|
| 232 |
+
<span className="material-icons">zoom_out</span>
|
| 233 |
+
</button>
|
| 234 |
+
<button
|
| 235 |
+
className="graph-icon-button"
|
| 236 |
+
onClick={() => {
|
| 237 |
+
const newActive = !physicsActive;
|
| 238 |
+
setPhysicsActive(newActive);
|
| 239 |
+
if (fgRef.current) {
|
| 240 |
+
if (physicsActive) {
|
| 241 |
+
fgRef.current.d3PauseSimulation();
|
| 242 |
+
} else {
|
| 243 |
+
fgRef.current.d3ResumeSimulation();
|
| 244 |
+
}
|
| 245 |
+
}
|
| 246 |
+
}}
|
| 247 |
+
title={physicsActive ? "Pause Simulation" : "Resume Simulation"}
|
| 248 |
+
>
|
| 249 |
+
<span className="material-icons">{physicsActive ? "pause" : "play_arrow"}</span>
|
| 250 |
+
</button>
|
| 251 |
+
<button
|
| 252 |
+
className="graph-icon-button"
|
| 253 |
+
onClick={() => {
|
| 254 |
+
fetchGraph();
|
| 255 |
+
fetchStats();
|
| 256 |
+
}}
|
| 257 |
+
title="Sync Mesh"
|
| 258 |
+
>
|
| 259 |
+
<span className="material-icons">sync</span>
|
| 260 |
+
</button>
|
| 261 |
</div>
|
| 262 |
</div>
|
| 263 |
|
| 264 |
+
<div className="graph-network-container" ref={containerRef}>
|
| 265 |
+
{/* Live Telemetry HUD Overlay */}
|
| 266 |
+
<div className="graph-hud-overlay">
|
| 267 |
+
<div className="hud-panel">
|
| 268 |
+
<div className="hud-header">
|
| 269 |
+
<span className="material-icons">analytics</span>
|
| 270 |
+
<span>Semantic Telemetry</span>
|
| 271 |
+
</div>
|
| 272 |
+
<div className="hud-row">
|
| 273 |
+
<span className="hud-label">Nodes:</span>
|
| 274 |
+
<span className="hud-value">{dbStatus === 'online' ? stats.node_count : graphData.nodes.length}</span>
|
| 275 |
+
</div>
|
| 276 |
+
<div className="hud-row">
|
| 277 |
+
<span className="hud-label">Synapses:</span>
|
| 278 |
+
<span className="hud-value">{dbStatus === 'online' ? stats.edge_count : graphData.links.length}</span>
|
| 279 |
+
</div>
|
| 280 |
+
<div className="hud-row">
|
| 281 |
+
<span className="hud-label">Storage:</span>
|
| 282 |
+
<span className="hud-value status-glow">{dbStatus === 'online' ? 'LTM (Neo4j)' : 'STM (Cache)'}</span>
|
| 283 |
+
</div>
|
| 284 |
+
</div>
|
| 285 |
+
|
| 286 |
+
{dbStatus === 'online' && stats.top_entities && stats.top_entities.length > 0 && (
|
| 287 |
+
<div className="hud-panel top-entities">
|
| 288 |
+
<div className="hud-header">
|
| 289 |
+
<span className="material-icons">star</span>
|
| 290 |
+
<span>Primary Hubs</span>
|
| 291 |
+
</div>
|
| 292 |
+
<div className="hud-entity-list">
|
| 293 |
+
{stats.top_entities.map((ent, idx) => (
|
| 294 |
+
<div key={idx} className="hud-entity-row">
|
| 295 |
+
<span className="entity-rank">#{idx+1}</span>
|
| 296 |
+
<span className="entity-name">{ent.entity}</span>
|
| 297 |
+
<span className="entity-connections">{ent.connections} rx</span>
|
| 298 |
+
</div>
|
| 299 |
+
))}
|
| 300 |
+
</div>
|
| 301 |
+
</div>
|
| 302 |
+
)}
|
| 303 |
</div>
|
| 304 |
|
| 305 |
+
<ForceGraph3D
|
| 306 |
+
ref={fgRef}
|
| 307 |
+
graphData={graphData}
|
| 308 |
+
width={dimensions.width}
|
| 309 |
+
height={dimensions.height}
|
| 310 |
+
backgroundColor="rgba(0, 0, 0, 0)" // Glassmorphic translucent rendering
|
| 311 |
+
|
| 312 |
+
// Render gorgeous glowing 3D spheres with emissive materials (100% stable!)
|
| 313 |
+
nodeThreeObject={node => {
|
| 314 |
+
const size = Math.max(3.2, Math.min(node.connections * 1.6, 9.5)); // Perfectly scaled spheres
|
| 315 |
+
const geom = new THREE.SphereGeometry(size, 24, 24);
|
| 316 |
+
const mat = new THREE.MeshLambertMaterial({
|
| 317 |
+
color: getNodeColor(node),
|
| 318 |
+
transparent: true,
|
| 319 |
+
opacity: 0.95,
|
| 320 |
+
emissive: getNodeColor(node),
|
| 321 |
+
emissiveIntensity: 0.45
|
| 322 |
+
});
|
| 323 |
+
return new THREE.Mesh(geom, mat);
|
| 324 |
+
}}
|
| 325 |
+
|
| 326 |
+
// Outlined holographic label rendered natively floating beside cursor on hover (100% stable!)
|
| 327 |
+
nodeLabel={node => `
|
| 328 |
+
<span style="color: ${getNodeColor(node)}; font-size: 24px; font-weight: 800;">
|
| 329 |
+
${node.label || node.id}
|
| 330 |
+
</span>
|
| 331 |
+
`}
|
| 332 |
+
|
| 333 |
+
// Outlined relationship tag rendered on link hover (100% stable!)
|
| 334 |
+
linkLabel={link => `
|
| 335 |
+
<span style="color: #6366f1; font-size: 16px; font-weight: 800;">
|
| 336 |
+
${link.label || 'RELATED_TO'}
|
| 337 |
+
</span>
|
| 338 |
+
`}
|
| 339 |
+
|
| 340 |
+
nodeRelSize={3}
|
| 341 |
+
linkColor={() => 'rgba(99, 102, 241, 0.25)'} // Soft, clean axon link fibers
|
| 342 |
+
linkWidth={1.8} // Sleek link line thickness
|
| 343 |
+
|
| 344 |
+
// Glowing Thought Flows (sliding directional particles along axons)
|
| 345 |
+
linkDirectionalParticles={3}
|
| 346 |
+
linkDirectionalParticleSpeed={0.006}
|
| 347 |
+
linkDirectionalParticleWidth={1.5}
|
| 348 |
+
linkDirectionalParticleColor={() => '#6366f1'}
|
| 349 |
+
|
| 350 |
+
showNavInfo={false}
|
| 351 |
+
enablePointerInteraction={true}
|
| 352 |
+
enableNodeDrag={true}
|
| 353 |
+
/>
|
| 354 |
|
| 355 |
+
{/* Beautiful Glassmorphic Legend HUD */}
|
| 356 |
+
<div className="graph-legend">
|
| 357 |
+
<div className="legend-item core">Core Hub</div>
|
| 358 |
+
<div className="legend-item high">High Density</div>
|
| 359 |
+
<div className="legend-item medium">Medium Density</div>
|
| 360 |
+
<div className="legend-item entity">Low Density</div>
|
| 361 |
+
</div>
|
| 362 |
|
| 363 |
+
{loading && (
|
| 364 |
+
<div className="graph-loading">
|
| 365 |
+
<span className="material-icons pulse">refresh</span>
|
| 366 |
+
<span>Synchronizing cognitive mesh...</span>
|
| 367 |
+
</div>
|
| 368 |
+
)}
|
| 369 |
</div>
|
| 370 |
</div>
|
| 371 |
);
|
frontend/src/components/KnowledgeInput.css
CHANGED
|
@@ -1,115 +1,382 @@
|
|
| 1 |
-
/* KnowledgeInput.css -
|
| 2 |
|
| 3 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
display: flex;
|
| 5 |
flex-direction: column;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
align-items: center;
|
| 7 |
-
|
| 8 |
-
text-align: center;
|
| 9 |
-
height: 100%;
|
| 10 |
}
|
| 11 |
|
| 12 |
-
.
|
| 13 |
-
width:
|
| 14 |
-
height:
|
| 15 |
-
background
|
| 16 |
-
border
|
|
|
|
| 17 |
display: flex;
|
| 18 |
align-items: center;
|
| 19 |
justify-content: center;
|
| 20 |
-
|
| 21 |
}
|
| 22 |
|
| 23 |
-
.
|
| 24 |
-
|
| 25 |
-
|
| 26 |
}
|
| 27 |
|
| 28 |
-
.
|
| 29 |
font-family: var(--font-display);
|
| 30 |
-
font-size: 2rem;
|
| 31 |
-
font-weight:
|
| 32 |
-
margin-bottom: 12px;
|
| 33 |
color: #1a1a1a;
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
-
.
|
| 37 |
-
font-size:
|
| 38 |
-
color: #
|
| 39 |
-
|
| 40 |
-
max-width: 500px;
|
| 41 |
}
|
| 42 |
|
| 43 |
-
.
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
border-radius:
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
-
.
|
|
|
|
| 54 |
width: 100%;
|
| 55 |
-
min-height: 280px;
|
| 56 |
background: transparent;
|
| 57 |
border: none;
|
| 58 |
outline: none;
|
| 59 |
-
font-size:
|
|
|
|
| 60 |
color: #1a1a1a;
|
| 61 |
resize: none;
|
| 62 |
-
line-height: 1.6;
|
| 63 |
font-weight: 500;
|
| 64 |
}
|
| 65 |
|
| 66 |
-
.
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
.knowledge-composer-footer {
|
| 71 |
display: flex;
|
| 72 |
justify-content: space-between;
|
| 73 |
align-items: center;
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
| 77 |
}
|
| 78 |
|
| 79 |
.char-count {
|
| 80 |
-
font-size: 0.
|
|
|
|
| 81 |
color: #bbb;
|
| 82 |
-
font-weight: 600;
|
| 83 |
text-transform: uppercase;
|
| 84 |
-
letter-spacing: 0.
|
| 85 |
}
|
| 86 |
|
| 87 |
-
.
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
color: white;
|
| 90 |
-
padding:
|
| 91 |
-
border-radius:
|
| 92 |
font-weight: 700;
|
| 93 |
-
font-size:
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
| 96 |
}
|
| 97 |
|
| 98 |
-
.
|
| 99 |
transform: translateY(-2px);
|
| 100 |
-
box-shadow: 0
|
| 101 |
}
|
| 102 |
|
| 103 |
-
.
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
}
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
font-weight: 700;
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
}
|
|
|
|
| 1 |
+
/* KnowledgeInput.css - Neural Inscription Workspace */
|
| 2 |
|
| 3 |
+
.inscription-grid {
|
| 4 |
+
display: grid;
|
| 5 |
+
grid-template-columns: 1fr 340px;
|
| 6 |
+
gap: 32px;
|
| 7 |
+
flex: 1;
|
| 8 |
+
min-height: 0;
|
| 9 |
+
padding: 0 40px 40px 40px;
|
| 10 |
+
overflow: hidden;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
.inscription-main {
|
| 14 |
display: flex;
|
| 15 |
flex-direction: column;
|
| 16 |
+
gap: 24px;
|
| 17 |
+
overflow-y: auto;
|
| 18 |
+
padding-right: 8px;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.inscription-header {
|
| 22 |
+
display: flex;
|
| 23 |
align-items: center;
|
| 24 |
+
gap: 20px;
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
+
.inscription-icon {
|
| 28 |
+
width: 56px;
|
| 29 |
+
height: 56px;
|
| 30 |
+
background: white;
|
| 31 |
+
border: 1px solid rgba(0,0,0,0.05);
|
| 32 |
+
border-radius: 16px;
|
| 33 |
display: flex;
|
| 34 |
align-items: center;
|
| 35 |
justify-content: center;
|
| 36 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.02);
|
| 37 |
}
|
| 38 |
|
| 39 |
+
.inscription-icon .material-icons {
|
| 40 |
+
color: #ff6b35;
|
| 41 |
+
font-size: 24px;
|
| 42 |
}
|
| 43 |
|
| 44 |
+
.inscription-title h3 {
|
| 45 |
font-family: var(--font-display);
|
| 46 |
+
font-size: 1.2rem;
|
| 47 |
+
font-weight: 800;
|
|
|
|
| 48 |
color: #1a1a1a;
|
| 49 |
+
margin-bottom: 2px;
|
| 50 |
}
|
| 51 |
|
| 52 |
+
.inscription-title p {
|
| 53 |
+
font-size: 0.75rem;
|
| 54 |
+
color: #999;
|
| 55 |
+
font-weight: 500;
|
|
|
|
| 56 |
}
|
| 57 |
|
| 58 |
+
.inscription-composer {
|
| 59 |
+
flex: 1;
|
| 60 |
+
background: rgba(255, 255, 255, 0.6);
|
| 61 |
+
backdrop-filter: blur(20px);
|
| 62 |
+
border-radius: 24px;
|
| 63 |
+
border: 1px solid rgba(255, 255, 255, 0.8);
|
| 64 |
+
padding: 32px;
|
| 65 |
+
display: flex;
|
| 66 |
+
flex-direction: column;
|
| 67 |
+
box-shadow: 0 20px 50px rgba(0,0,0,0.04);
|
| 68 |
+
max-height: 600px;
|
| 69 |
}
|
| 70 |
|
| 71 |
+
.inscription-composer textarea {
|
| 72 |
+
flex: 1;
|
| 73 |
width: 100%;
|
|
|
|
| 74 |
background: transparent;
|
| 75 |
border: none;
|
| 76 |
outline: none;
|
| 77 |
+
font-size: 0.85rem;
|
| 78 |
+
line-height: 1.6;
|
| 79 |
color: #1a1a1a;
|
| 80 |
resize: none;
|
|
|
|
| 81 |
font-weight: 500;
|
| 82 |
}
|
| 83 |
|
| 84 |
+
.inscription-footer {
|
| 85 |
+
margin-top: 24px;
|
| 86 |
+
padding-top: 24px;
|
| 87 |
+
border-top: 1px solid #f8f8f8;
|
|
|
|
| 88 |
display: flex;
|
| 89 |
justify-content: space-between;
|
| 90 |
align-items: center;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.inscription-stats {
|
| 94 |
+
display: flex;
|
| 95 |
+
flex-direction: column;
|
| 96 |
+
gap: 4px;
|
| 97 |
}
|
| 98 |
|
| 99 |
.char-count {
|
| 100 |
+
font-size: 0.65rem;
|
| 101 |
+
font-weight: 800;
|
| 102 |
color: #bbb;
|
|
|
|
| 103 |
text-transform: uppercase;
|
| 104 |
+
letter-spacing: 0.1em;
|
| 105 |
}
|
| 106 |
|
| 107 |
+
.busy-tag {
|
| 108 |
+
font-size: 0.65rem;
|
| 109 |
+
font-weight: 800;
|
| 110 |
+
color: #ff6b35;
|
| 111 |
+
text-transform: uppercase;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.inscription-btn {
|
| 115 |
+
background: #ff6b35;
|
| 116 |
color: white;
|
| 117 |
+
padding: 12px 28px;
|
| 118 |
+
border-radius: 14px;
|
| 119 |
font-weight: 700;
|
| 120 |
+
font-size: 0.85rem;
|
| 121 |
+
display: flex;
|
| 122 |
+
align-items: center;
|
| 123 |
+
gap: 8px;
|
| 124 |
+
transition: all 0.2s;
|
| 125 |
+
box-shadow: 0 8px 20px rgba(255, 107, 53, 0.2);
|
| 126 |
}
|
| 127 |
|
| 128 |
+
.inscription-btn:hover:not(:disabled) {
|
| 129 |
transform: translateY(-2px);
|
| 130 |
+
box-shadow: 0 12px 25px rgba(255, 107, 53, 0.3);
|
| 131 |
}
|
| 132 |
|
| 133 |
+
.inscription-status {
|
| 134 |
+
display: flex;
|
| 135 |
+
align-items: center;
|
| 136 |
+
gap: 12px;
|
| 137 |
+
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1), rgba(16, 185, 129, 0.05));
|
| 138 |
+
color: #059669;
|
| 139 |
+
padding: 16px 24px;
|
| 140 |
+
border-radius: 16px;
|
| 141 |
+
font-size: 0.85rem;
|
| 142 |
+
font-weight: 700;
|
| 143 |
+
border: 1px solid rgba(16, 185, 129, 0.2);
|
| 144 |
+
box-shadow: 0 4px 20px rgba(16, 185, 129, 0.1);
|
| 145 |
}
|
| 146 |
|
| 147 |
+
/* ── Analysis Sidebar ── */
|
| 148 |
+
.inscription-sidebar {
|
| 149 |
+
display: flex;
|
| 150 |
+
flex-direction: column;
|
| 151 |
+
overflow-y: auto;
|
| 152 |
+
padding-right: 4px;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.inscription-sidebar::-webkit-scrollbar {
|
| 156 |
+
width: 4px;
|
| 157 |
+
}
|
| 158 |
+
.inscription-sidebar::-webkit-scrollbar-track {
|
| 159 |
+
background: transparent;
|
| 160 |
+
}
|
| 161 |
+
.inscription-sidebar::-webkit-scrollbar-thumb {
|
| 162 |
+
background: rgba(0,0,0,0.1);
|
| 163 |
+
border-radius: 10px;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.analysis-panel {
|
| 167 |
+
background: rgba(255, 255, 255, 0.4);
|
| 168 |
+
backdrop-filter: blur(20px);
|
| 169 |
+
border: 1px solid rgba(255, 255, 255, 0.5);
|
| 170 |
+
border-radius: 24px;
|
| 171 |
+
padding: 24px;
|
| 172 |
+
display: flex;
|
| 173 |
+
flex-direction: column;
|
| 174 |
+
gap: 24px;
|
| 175 |
+
height: 100%;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.panel-label {
|
| 179 |
+
font-size: 0.65rem;
|
| 180 |
+
font-weight: 800;
|
| 181 |
+
color: #aaa;
|
| 182 |
+
text-transform: uppercase;
|
| 183 |
+
letter-spacing: 0.1em;
|
| 184 |
+
display: flex;
|
| 185 |
+
align-items: center;
|
| 186 |
+
gap: 8px;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.analysis-content {
|
| 190 |
+
flex: 1;
|
| 191 |
+
display: flex;
|
| 192 |
+
flex-direction: column;
|
| 193 |
+
gap: 32px;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.analysis-metric {
|
| 197 |
+
display: flex;
|
| 198 |
+
flex-direction: column;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.analysis-section label,
|
| 202 |
+
.analysis-metric label {
|
| 203 |
+
font-size: 0.65rem;
|
| 204 |
+
font-weight: 800;
|
| 205 |
+
color: #999;
|
| 206 |
+
text-transform: uppercase;
|
| 207 |
+
display: block;
|
| 208 |
+
margin-bottom: 8px;
|
| 209 |
+
letter-spacing: 0.05em;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.mini-meter {
|
| 213 |
+
height: 6px;
|
| 214 |
+
background: rgba(0,0,0,0.05);
|
| 215 |
+
border-radius: 10px;
|
| 216 |
+
margin-top: 8px;
|
| 217 |
+
overflow: hidden;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.mini-fill {
|
| 221 |
+
height: 100%;
|
| 222 |
+
background: #ff6b35;
|
| 223 |
+
transition: width 0.3s;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.link-tags {
|
| 227 |
+
display: flex;
|
| 228 |
+
flex-wrap: wrap;
|
| 229 |
+
gap: 8px;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.link-tag {
|
| 233 |
+
background: white;
|
| 234 |
+
padding: 6px 12px;
|
| 235 |
+
border-radius: 100px;
|
| 236 |
+
font-size: 0.7rem;
|
| 237 |
+
font-weight: 700;
|
| 238 |
+
color: #555;
|
| 239 |
+
border: 1px solid rgba(0,0,0,0.03);
|
| 240 |
+
display: flex;
|
| 241 |
+
align-items: center;
|
| 242 |
+
gap: 6px;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
.link-tag .dot {
|
| 246 |
+
width: 4px;
|
| 247 |
+
height: 4px;
|
| 248 |
+
border-radius: 50%;
|
| 249 |
+
background: #ff6b35;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.empty-tag {
|
| 253 |
+
font-size: 0.7rem;
|
| 254 |
+
color: #ccc;
|
| 255 |
+
font-style: italic;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.layer-item {
|
| 259 |
+
display: flex;
|
| 260 |
+
align-items: center;
|
| 261 |
+
gap: 10px;
|
| 262 |
+
margin-bottom: 8px;
|
| 263 |
+
font-size: 0.75rem;
|
| 264 |
+
font-weight: 600;
|
| 265 |
+
color: #666;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.layer-item .dot {
|
| 269 |
+
width: 6px;
|
| 270 |
+
height: 6px;
|
| 271 |
+
border-radius: 50%;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.metric-sub {
|
| 275 |
+
display: flex;
|
| 276 |
+
justify-content: space-between;
|
| 277 |
+
margin-top: 6px;
|
| 278 |
+
font-size: 0.6rem;
|
| 279 |
+
font-weight: 700;
|
| 280 |
+
color: #999;
|
| 281 |
+
text-transform: uppercase;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.impact-stats {
|
| 285 |
+
display: grid;
|
| 286 |
+
grid-template-columns: 1fr 1fr;
|
| 287 |
+
gap: 12px;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.impact-item {
|
| 291 |
+
background: white;
|
| 292 |
+
padding: 12px;
|
| 293 |
+
border-radius: 12px;
|
| 294 |
+
border: 1px solid rgba(0,0,0,0.03);
|
| 295 |
+
display: flex;
|
| 296 |
+
flex-direction: column;
|
| 297 |
+
gap: 4px;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.impact-val {
|
| 301 |
+
font-size: 1.1rem;
|
| 302 |
+
font-weight: 800;
|
| 303 |
+
color: #1a1a1a;
|
| 304 |
+
font-family: var(--font-display);
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.impact-label {
|
| 308 |
+
font-size: 0.55rem;
|
| 309 |
+
font-weight: 700;
|
| 310 |
+
color: #aaa;
|
| 311 |
+
text-transform: uppercase;
|
| 312 |
+
letter-spacing: 0.05em;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.link-tag.reinforced {
|
| 316 |
+
border-color: rgba(16, 185, 129, 0.3);
|
| 317 |
+
background: rgba(16, 185, 129, 0.05);
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.reinforced-icon {
|
| 321 |
+
font-size: 12px !important;
|
| 322 |
+
color: #10b981;
|
| 323 |
+
margin-left: 2px;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.existing-links-list {
|
| 327 |
+
display: flex;
|
| 328 |
+
flex-direction: column;
|
| 329 |
+
gap: 12px;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.existing-link-item {
|
| 333 |
+
display: flex;
|
| 334 |
+
align-items: center;
|
| 335 |
+
gap: 12px;
|
| 336 |
+
background: white;
|
| 337 |
+
padding: 10px;
|
| 338 |
+
border-radius: 12px;
|
| 339 |
+
border: 1px solid rgba(0,0,0,0.03);
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.existing-link-item .material-icons {
|
| 343 |
+
font-size: 18px;
|
| 344 |
+
color: #10b981;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.link-info {
|
| 348 |
+
display: flex;
|
| 349 |
+
flex-direction: column;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.link-info strong {
|
| 353 |
+
font-size: 0.75rem;
|
| 354 |
font-weight: 700;
|
| 355 |
+
color: #1a1a1a;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
.link-info span {
|
| 359 |
+
font-size: 0.6rem;
|
| 360 |
+
color: #999;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.panel-footer {
|
| 364 |
+
font-size: 0.65rem;
|
| 365 |
+
color: #bbb;
|
| 366 |
+
display: flex;
|
| 367 |
+
align-items: center;
|
| 368 |
+
gap: 8px;
|
| 369 |
+
padding-top: 20px;
|
| 370 |
+
border-top: 1px solid rgba(0,0,0,0.05);
|
| 371 |
+
margin-top: auto;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
@keyframes pulse-blue {
|
| 375 |
+
0% { opacity: 1; }
|
| 376 |
+
50% { opacity: 0.5; }
|
| 377 |
+
100% { opacity: 1; }
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.busy-tag.pulse {
|
| 381 |
+
animation: pulse-blue 1.5s infinite ease-in-out;
|
| 382 |
}
|
frontend/src/components/KnowledgeInput.jsx
CHANGED
|
@@ -1,46 +1,227 @@
|
|
| 1 |
-
import { useState } from 'react';
|
|
|
|
| 2 |
import './KnowledgeInput.css';
|
| 3 |
|
| 4 |
function KnowledgeInput({ onKnowledgeSubmit, isBusy, status }) {
|
| 5 |
const [text, setText] = useState('');
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
const handleSubmit = () => {
|
| 8 |
if (!text.trim() || isBusy) return;
|
| 9 |
onKnowledgeSubmit(text);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
setText('');
|
|
|
|
| 11 |
};
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
return (
|
| 14 |
-
<div className="
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
<
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
</div>
|
| 39 |
</div>
|
| 40 |
|
| 41 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
</div>
|
| 43 |
);
|
| 44 |
}
|
| 45 |
|
| 46 |
export default KnowledgeInput;
|
|
|
|
|
|
| 1 |
+
import { useState, useMemo, useEffect, useCallback } from 'react';
|
| 2 |
+
import { apiFetch } from '../api';
|
| 3 |
import './KnowledgeInput.css';
|
| 4 |
|
| 5 |
function KnowledgeInput({ onKnowledgeSubmit, isBusy, status }) {
|
| 6 |
const [text, setText] = useState('');
|
| 7 |
+
const [analysis, setAnalysis] = useState(null);
|
| 8 |
+
const [lastAnalysis, setLastAnalysis] = useState(null);
|
| 9 |
+
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
| 10 |
|
| 11 |
const handleSubmit = () => {
|
| 12 |
if (!text.trim() || isBusy) return;
|
| 13 |
onKnowledgeSubmit(text);
|
| 14 |
+
|
| 15 |
+
if (analysis) {
|
| 16 |
+
setLastAnalysis(analysis);
|
| 17 |
+
} else {
|
| 18 |
+
// Fallback if they clicked instantly or text was < 50 chars
|
| 19 |
+
const charCount = text.length;
|
| 20 |
+
setLastAnalysis({
|
| 21 |
+
metrics: {
|
| 22 |
+
density: Math.min(charCount / 2000, 1.0),
|
| 23 |
+
chunks: Math.floor(charCount / 500) + 1,
|
| 24 |
+
estimated_links: 0,
|
| 25 |
+
reinforcement_index: 0
|
| 26 |
+
},
|
| 27 |
+
entities: [],
|
| 28 |
+
existing_links: []
|
| 29 |
+
});
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
setText('');
|
| 33 |
+
setAnalysis(null);
|
| 34 |
};
|
| 35 |
|
| 36 |
+
// Debounced analysis function
|
| 37 |
+
useEffect(() => {
|
| 38 |
+
if (text.length < 50) {
|
| 39 |
+
setAnalysis(null);
|
| 40 |
+
return;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
const timer = setTimeout(async () => {
|
| 44 |
+
setIsAnalyzing(true);
|
| 45 |
+
try {
|
| 46 |
+
const res = await apiFetch('/api/v1/analyze', {
|
| 47 |
+
method: 'POST',
|
| 48 |
+
body: JSON.stringify({ text })
|
| 49 |
+
});
|
| 50 |
+
if (res.ok) {
|
| 51 |
+
const data = await res.json();
|
| 52 |
+
setAnalysis(data);
|
| 53 |
+
}
|
| 54 |
+
} catch (error) {
|
| 55 |
+
console.error('Analysis failed', error);
|
| 56 |
+
} finally {
|
| 57 |
+
setIsAnalyzing(false);
|
| 58 |
+
}
|
| 59 |
+
}, 1500); // 1.5s debounce
|
| 60 |
+
|
| 61 |
+
return () => clearTimeout(timer);
|
| 62 |
+
}, [text]);
|
| 63 |
+
|
| 64 |
+
const displayAnalysis = analysis || lastAnalysis;
|
| 65 |
+
|
| 66 |
return (
|
| 67 |
+
<div className="inscription-grid fade-in">
|
| 68 |
+
{/* ── LEFT: The Inscription Terminal ── */}
|
| 69 |
+
<div className="inscription-main">
|
| 70 |
+
{status && (
|
| 71 |
+
<div className="inscription-status fade-in">
|
| 72 |
+
<span className="material-icons">check_circle</span>
|
| 73 |
+
{status}
|
| 74 |
+
</div>
|
| 75 |
+
)}
|
| 76 |
+
|
| 77 |
+
<div className="inscription-header">
|
| 78 |
+
<div className="inscription-icon">
|
| 79 |
+
<span className="material-icons">auto_awesome</span>
|
| 80 |
+
</div>
|
| 81 |
+
<div className="inscription-title">
|
| 82 |
+
<h3>Neural Inscription</h3>
|
| 83 |
+
<p>Directly seed the core cortex with high-fidelity knowledge.</p>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<div className="inscription-composer">
|
| 88 |
+
<textarea
|
| 89 |
+
value={text}
|
| 90 |
+
onChange={(e) => {
|
| 91 |
+
setText(e.target.value);
|
| 92 |
+
if (lastAnalysis) setLastAnalysis(null);
|
| 93 |
+
}}
|
| 94 |
+
placeholder="Paste research, notes, or data chunks here for deep integration..."
|
| 95 |
+
maxLength={10000}
|
| 96 |
+
/>
|
| 97 |
+
<div className="inscription-footer">
|
| 98 |
+
<div className="inscription-stats">
|
| 99 |
+
<span className="char-count">{text.length} / 10000 Chars</span>
|
| 100 |
+
{isBusy && <span className="busy-tag pulse">Integrating...</span>}
|
| 101 |
+
{isAnalyzing && <span className="busy-tag pulse" style={{color: '#3b82f6'}}>Analyzing Pattern...</span>}
|
| 102 |
+
</div>
|
| 103 |
+
<button
|
| 104 |
+
className="inscription-btn"
|
| 105 |
+
onClick={handleSubmit}
|
| 106 |
+
disabled={!text.trim() || isBusy}
|
| 107 |
+
>
|
| 108 |
+
<span className="material-icons">memory</span>
|
| 109 |
+
Inscribe Knowledge
|
| 110 |
+
</button>
|
| 111 |
+
</div>
|
| 112 |
</div>
|
| 113 |
</div>
|
| 114 |
|
| 115 |
+
{/* ── RIGHT: Neural Analysis Preview ── */}
|
| 116 |
+
<div className="inscription-sidebar">
|
| 117 |
+
<div className="analysis-panel">
|
| 118 |
+
<div className="panel-label">
|
| 119 |
+
<span className="material-icons">biotech</span>
|
| 120 |
+
{(!analysis && lastAnalysis) ? 'Last Inscription Details' : 'Neural Analysis Preview'}
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<div className="analysis-content">
|
| 124 |
+
<div className="analysis-metric">
|
| 125 |
+
<label>Information Density</label>
|
| 126 |
+
<div className="mini-meter">
|
| 127 |
+
<div className="mini-fill" style={{ width: `${(displayAnalysis?.metrics?.density || 0) * 100}%` }} />
|
| 128 |
+
</div>
|
| 129 |
+
<div className="metric-sub">
|
| 130 |
+
<span>{Math.round((displayAnalysis?.metrics?.density || 0) * 100)}% Salience</span>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<div className="analysis-metric">
|
| 135 |
+
<label>Knowledge Reinforcement</label>
|
| 136 |
+
<div className="mini-meter">
|
| 137 |
+
<div className="mini-fill" style={{
|
| 138 |
+
width: `${(displayAnalysis?.metrics?.reinforcement_index || 0) * 100}%`,
|
| 139 |
+
background: '#10b981'
|
| 140 |
+
}} />
|
| 141 |
+
</div>
|
| 142 |
+
<div className="metric-sub">
|
| 143 |
+
<span>{Math.round((displayAnalysis?.metrics?.reinforcement_index || 0) * 100)}% Integration</span>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<div className="analysis-section">
|
| 148 |
+
<label>Potential Semantic Links</label>
|
| 149 |
+
<div className="link-tags">
|
| 150 |
+
{(displayAnalysis?.entities || []).map((entity, i) => {
|
| 151 |
+
const isExisting = displayAnalysis?.existing_links?.some(l => l.name.toLowerCase() === entity.toLowerCase());
|
| 152 |
+
return (
|
| 153 |
+
<div
|
| 154 |
+
key={i}
|
| 155 |
+
className={`link-tag fade-in ${isExisting ? 'reinforced' : ''}`}
|
| 156 |
+
style={{animationDelay: `${i * 0.1}s`}}
|
| 157 |
+
>
|
| 158 |
+
<span className="dot" />
|
| 159 |
+
{entity}
|
| 160 |
+
{isExisting && <span className="material-icons reinforced-icon">offline_bolt</span>}
|
| 161 |
+
</div>
|
| 162 |
+
);
|
| 163 |
+
})}
|
| 164 |
+
{(!displayAnalysis?.entities || displayAnalysis.entities.length === 0) && (
|
| 165 |
+
<div className="empty-tag">
|
| 166 |
+
{isAnalyzing ? 'Decoding semantic structure...' : 'Awaiting sufficient context (50+ chars)...'}
|
| 167 |
+
</div>
|
| 168 |
+
)}
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
|
| 172 |
+
{displayAnalysis?.existing_links?.length > 0 && (
|
| 173 |
+
<div className="analysis-section fade-in">
|
| 174 |
+
<label>Reinforced Concepts</label>
|
| 175 |
+
<div className="existing-links-list">
|
| 176 |
+
{displayAnalysis.existing_links.map((link, i) => (
|
| 177 |
+
<div key={i} className="existing-link-item">
|
| 178 |
+
<span className="material-icons">hub</span>
|
| 179 |
+
<div className="link-info">
|
| 180 |
+
<strong>{link.name}</strong>
|
| 181 |
+
<span>{link.connections} existing connections</span>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
))}
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
)}
|
| 188 |
+
|
| 189 |
+
<div className="analysis-section">
|
| 190 |
+
<label>Estimated Memory Impact</label>
|
| 191 |
+
<div className="impact-stats">
|
| 192 |
+
<div className="impact-item">
|
| 193 |
+
<span className="impact-val">{displayAnalysis?.metrics?.chunks || 0}</span>
|
| 194 |
+
<span className="impact-label">Sensory Chunks</span>
|
| 195 |
+
</div>
|
| 196 |
+
<div className="impact-item">
|
| 197 |
+
<span className="impact-val">{Math.floor(displayAnalysis?.metrics?.estimated_links || 0)}</span>
|
| 198 |
+
<span className="impact-label">Graph Triples</span>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
|
| 203 |
+
<div className="analysis-section">
|
| 204 |
+
<label>Target Layers</label>
|
| 205 |
+
<div className="layer-item">
|
| 206 |
+
<span className="dot" style={{background: '#ff6b35'}} />
|
| 207 |
+
<span>Semantic Memory (Graph)</span>
|
| 208 |
+
</div>
|
| 209 |
+
<div className="layer-item">
|
| 210 |
+
<span className="dot" style={{background: '#10b981'}} />
|
| 211 |
+
<span>Sensory Cortex (Vector)</span>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
<div className="panel-footer">
|
| 217 |
+
<span className="material-icons">info</span>
|
| 218 |
+
Data will be parsed into triples and vectorized.
|
| 219 |
+
</div>
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
</div>
|
| 223 |
);
|
| 224 |
}
|
| 225 |
|
| 226 |
export default KnowledgeInput;
|
| 227 |
+
|
frontend/src/components/MemoryExplorer.css
CHANGED
|
@@ -1,194 +1,511 @@
|
|
| 1 |
-
/* MemoryExplorer.css -
|
| 2 |
|
| 3 |
.memory-view-grid {
|
| 4 |
display: grid;
|
| 5 |
-
grid-template-columns:
|
| 6 |
-
gap:
|
| 7 |
-
height:
|
|
|
|
|
|
|
| 8 |
}
|
| 9 |
|
| 10 |
.memory-surface {
|
| 11 |
display: flex;
|
| 12 |
flex-direction: column;
|
| 13 |
min-height: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
}
|
| 15 |
|
| 16 |
-
/* ── Toolbar ── */
|
| 17 |
.memory-toolbar {
|
| 18 |
display: flex;
|
| 19 |
-
|
| 20 |
-
|
|
|
|
| 21 |
}
|
| 22 |
|
| 23 |
-
.memory-search {
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
border-radius:
|
| 27 |
-
padding: 0
|
| 28 |
display: flex;
|
| 29 |
align-items: center;
|
| 30 |
-
gap:
|
| 31 |
-
height:
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
-
.memory-search
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
flex: 1;
|
| 36 |
background: transparent;
|
| 37 |
border: none;
|
| 38 |
outline: none;
|
| 39 |
-
font-size:
|
| 40 |
color: #1a1a1a;
|
| 41 |
font-weight: 500;
|
| 42 |
}
|
| 43 |
|
| 44 |
-
.memory-search .material-icons {
|
| 45 |
-
color: #
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
border-radius: 18px;
|
| 51 |
-
padding: 0 20px;
|
| 52 |
display: flex;
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
color: #1a1a1a;
|
| 58 |
-
|
| 59 |
}
|
| 60 |
|
| 61 |
-
/* ── List ── */
|
| 62 |
.memory-list {
|
| 63 |
flex: 1;
|
| 64 |
overflow-y: auto;
|
| 65 |
display: flex;
|
| 66 |
flex-direction: column;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
gap: 12px;
|
|
|
|
| 68 |
}
|
| 69 |
|
| 70 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
display: flex;
|
| 72 |
-
|
| 73 |
gap: 20px;
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
text-align: left;
|
| 77 |
-
transition: all 0.
|
| 78 |
border: 1px solid transparent;
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
|
| 81 |
.memory-row:hover {
|
| 82 |
-
background
|
| 83 |
}
|
| 84 |
|
| 85 |
.memory-row.active {
|
| 86 |
-
background
|
| 87 |
-
box-shadow: 0
|
| 88 |
-
border-color: rgba(0,0,0,0.
|
| 89 |
-
transform: translateX(8px);
|
| 90 |
}
|
| 91 |
|
| 92 |
.memory-dot {
|
| 93 |
-
width:
|
| 94 |
-
height:
|
| 95 |
border-radius: 50%;
|
| 96 |
-
margin-top: 6px;
|
| 97 |
flex-shrink: 0;
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
.memory-row-copy {
|
| 101 |
flex: 1;
|
|
|
|
| 102 |
}
|
| 103 |
|
| 104 |
.memory-row-copy strong {
|
| 105 |
display: block;
|
| 106 |
-
font-size:
|
| 107 |
font-weight: 700;
|
| 108 |
color: #1a1a1a;
|
| 109 |
-
margin-bottom:
|
| 110 |
-
line-height: 1.
|
|
|
|
|
|
|
|
|
|
| 111 |
}
|
| 112 |
|
| 113 |
.memory-row-meta {
|
| 114 |
display: flex;
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
| 119 |
}
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
text-transform: uppercase;
|
| 125 |
-
letter-spacing: 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
font-weight: 700;
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
| 128 |
}
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
border-radius: 40px;
|
| 134 |
-
padding: 48px;
|
| 135 |
-
box-shadow: 0 10px 40px rgba(0,0,0,0.03);
|
| 136 |
-
border: 1px solid rgba(0,0,0,0.02);
|
| 137 |
}
|
| 138 |
|
| 139 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
font-family: var(--font-display);
|
| 141 |
-
font-size: 1.
|
| 142 |
-
font-weight:
|
| 143 |
-
margin-bottom:
|
| 144 |
color: #1a1a1a;
|
| 145 |
-
|
| 146 |
}
|
| 147 |
|
| 148 |
.memory-detail-date {
|
| 149 |
-
font-size: 0.
|
| 150 |
-
color: #
|
| 151 |
margin-bottom: 32px;
|
| 152 |
-
font-weight:
|
|
|
|
|
|
|
|
|
|
| 153 |
}
|
| 154 |
|
| 155 |
-
.memory-
|
| 156 |
-
font-size:
|
| 157 |
-
line-height: 1.
|
| 158 |
-
color: #
|
| 159 |
margin-bottom: 40px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
}
|
| 161 |
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
margin-bottom: 40px;
|
| 164 |
}
|
| 165 |
|
| 166 |
-
.
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
text-transform: uppercase;
|
| 169 |
letter-spacing: 0.1em;
|
| 170 |
-
color: #
|
| 171 |
-
margin-bottom:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
font-weight: 700;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
}
|
| 174 |
|
| 175 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
display: flex;
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
gap: 12px;
|
| 179 |
}
|
| 180 |
|
| 181 |
-
.
|
| 182 |
-
background
|
| 183 |
-
padding:
|
| 184 |
border-radius: 14px;
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
font-weight: 600;
|
| 188 |
-
|
| 189 |
}
|
| 190 |
|
| 191 |
-
.
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* MemoryExplorer.css - Scientific Instrument Aesthetic */
|
| 2 |
|
| 3 |
.memory-view-grid {
|
| 4 |
display: grid;
|
| 5 |
+
grid-template-columns: 320px 1fr;
|
| 6 |
+
gap: 24px;
|
| 7 |
+
height: calc(100vh - 140px);
|
| 8 |
+
padding: 0 24px 24px;
|
| 9 |
+
overflow: hidden;
|
| 10 |
}
|
| 11 |
|
| 12 |
.memory-surface {
|
| 13 |
display: flex;
|
| 14 |
flex-direction: column;
|
| 15 |
min-height: 0;
|
| 16 |
+
background: rgba(255, 255, 255, 0.4);
|
| 17 |
+
backdrop-filter: blur(20px);
|
| 18 |
+
border: 1px solid rgba(255, 255, 255, 0.5);
|
| 19 |
+
border-radius: 24px;
|
| 20 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
/* ── List Surface ── */
|
| 24 |
+
.list-surface {
|
| 25 |
+
padding: 20px;
|
| 26 |
}
|
| 27 |
|
|
|
|
| 28 |
.memory-toolbar {
|
| 29 |
display: flex;
|
| 30 |
+
flex-direction: column;
|
| 31 |
+
gap: 12px;
|
| 32 |
+
margin-bottom: 20px;
|
| 33 |
}
|
| 34 |
|
| 35 |
+
.memory-search-wrap {
|
| 36 |
+
background: white;
|
| 37 |
+
border: 1px solid rgba(0,0,0,0.08);
|
| 38 |
+
border-radius: 12px;
|
| 39 |
+
padding: 0 12px;
|
| 40 |
display: flex;
|
| 41 |
align-items: center;
|
| 42 |
+
gap: 10px;
|
| 43 |
+
height: 40px;
|
| 44 |
+
transition: border-color 0.2s;
|
| 45 |
}
|
| 46 |
|
| 47 |
+
.memory-search-wrap:focus-within {
|
| 48 |
+
border-color: var(--secondary);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.memory-search-wrap input {
|
| 52 |
flex: 1;
|
| 53 |
background: transparent;
|
| 54 |
border: none;
|
| 55 |
outline: none;
|
| 56 |
+
font-size: 0.8rem;
|
| 57 |
color: #1a1a1a;
|
| 58 |
font-weight: 500;
|
| 59 |
}
|
| 60 |
|
| 61 |
+
.memory-search-wrap .material-icons {
|
| 62 |
+
color: #bbb;
|
| 63 |
+
font-size: 18px;
|
| 64 |
}
|
| 65 |
|
| 66 |
+
/* ── Filter Tabs ── */
|
| 67 |
+
.filter-tabs {
|
|
|
|
|
|
|
| 68 |
display: flex;
|
| 69 |
+
gap: 6px;
|
| 70 |
+
background: rgba(0,0,0,0.03);
|
| 71 |
+
padding: 4px;
|
| 72 |
+
border-radius: 10px;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.filter-tab {
|
| 76 |
+
flex: 1;
|
| 77 |
+
font-size: 0.6rem;
|
| 78 |
+
font-weight: 800;
|
| 79 |
+
text-transform: uppercase;
|
| 80 |
+
padding: 6px 0;
|
| 81 |
+
border-radius: 8px;
|
| 82 |
+
color: #999;
|
| 83 |
+
transition: all 0.2s;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.filter-tab.active {
|
| 87 |
+
background: white;
|
| 88 |
color: #1a1a1a;
|
| 89 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
| 90 |
}
|
| 91 |
|
|
|
|
| 92 |
.memory-list {
|
| 93 |
flex: 1;
|
| 94 |
overflow-y: auto;
|
| 95 |
display: flex;
|
| 96 |
flex-direction: column;
|
| 97 |
+
gap: 8px;
|
| 98 |
+
padding-right: 4px;
|
| 99 |
+
margin-top: 12px;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/* ── Neighborhood Section ── */
|
| 103 |
+
.neighbor-card {
|
| 104 |
+
background: white;
|
| 105 |
+
padding: 14px;
|
| 106 |
+
border-radius: 14px;
|
| 107 |
+
border: 1px solid rgba(0,0,0,0.03);
|
| 108 |
+
display: flex;
|
| 109 |
+
flex-direction: column;
|
| 110 |
+
gap: 4px;
|
| 111 |
+
text-align: left;
|
| 112 |
+
cursor: pointer;
|
| 113 |
+
transition: transform 0.2s, box-shadow 0.2s;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.neighbor-card:hover {
|
| 117 |
+
transform: translateY(-2px);
|
| 118 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
| 119 |
+
border-color: var(--secondary);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/* ── Neural Insight Overlay ── */
|
| 123 |
+
.neural-insight-overlay {
|
| 124 |
+
position: absolute;
|
| 125 |
+
top: 0;
|
| 126 |
+
left: 0;
|
| 127 |
+
right: 0;
|
| 128 |
+
bottom: 0;
|
| 129 |
+
background: rgba(255, 255, 255, 0.4);
|
| 130 |
+
backdrop-filter: blur(10px);
|
| 131 |
+
z-index: 100;
|
| 132 |
+
display: flex;
|
| 133 |
+
align-items: center;
|
| 134 |
+
justify-content: center;
|
| 135 |
+
padding: 40px;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.neural-insight-modal {
|
| 139 |
+
background: white;
|
| 140 |
+
width: 100%;
|
| 141 |
+
max-width: 400px;
|
| 142 |
+
border-radius: 32px;
|
| 143 |
+
padding: 32px;
|
| 144 |
+
box-shadow: 0 30px 100px rgba(0,0,0,0.1);
|
| 145 |
+
border: 1px solid rgba(0,0,0,0.02);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.insight-header {
|
| 149 |
+
display: flex;
|
| 150 |
+
align-items: center;
|
| 151 |
gap: 12px;
|
| 152 |
+
margin-bottom: 24px;
|
| 153 |
}
|
| 154 |
|
| 155 |
+
.insight-header .material-icons {
|
| 156 |
+
color: var(--secondary);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.insight-header h3 {
|
| 160 |
+
font-family: var(--font-display);
|
| 161 |
+
font-size: 1.1rem;
|
| 162 |
+
flex: 1;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.insight-close {
|
| 166 |
+
color: #ccc;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.insight-body {
|
| 170 |
display: flex;
|
| 171 |
+
flex-direction: column;
|
| 172 |
gap: 20px;
|
| 173 |
+
margin-bottom: 32px;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.insight-stat {
|
| 177 |
+
display: flex;
|
| 178 |
+
flex-direction: column;
|
| 179 |
+
gap: 4px;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.insight-stat label {
|
| 183 |
+
font-size: 0.6rem;
|
| 184 |
+
text-transform: uppercase;
|
| 185 |
+
color: #bbb;
|
| 186 |
+
font-weight: 800;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.insight-stat strong {
|
| 190 |
+
font-family: 'Space Mono', monospace;
|
| 191 |
+
font-size: 1.1rem;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.insight-body p {
|
| 195 |
+
font-size: 0.8rem;
|
| 196 |
+
line-height: 1.6;
|
| 197 |
+
color: #666;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.insight-btn {
|
| 201 |
+
width: 100%;
|
| 202 |
+
padding: 12px;
|
| 203 |
+
background: #1a1a1a;
|
| 204 |
+
color: white;
|
| 205 |
+
border-radius: 12px;
|
| 206 |
+
font-size: 0.75rem;
|
| 207 |
+
font-weight: 700;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.memory-list::-webkit-scrollbar {
|
| 211 |
+
width: 4px;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.memory-list::-webkit-scrollbar-thumb {
|
| 215 |
+
background: rgba(0,0,0,0.1);
|
| 216 |
+
border-radius: 10px;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.memory-row {
|
| 220 |
+
display: flex;
|
| 221 |
+
align-items: center;
|
| 222 |
+
gap: 12px;
|
| 223 |
+
padding: 12px 16px;
|
| 224 |
+
border-radius: 12px;
|
| 225 |
text-align: left;
|
| 226 |
+
transition: all 0.2s ease;
|
| 227 |
border: 1px solid transparent;
|
| 228 |
+
background: transparent;
|
| 229 |
+
cursor: pointer;
|
| 230 |
}
|
| 231 |
|
| 232 |
.memory-row:hover {
|
| 233 |
+
background: rgba(255, 255, 255, 0.6);
|
| 234 |
}
|
| 235 |
|
| 236 |
.memory-row.active {
|
| 237 |
+
background: white;
|
| 238 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03);
|
| 239 |
+
border-color: rgba(0, 0, 0, 0.05);
|
|
|
|
| 240 |
}
|
| 241 |
|
| 242 |
.memory-dot {
|
| 243 |
+
width: 8px;
|
| 244 |
+
height: 8px;
|
| 245 |
border-radius: 50%;
|
|
|
|
| 246 |
flex-shrink: 0;
|
| 247 |
+
box-shadow: 0 0 8px rgba(0,0,0,0.1);
|
| 248 |
}
|
| 249 |
|
| 250 |
.memory-row-copy {
|
| 251 |
flex: 1;
|
| 252 |
+
min-width: 0;
|
| 253 |
}
|
| 254 |
|
| 255 |
.memory-row-copy strong {
|
| 256 |
display: block;
|
| 257 |
+
font-size: 0.8rem;
|
| 258 |
font-weight: 700;
|
| 259 |
color: #1a1a1a;
|
| 260 |
+
margin-bottom: 2px;
|
| 261 |
+
line-height: 1.2;
|
| 262 |
+
white-space: nowrap;
|
| 263 |
+
overflow: hidden;
|
| 264 |
+
text-overflow: ellipsis;
|
| 265 |
}
|
| 266 |
|
| 267 |
.memory-row-meta {
|
| 268 |
display: flex;
|
| 269 |
+
align-items: center;
|
| 270 |
+
gap: 6px;
|
| 271 |
+
font-size: 0.6rem;
|
| 272 |
+
color: #bbb;
|
| 273 |
+
font-weight: 700;
|
| 274 |
+
text-transform: uppercase;
|
| 275 |
}
|
| 276 |
|
| 277 |
+
/* ── Detail Surface ── */
|
| 278 |
+
.detail-surface {
|
| 279 |
+
padding: 40px;
|
| 280 |
+
overflow-y: auto;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.memory-header-meta {
|
| 284 |
+
display: flex;
|
| 285 |
+
justify-content: space-between;
|
| 286 |
+
align-items: center;
|
| 287 |
+
margin-bottom: 24px;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.memory-type-pill {
|
| 291 |
+
font-size: 0.6rem;
|
| 292 |
+
font-weight: 800;
|
| 293 |
text-transform: uppercase;
|
| 294 |
+
letter-spacing: 0.1em;
|
| 295 |
+
padding: 4px 12px;
|
| 296 |
+
border-radius: 100px;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.memory-id-badge {
|
| 300 |
+
font-family: 'Space Mono', monospace;
|
| 301 |
+
font-size: 0.6rem;
|
| 302 |
+
color: #999;
|
| 303 |
+
background: rgba(0,0,0,0.03);
|
| 304 |
+
padding: 4px 10px;
|
| 305 |
+
border-radius: 6px;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.purge-btn {
|
| 309 |
+
display: flex;
|
| 310 |
+
align-items: center;
|
| 311 |
+
gap: 4px;
|
| 312 |
+
padding: 2px 8px;
|
| 313 |
+
border-radius: 4px;
|
| 314 |
+
background: rgba(255, 107, 53, 0.08);
|
| 315 |
+
color: #ff6b35;
|
| 316 |
font-weight: 700;
|
| 317 |
+
text-transform: uppercase;
|
| 318 |
+
font-size: 0.55rem;
|
| 319 |
+
transition: all 0.2s;
|
| 320 |
+
border: 1px solid transparent;
|
| 321 |
}
|
| 322 |
|
| 323 |
+
.purge-btn:hover {
|
| 324 |
+
background: #ff6b35;
|
| 325 |
+
color: white;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
}
|
| 327 |
|
| 328 |
+
.purge-btn:disabled {
|
| 329 |
+
opacity: 0.5;
|
| 330 |
+
cursor: not-allowed;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.memory-detail-title {
|
| 334 |
font-family: var(--font-display);
|
| 335 |
+
font-size: 1.75rem;
|
| 336 |
+
font-weight: 800;
|
| 337 |
+
margin-bottom: 8px;
|
| 338 |
color: #1a1a1a;
|
| 339 |
+
letter-spacing: -0.02em;
|
| 340 |
}
|
| 341 |
|
| 342 |
.memory-detail-date {
|
| 343 |
+
font-size: 0.75rem;
|
| 344 |
+
color: #bbb;
|
| 345 |
margin-bottom: 32px;
|
| 346 |
+
font-weight: 600;
|
| 347 |
+
display: flex;
|
| 348 |
+
align-items: center;
|
| 349 |
+
gap: 6px;
|
| 350 |
}
|
| 351 |
|
| 352 |
+
.memory-content-box {
|
| 353 |
+
font-size: 0.95rem;
|
| 354 |
+
line-height: 1.6;
|
| 355 |
+
color: #333;
|
| 356 |
margin-bottom: 40px;
|
| 357 |
+
padding: 24px;
|
| 358 |
+
background: white;
|
| 359 |
+
border-radius: 20px;
|
| 360 |
+
border: 1px solid rgba(0,0,0,0.03);
|
| 361 |
+
white-space: pre-wrap;
|
| 362 |
+
box-shadow: 0 4px 20px rgba(0,0,0,0.01);
|
| 363 |
}
|
| 364 |
|
| 365 |
+
/* ── Instrument Grid ── */
|
| 366 |
+
.instrument-grid {
|
| 367 |
+
display: grid;
|
| 368 |
+
grid-template-columns: repeat(3, 1fr);
|
| 369 |
+
gap: 20px;
|
| 370 |
margin-bottom: 40px;
|
| 371 |
}
|
| 372 |
|
| 373 |
+
.instrument-card {
|
| 374 |
+
padding: 20px;
|
| 375 |
+
background: rgba(255, 255, 255, 0.5);
|
| 376 |
+
border: 1px solid rgba(0,0,0,0.02);
|
| 377 |
+
border-radius: 20px;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.instrument-card h4 {
|
| 381 |
+
font-size: 0.6rem;
|
| 382 |
text-transform: uppercase;
|
| 383 |
letter-spacing: 0.1em;
|
| 384 |
+
color: #aaa;
|
| 385 |
+
margin-bottom: 12px;
|
| 386 |
+
font-weight: 800;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.meter-track {
|
| 390 |
+
height: 6px;
|
| 391 |
+
background: rgba(0,0,0,0.05);
|
| 392 |
+
border-radius: 10px;
|
| 393 |
+
margin-bottom: 8px;
|
| 394 |
+
overflow: hidden;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.meter-fill {
|
| 398 |
+
height: 100%;
|
| 399 |
+
border-radius: 10px;
|
| 400 |
+
transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.metric-value {
|
| 404 |
+
font-family: 'Space Mono', monospace;
|
| 405 |
+
font-size: 0.9rem;
|
| 406 |
font-weight: 700;
|
| 407 |
+
color: #1a1a1a;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
/* ── Neighborhood ── */
|
| 411 |
+
.neighborhood-section {
|
| 412 |
+
margin-top: 40px;
|
| 413 |
}
|
| 414 |
|
| 415 |
+
.section-label {
|
| 416 |
+
font-size: 0.65rem;
|
| 417 |
+
text-transform: uppercase;
|
| 418 |
+
letter-spacing: 0.12em;
|
| 419 |
+
color: #aaa;
|
| 420 |
+
margin-bottom: 20px;
|
| 421 |
+
font-weight: 800;
|
| 422 |
display: flex;
|
| 423 |
+
align-items: center;
|
| 424 |
+
gap: 8px;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.neighborhood-grid {
|
| 428 |
+
display: grid;
|
| 429 |
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
| 430 |
gap: 12px;
|
| 431 |
}
|
| 432 |
|
| 433 |
+
.neighbor-card {
|
| 434 |
+
background: white;
|
| 435 |
+
padding: 14px;
|
| 436 |
border-radius: 14px;
|
| 437 |
+
border: 1px solid rgba(0,0,0,0.03);
|
| 438 |
+
display: flex;
|
| 439 |
+
flex-direction: column;
|
| 440 |
+
gap: 4px;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.neighbor-card strong {
|
| 444 |
+
font-size: 0.75rem;
|
| 445 |
+
color: #1a1a1a;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.neighbor-card span {
|
| 449 |
+
font-size: 0.6rem;
|
| 450 |
+
color: #bbb;
|
| 451 |
+
font-weight: 700;
|
| 452 |
+
text-transform: uppercase;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
/* ── Raw Metadata ── */
|
| 456 |
+
.metadata-grid {
|
| 457 |
+
display: grid;
|
| 458 |
+
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
| 459 |
+
gap: 8px;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
.metadata-tag {
|
| 463 |
+
background: rgba(0,0,0,0.02);
|
| 464 |
+
padding: 8px 12px;
|
| 465 |
+
border-radius: 10px;
|
| 466 |
+
border: 1px solid rgba(0,0,0,0.02);
|
| 467 |
+
display: flex;
|
| 468 |
+
flex-direction: column;
|
| 469 |
+
gap: 2px;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
.metadata-tag span:first-child {
|
| 473 |
+
font-size: 0.55rem;
|
| 474 |
+
color: #aaa;
|
| 475 |
+
text-transform: uppercase;
|
| 476 |
+
font-weight: 800;
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
.metadata-tag span:last-child {
|
| 480 |
+
font-size: 0.7rem;
|
| 481 |
+
color: #555;
|
| 482 |
font-weight: 600;
|
| 483 |
+
word-break: break-all;
|
| 484 |
}
|
| 485 |
|
| 486 |
+
.empty-detail {
|
| 487 |
+
display: flex;
|
| 488 |
+
flex-direction: column;
|
| 489 |
+
align-items: center;
|
| 490 |
+
justify-content: center;
|
| 491 |
+
height: 100%;
|
| 492 |
+
color: #ccc;
|
| 493 |
+
text-align: center;
|
| 494 |
+
gap: 12px;
|
| 495 |
}
|
| 496 |
+
|
| 497 |
+
.empty-detail .material-icons {
|
| 498 |
+
font-size: 48px;
|
| 499 |
+
opacity: 0.3;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
.pulse {
|
| 503 |
+
animation: pulse 2s infinite;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
@keyframes pulse {
|
| 507 |
+
0% { opacity: 0.4; }
|
| 508 |
+
50% { opacity: 1; }
|
| 509 |
+
100% { opacity: 0.4; }
|
| 510 |
+
}
|
| 511 |
+
|
frontend/src/components/MemoryExplorer.jsx
CHANGED
|
@@ -1,139 +1,340 @@
|
|
| 1 |
-
import { useState } from 'react';
|
|
|
|
| 2 |
import './MemoryExplorer.css';
|
| 3 |
|
| 4 |
-
const MEMORIES = [
|
| 5 |
-
{
|
| 6 |
-
id: '1',
|
| 7 |
-
title: 'Neural architecture trade-offs in transformer models',
|
| 8 |
-
type: 'conversation',
|
| 9 |
-
dot: '#3b82f6',
|
| 10 |
-
date: 'Today, 10:41 AM',
|
| 11 |
-
similarity: 0.92,
|
| 12 |
-
summary: 'We discussed how attention mechanisms enable parallelism but have quadratic complexity with sequence length. Sparse patterns, recurrence, and hybrid approaches help balance efficiency and expressivity.',
|
| 13 |
-
entities: ['Attention Mechanism', 'Long-range Dependencies', 'Transformer Models', 'Efficiency', 'Expressivity'],
|
| 14 |
-
source: { memories: 5, entities: 2 }
|
| 15 |
-
},
|
| 16 |
-
{
|
| 17 |
-
id: '2',
|
| 18 |
-
title: 'Attention mechanisms and long-range dependencies',
|
| 19 |
-
type: 'concept',
|
| 20 |
-
dot: '#ff6b35',
|
| 21 |
-
date: 'Today, 10:32 AM',
|
| 22 |
-
similarity: 0.89,
|
| 23 |
-
summary: 'Long-range dependencies benefit from attention, but scaling cost needs careful control for production systems.',
|
| 24 |
-
entities: ['Attention', 'Context Windows', 'Scalability'],
|
| 25 |
-
source: { memories: 3, entities: 1 }
|
| 26 |
-
},
|
| 27 |
-
{
|
| 28 |
-
id: '3',
|
| 29 |
-
title: 'Sparse attention patterns for efficient inference',
|
| 30 |
-
type: 'note',
|
| 31 |
-
dot: '#10b981',
|
| 32 |
-
date: 'Yesterday, 08:15 PM',
|
| 33 |
-
similarity: 0.87,
|
| 34 |
-
summary: 'Sparse routing lowers compute while preserving useful global pathways for key tokens.',
|
| 35 |
-
entities: ['Sparse Attention', 'Inference', 'Efficiency'],
|
| 36 |
-
source: { memories: 2, entities: 1 }
|
| 37 |
-
},
|
| 38 |
-
{
|
| 39 |
-
id: '4',
|
| 40 |
-
title: 'Depth vs width scaling laws in neural networks',
|
| 41 |
-
type: 'note',
|
| 42 |
-
dot: '#10b981',
|
| 43 |
-
date: 'Yesterday, 07:42 PM',
|
| 44 |
-
similarity: 0.85,
|
| 45 |
-
summary: 'Model depth and width trade off optimization stability, memory, and generalization.',
|
| 46 |
-
entities: ['Scaling Laws', 'Depth', 'Width'],
|
| 47 |
-
source: { memories: 4, entities: 3 }
|
| 48 |
-
},
|
| 49 |
-
{
|
| 50 |
-
id: '5',
|
| 51 |
-
title: 'Positional encoding variants and their impacts',
|
| 52 |
-
type: 'concept',
|
| 53 |
-
dot: '#ff6b35',
|
| 54 |
-
date: 'Yesterday, 06:11 PM',
|
| 55 |
-
similarity: 0.80,
|
| 56 |
-
summary: 'Rotary, learned, and relative position schemes each affect extrapolation and training behavior differently.',
|
| 57 |
-
entities: ['RoPE', 'Relative Position', 'Learned Embeddings'],
|
| 58 |
-
source: { memories: 6, entities: 2 }
|
| 59 |
-
},
|
| 60 |
-
];
|
| 61 |
-
|
| 62 |
function MemoryExplorer() {
|
| 63 |
const [search, setSearch] = useState('');
|
| 64 |
-
const [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
);
|
| 69 |
|
| 70 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
return (
|
| 73 |
<div className="memory-view-grid fade-in">
|
|
|
|
| 74 |
<section className="memory-surface list-surface">
|
| 75 |
<div className="memory-toolbar">
|
| 76 |
-
<
|
| 77 |
-
<span className="material-icons">search</span>
|
| 78 |
<input
|
| 79 |
type="text"
|
| 80 |
value={search}
|
| 81 |
onChange={(e) => setSearch(e.target.value)}
|
| 82 |
-
placeholder="
|
| 83 |
/>
|
| 84 |
-
</
|
| 85 |
-
<
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
</div>
|
| 90 |
|
| 91 |
<div className="memory-list">
|
| 92 |
-
{
|
| 93 |
-
<
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
<
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
</div>
|
| 107 |
-
<span className="memory-tag">{memory.type}</span>
|
| 108 |
-
</button>
|
| 109 |
-
))}
|
| 110 |
-
</div>
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
</div>
|
| 127 |
-
</div>
|
| 128 |
|
| 129 |
-
|
| 130 |
-
<
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
</div>
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
</section>
|
| 138 |
</div>
|
| 139 |
);
|
|
|
|
| 1 |
+
import { useState, useEffect, useCallback, useMemo } from 'react';
|
| 2 |
+
import { apiFetch } from '../api';
|
| 3 |
import './MemoryExplorer.css';
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
function MemoryExplorer() {
|
| 6 |
const [search, setSearch] = useState('');
|
| 7 |
+
const [memories, setMemories] = useState([]);
|
| 8 |
+
const [graphData, setGraphData] = useState({ nodes: [], edges: [] });
|
| 9 |
+
const [selectedId, setSelectedId] = useState(null);
|
| 10 |
+
const [isLoading, setIsLoading] = useState(true);
|
| 11 |
+
const [isSearching, setIsSearching] = useState(false);
|
| 12 |
+
const [isDeleting, setIsDeleting] = useState(false);
|
| 13 |
+
const [activeFilter, setActiveFilter] = useState('all');
|
| 14 |
+
const [insightEntity, setInsightEntity] = useState(null);
|
| 15 |
+
|
| 16 |
+
const fetchData = useCallback(async (query = '') => {
|
| 17 |
+
const isSearch = query.trim().length > 0;
|
| 18 |
+
if (isSearch) setIsSearching(true);
|
| 19 |
+
else setIsLoading(true);
|
| 20 |
+
|
| 21 |
+
try {
|
| 22 |
+
const memoryUrl = isSearch
|
| 23 |
+
? `/api/v1/memory/search?q=${encodeURIComponent(query)}`
|
| 24 |
+
: '/api/v1/memory/sensory';
|
| 25 |
+
|
| 26 |
+
const [memRes, graphRes] = await Promise.all([
|
| 27 |
+
apiFetch(memoryUrl),
|
| 28 |
+
apiFetch('/api/v1/graph')
|
| 29 |
+
]);
|
| 30 |
+
|
| 31 |
+
if (memRes.ok) {
|
| 32 |
+
const data = await memRes.json();
|
| 33 |
+
const results = data.memories || [];
|
| 34 |
+
setMemories(results);
|
| 35 |
+
|
| 36 |
+
if (results.length > 0) {
|
| 37 |
+
if (!selectedId || !results.find(m => m.id === selectedId)) {
|
| 38 |
+
setSelectedId(results[0].id);
|
| 39 |
+
}
|
| 40 |
+
} else {
|
| 41 |
+
setSelectedId(null);
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
if (graphRes.ok) {
|
| 46 |
+
const gData = await graphRes.json();
|
| 47 |
+
setGraphData(gData);
|
| 48 |
+
}
|
| 49 |
+
} catch (error) {
|
| 50 |
+
console.error('Neural synchronization failure', error);
|
| 51 |
+
} finally {
|
| 52 |
+
setIsLoading(false);
|
| 53 |
+
setIsSearching(false);
|
| 54 |
+
}
|
| 55 |
+
}, [selectedId]);
|
| 56 |
+
|
| 57 |
+
const handlePurge = async (id) => {
|
| 58 |
+
if (!window.confirm("Are you sure you want to purge this memory chunk from the neural cortex? This action is irreversible.")) return;
|
| 59 |
+
|
| 60 |
+
setIsDeleting(true);
|
| 61 |
+
try {
|
| 62 |
+
const res = await apiFetch(`/api/v1/memory/${id}`, { method: 'DELETE' });
|
| 63 |
+
if (res.ok) {
|
| 64 |
+
setMemories(prev => prev.filter(m => m.id !== id));
|
| 65 |
+
setSelectedId(null);
|
| 66 |
+
fetchData(search);
|
| 67 |
+
}
|
| 68 |
+
} catch (error) {
|
| 69 |
+
console.error('Purge failed', error);
|
| 70 |
+
} finally {
|
| 71 |
+
setIsDeleting(false);
|
| 72 |
+
}
|
| 73 |
+
};
|
| 74 |
|
| 75 |
+
useEffect(() => {
|
| 76 |
+
fetchData();
|
| 77 |
+
}, []);
|
| 78 |
+
|
| 79 |
+
useEffect(() => {
|
| 80 |
+
const timer = setTimeout(() => {
|
| 81 |
+
fetchData(search);
|
| 82 |
+
}, 500);
|
| 83 |
+
return () => clearTimeout(timer);
|
| 84 |
+
}, [search]);
|
| 85 |
+
|
| 86 |
+
const filteredMemories = useMemo(() => {
|
| 87 |
+
if (activeFilter === 'all') return memories;
|
| 88 |
+
return memories.filter(m => {
|
| 89 |
+
const type = m.metadata?.type;
|
| 90 |
+
if (activeFilter === 'episodic') return type === 'conversation' || type === 'chat_exchange';
|
| 91 |
+
if (activeFilter === 'semantic') return type === 'concept' || type === 'note';
|
| 92 |
+
if (activeFilter === 'sleep') return type === 'sleep_summary';
|
| 93 |
+
return true;
|
| 94 |
+
});
|
| 95 |
+
}, [memories, activeFilter]);
|
| 96 |
+
|
| 97 |
+
const selectedMemory = useMemo(() =>
|
| 98 |
+
memories.find(m => m.id === selectedId) || memories[0],
|
| 99 |
+
[memories, selectedId]
|
| 100 |
);
|
| 101 |
|
| 102 |
+
const linkedEntities = useMemo(() => {
|
| 103 |
+
if (!selectedMemory || !graphData.nodes) return [];
|
| 104 |
+
const content = selectedMemory.content.toLowerCase();
|
| 105 |
+
return graphData.nodes.filter(node =>
|
| 106 |
+
content.includes(node.id.toLowerCase()) ||
|
| 107 |
+
(node.label && content.includes(node.label.toLowerCase()))
|
| 108 |
+
);
|
| 109 |
+
}, [selectedMemory, graphData]);
|
| 110 |
+
|
| 111 |
+
const getTitle = (content) => {
|
| 112 |
+
if (!content) return 'Untitled Memory Chunks';
|
| 113 |
+
const lines = content.split('\n');
|
| 114 |
+
const firstLine = lines[0].trim();
|
| 115 |
+
if (firstLine.length > 40) return firstLine.substring(0, 37) + '...';
|
| 116 |
+
return firstLine || 'Untitled Memory Chunks';
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
const getMemoryTheme = (type) => {
|
| 120 |
+
switch (type) {
|
| 121 |
+
case 'conversation': return { color: '#3b82f6', label: 'Episodic' };
|
| 122 |
+
case 'chat_exchange': return { color: '#8b5cf6', label: 'Social' };
|
| 123 |
+
case 'concept': return { color: '#ff6b35', label: 'Semantic' };
|
| 124 |
+
case 'note': return { color: '#10b981', label: 'Explicit' };
|
| 125 |
+
case 'sleep_summary': return { color: '#f59e0b', label: 'Consolidated' };
|
| 126 |
+
default: return { color: '#94a3b8', label: 'Raw Sensory' };
|
| 127 |
+
}
|
| 128 |
+
};
|
| 129 |
|
| 130 |
return (
|
| 131 |
<div className="memory-view-grid fade-in">
|
| 132 |
+
{/* ── SIDEBAR: Memory Feed ── */}
|
| 133 |
<section className="memory-surface list-surface">
|
| 134 |
<div className="memory-toolbar">
|
| 135 |
+
<div className="memory-search-wrap">
|
| 136 |
+
<span className="material-icons">{isSearching ? 'sync' : 'search'}</span>
|
| 137 |
<input
|
| 138 |
type="text"
|
| 139 |
value={search}
|
| 140 |
onChange={(e) => setSearch(e.target.value)}
|
| 141 |
+
placeholder="Query sensory cortex..."
|
| 142 |
/>
|
| 143 |
+
</div>
|
| 144 |
+
<div className="filter-tabs">
|
| 145 |
+
{['all', 'episodic', 'semantic', 'sleep'].map(f => (
|
| 146 |
+
<button
|
| 147 |
+
key={f}
|
| 148 |
+
className={`filter-tab ${activeFilter === f ? 'active' : ''}`}
|
| 149 |
+
onClick={() => setActiveFilter(f)}
|
| 150 |
+
>
|
| 151 |
+
{f}
|
| 152 |
+
</button>
|
| 153 |
+
))}
|
| 154 |
+
</div>
|
| 155 |
+
<div className="density-stats">
|
| 156 |
+
<span className="material-icons" style={{fontSize: '14px'}}>dns</span>
|
| 157 |
+
<span>{filteredMemories.length} Active Nodes</span>
|
| 158 |
+
</div>
|
| 159 |
</div>
|
| 160 |
|
| 161 |
<div className="memory-list">
|
| 162 |
+
{isLoading ? (
|
| 163 |
+
<div className="empty-detail">
|
| 164 |
+
<span className="material-icons pulse">settings_input_component</span>
|
| 165 |
+
<p style={{fontSize: '0.7rem'}}>Calibrating neural paths...</p>
|
| 166 |
+
</div>
|
| 167 |
+
) : (
|
| 168 |
+
filteredMemories.map((memory) => {
|
| 169 |
+
const theme = getMemoryTheme(memory.metadata?.type);
|
| 170 |
+
return (
|
| 171 |
+
<button
|
| 172 |
+
key={memory.id}
|
| 173 |
+
className={`memory-row ${memory.id === selectedId ? 'active' : ''}`}
|
| 174 |
+
onClick={() => setSelectedId(memory.id)}
|
| 175 |
+
>
|
| 176 |
+
<div
|
| 177 |
+
className="memory-dot"
|
| 178 |
+
style={{ backgroundColor: theme.color }}
|
| 179 |
+
/>
|
| 180 |
+
<div className="memory-row-copy">
|
| 181 |
+
<strong>{getTitle(memory.content)}</strong>
|
| 182 |
+
<div className="memory-row-meta">
|
| 183 |
+
<span style={{ color: theme.color }}>{theme.label}</span>
|
| 184 |
+
<span>•</span>
|
| 185 |
+
<span>{memory.id.substring(0, 8)}</span>
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
</button>
|
| 189 |
+
);
|
| 190 |
+
})
|
| 191 |
+
)}
|
| 192 |
+
</div>
|
| 193 |
+
</section>
|
| 194 |
+
|
| 195 |
+
{/* ── MAIN: Diagnostic Detail ── */}
|
| 196 |
+
<section className="memory-surface detail-surface" style={{position: 'relative'}}>
|
| 197 |
+
{selectedMemory ? (
|
| 198 |
+
<div className="fade-in">
|
| 199 |
+
<div className="memory-header-meta">
|
| 200 |
+
<div className="memory-type-pill" style={{
|
| 201 |
+
color: getMemoryTheme(selectedMemory.metadata?.type).color,
|
| 202 |
+
background: `${getMemoryTheme(selectedMemory.metadata?.type).color}15`
|
| 203 |
+
}}>
|
| 204 |
+
Layer: {getMemoryTheme(selectedMemory.metadata?.type).label}
|
| 205 |
+
</div>
|
| 206 |
+
<div className="memory-id-badge" style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
| 207 |
+
<span>UUID: {selectedMemory.id}</span>
|
| 208 |
+
<button
|
| 209 |
+
className="purge-btn"
|
| 210 |
+
onClick={() => handlePurge(selectedMemory.id)}
|
| 211 |
+
disabled={isDeleting}
|
| 212 |
+
>
|
| 213 |
+
<span className="material-icons" style={{fontSize: '14px'}}>{isDeleting ? 'sync' : 'delete_forever'}</span>
|
| 214 |
+
Purge
|
| 215 |
+
</button>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
|
| 219 |
+
<h1 className="memory-detail-title">{getTitle(selectedMemory.content)}</h1>
|
| 220 |
+
<p className="memory-detail-date">
|
| 221 |
+
<span className="material-icons" style={{fontSize: '14px'}}>event</span>
|
| 222 |
+
Encoded: {selectedMemory.metadata?.timestamp ? new Date(selectedMemory.metadata.timestamp).toLocaleString() : 'System Boot Sequence'}
|
| 223 |
+
</p>
|
| 224 |
+
|
| 225 |
+
<div className="memory-content-box">
|
| 226 |
+
{selectedMemory.content}
|
| 227 |
+
</div>
|
| 228 |
+
|
| 229 |
+
{/* Instrument Metrics */}
|
| 230 |
+
<div className="instrument-grid">
|
| 231 |
+
<div className="instrument-card">
|
| 232 |
+
<h4>Retrieval Salience</h4>
|
| 233 |
+
<div className="meter-track">
|
| 234 |
+
<div className="meter-fill" style={{
|
| 235 |
+
width: `${Math.round((selectedMemory.similarity || 0.85) * 100)}%`,
|
| 236 |
+
backgroundColor: getMemoryTheme(selectedMemory.metadata?.type).color
|
| 237 |
+
}} />
|
| 238 |
</div>
|
| 239 |
+
<div className="metric-value">{(selectedMemory.similarity || 0.85).toFixed(4)}</div>
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
<div className="instrument-card">
|
| 243 |
+
<h4>Graph Associations</h4>
|
| 244 |
+
<div className="meter-track">
|
| 245 |
+
<div className="meter-fill" style={{
|
| 246 |
+
width: `${Math.min(linkedEntities.length * 20, 100)}%`,
|
| 247 |
+
backgroundColor: '#ff6b35'
|
| 248 |
+
}} />
|
| 249 |
+
</div>
|
| 250 |
+
<div className="metric-value">{linkedEntities.length} Links</div>
|
| 251 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
+
<div className="instrument-card">
|
| 254 |
+
<h4>Entropy Score</h4>
|
| 255 |
+
<div className="meter-track">
|
| 256 |
+
<div className="meter-fill" style={{
|
| 257 |
+
width: '42%',
|
| 258 |
+
backgroundColor: '#10b981'
|
| 259 |
+
}} />
|
| 260 |
+
</div>
|
| 261 |
+
<div className="metric-value">0.4281</div>
|
| 262 |
+
</div>
|
| 263 |
+
</div>
|
| 264 |
|
| 265 |
+
{/* Knowledge Links */}
|
| 266 |
+
<div className="neighborhood-section">
|
| 267 |
+
<div className="section-label">
|
| 268 |
+
<span className="material-icons" style={{fontSize: '16px'}}>account_tree</span>
|
| 269 |
+
Knowledge Graph Intersections (Click for Insights)
|
| 270 |
+
</div>
|
| 271 |
+
<div className="neighborhood-grid">
|
| 272 |
+
{linkedEntities.map(node => (
|
| 273 |
+
<button
|
| 274 |
+
key={node.id}
|
| 275 |
+
className="neighbor-card"
|
| 276 |
+
onClick={() => setInsightEntity(node)}
|
| 277 |
+
>
|
| 278 |
+
<strong>{node.id}</strong>
|
| 279 |
+
<span>Connections: {node.connections || 0}</span>
|
| 280 |
+
</button>
|
| 281 |
+
))}
|
| 282 |
+
{linkedEntities.length === 0 && (
|
| 283 |
+
<p style={{fontSize: '0.75rem', color: '#999', fontStyle: 'italic'}}>No direct semantic associations detected in current graph state.</p>
|
| 284 |
+
)}
|
| 285 |
+
</div>
|
| 286 |
</div>
|
|
|
|
| 287 |
|
| 288 |
+
{/* Raw Metadata */}
|
| 289 |
+
<div className="neighborhood-section">
|
| 290 |
+
<div className="section-label">
|
| 291 |
+
<span className="material-icons" style={{fontSize: '16px'}}>terminal</span>
|
| 292 |
+
Raw Metadata Explorer
|
| 293 |
+
</div>
|
| 294 |
+
<div className="metadata-grid">
|
| 295 |
+
{Object.entries(selectedMemory.metadata || {}).map(([key, val]) => (
|
| 296 |
+
<div key={key} className="metadata-tag">
|
| 297 |
+
<span>{key}</span>
|
| 298 |
+
<span>{String(val)}</span>
|
| 299 |
+
</div>
|
| 300 |
+
))}
|
| 301 |
+
</div>
|
| 302 |
</div>
|
| 303 |
+
|
| 304 |
+
{/* Neural Insight Overlay */}
|
| 305 |
+
{insightEntity && (
|
| 306 |
+
<div className="neural-insight-overlay fade-in">
|
| 307 |
+
<div className="neural-insight-modal">
|
| 308 |
+
<div className="insight-header">
|
| 309 |
+
<span className="material-icons">psychology</span>
|
| 310 |
+
<h3>Neural Insight: {insightEntity.id}</h3>
|
| 311 |
+
<button className="insight-close" onClick={() => setInsightEntity(null)}>
|
| 312 |
+
<span className="material-icons">close</span>
|
| 313 |
+
</button>
|
| 314 |
+
</div>
|
| 315 |
+
<div className="insight-body">
|
| 316 |
+
<div className="insight-stat">
|
| 317 |
+
<label>Semantic Weight</label>
|
| 318 |
+
<strong>{insightEntity.connections * 12.5} nm</strong>
|
| 319 |
+
</div>
|
| 320 |
+
<div className="insight-stat">
|
| 321 |
+
<label>Active Connections</label>
|
| 322 |
+
<strong>{insightEntity.connections} Nodes</strong>
|
| 323 |
+
</div>
|
| 324 |
+
<p>This entity is a core semantic node within your current knowledge base. It facilitates retrieval of related episodic contexts and strengthens the reasoning cortex.</p>
|
| 325 |
+
</div>
|
| 326 |
+
<button className="insight-btn" onClick={() => setInsightEntity(null)}>Close Diagnostic</button>
|
| 327 |
+
</div>
|
| 328 |
+
</div>
|
| 329 |
+
)}
|
| 330 |
</div>
|
| 331 |
+
) : (
|
| 332 |
+
<div className="empty-detail">
|
| 333 |
+
<span className="material-icons pulse">sensors</span>
|
| 334 |
+
<h3>No Active Focus</h3>
|
| 335 |
+
<p style={{maxWidth: '280px', margin: '0 auto', fontSize: '0.85rem'}}>Select a sensory chunk from the feed to begin deep-layer diagnostic analysis.</p>
|
| 336 |
+
</div>
|
| 337 |
+
)}
|
| 338 |
</section>
|
| 339 |
</div>
|
| 340 |
);
|
frontend/src/index.css
CHANGED
|
@@ -34,8 +34,12 @@ body,
|
|
| 34 |
|
| 35 |
body {
|
| 36 |
font-family: var(--font-body);
|
| 37 |
-
background:
|
|
|
|
|
|
|
|
|
|
| 38 |
color: var(--text-main);
|
|
|
|
| 39 |
}
|
| 40 |
|
| 41 |
button,
|
|
@@ -69,3 +73,645 @@ img {
|
|
| 69 |
direction: ltr;
|
| 70 |
-webkit-font-feature-settings: 'liga';
|
| 71 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
body {
|
| 36 |
font-family: var(--font-body);
|
| 37 |
+
background:
|
| 38 |
+
radial-gradient(circle at top left, rgba(255, 255, 255, 0.9), transparent 50%),
|
| 39 |
+
radial-gradient(circle at bottom right, rgba(255, 107, 53, 0.08), transparent 50%),
|
| 40 |
+
#f2f2f2;
|
| 41 |
color: var(--text-main);
|
| 42 |
+
overflow: hidden;
|
| 43 |
}
|
| 44 |
|
| 45 |
button,
|
|
|
|
| 73 |
direction: ltr;
|
| 74 |
-webkit-font-feature-settings: 'liga';
|
| 75 |
}
|
| 76 |
+
|
| 77 |
+
/* ── SOMA Premium Glassmorphic Dark Theme ── */
|
| 78 |
+
body.dark-theme {
|
| 79 |
+
--bg-app: #000000;
|
| 80 |
+
--bg-sidebar: #050505;
|
| 81 |
+
--bg-card: rgba(18, 18, 18, 0.75);
|
| 82 |
+
--primary: #f8fafc;
|
| 83 |
+
--secondary: #ff7d4d;
|
| 84 |
+
--accent: #ff7d4d;
|
| 85 |
+
--text-main: #f8fafc;
|
| 86 |
+
--text-muted: #a3a3a3; /* Neutral light grey, no blue/slate tint */
|
| 87 |
+
--text-light: #737373; /* Neutral dark grey, no blue/slate tint */
|
| 88 |
+
--border-card: 1px solid rgba(255, 255, 255, 0.06);
|
| 89 |
+
--shadow-premium: 0 40px 100px rgba(0, 0, 0, 0.6);
|
| 90 |
+
|
| 91 |
+
background:
|
| 92 |
+
radial-gradient(circle at top left, rgba(255, 107, 53, 0.04), transparent 50%),
|
| 93 |
+
radial-gradient(circle at bottom right, rgba(255, 255, 255, 0.015), transparent 50%),
|
| 94 |
+
#000000;
|
| 95 |
+
color: var(--text-main);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/* Header & Sidebar Titles */
|
| 99 |
+
body.dark-theme .page-header h2 {
|
| 100 |
+
color: #f8fafc !important;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
body.dark-theme .brand-copy h1 {
|
| 104 |
+
color: #f8fafc !important;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/* Brain Ambient Node Labels Contrast Correction */
|
| 108 |
+
body.dark-theme .signal-copy strong {
|
| 109 |
+
color: #e2e8f0 !important;
|
| 110 |
+
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
body.dark-theme .signal-node {
|
| 114 |
+
opacity: 0.38 !important; /* Raised from 0.2 to prevent faintness */
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
body.dark-theme .signal-node.active,
|
| 118 |
+
body.dark-theme .signal-node.hot {
|
| 119 |
+
opacity: 1 !important;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
body.dark-theme .signal-dot {
|
| 123 |
+
background-color: #475569 !important;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/* Timeline & Feed Overrides */
|
| 127 |
+
body.dark-theme .phase-title {
|
| 128 |
+
color: #f8fafc !important;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
body.dark-theme .phase-desc {
|
| 132 |
+
color: #a3a3a3 !important;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
body.dark-theme .timeline-row::before {
|
| 136 |
+
background-color: rgba(255, 255, 255, 0.06) !important;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
body.dark-theme .timeline-dot {
|
| 140 |
+
box-shadow: 0 0 0 4px #050505 !important;
|
| 141 |
+
background-color: #ff6b35 !important; /* Active accent node dot */
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
body.dark-theme .timeline-empty {
|
| 145 |
+
color: #737373 !important;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/* Chat bubble dark mode overrides */
|
| 149 |
+
body.dark-theme .bubble-body {
|
| 150 |
+
background-color: rgba(24, 24, 24, 0.75) !important; /* Pure neutral dark-grey glass bubble, no navy blue */
|
| 151 |
+
color: #f8fafc !important;
|
| 152 |
+
border: 1px solid rgba(255, 255, 255, 0.04) !important;
|
| 153 |
+
box-shadow: 0 4px 20px rgba(0,0,0,0.2) !important;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
body.dark-theme .chat-bubble-group.user .bubble-body {
|
| 157 |
+
background-color: rgba(28, 28, 28, 0.85) !important; /* Elegant neutral deep charcoal, completely mud-free */
|
| 158 |
+
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
| 159 |
+
color: #f8fafc !important;
|
| 160 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25) !important;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
body.dark-theme .bubble-text {
|
| 164 |
+
color: #e5e5e5 !important;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
body.dark-theme .bubble-meta strong {
|
| 168 |
+
color: #e5e5e5 !important;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
body.dark-theme .chat-bubble-group.soma .bubble-meta strong {
|
| 172 |
+
color: #ff8b54 !important; /* Elegant warm orange highlight for Soma in pure dark mode */
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/* Input elements dark mode overrides */
|
| 176 |
+
body.dark-theme .input-field-wrap {
|
| 177 |
+
background-color: rgba(24, 24, 24, 0.75) !important;
|
| 178 |
+
border: 1px solid rgba(255, 255, 255, 0.05) !important;
|
| 179 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.15) !important;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
body.dark-theme .input-field-wrap input {
|
| 183 |
+
color: #f8fafc !important;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
body.dark-theme .new-chat-btn {
|
| 187 |
+
background-color: rgba(24, 24, 24, 0.75) !important;
|
| 188 |
+
border: 1px solid rgba(255, 255, 255, 0.05) !important;
|
| 189 |
+
color: #f8fafc !important;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
body.dark-theme .new-chat-btn:hover {
|
| 193 |
+
background-color: rgba(255, 107, 53, 0.1) !important;
|
| 194 |
+
color: #ff6b35 !important;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
body.dark-theme .suggested-starters button {
|
| 198 |
+
background-color: rgba(24, 24, 24, 0.5) !important;
|
| 199 |
+
border: 1px solid rgba(255, 255, 255, 0.05) !important;
|
| 200 |
+
color: #e5e5e5 !important;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
body.dark-theme .suggested-starters button:hover {
|
| 204 |
+
background-color: rgba(255, 107, 53, 0.1) !important;
|
| 205 |
+
color: #ff7d4d !important;
|
| 206 |
+
border-color: rgba(255, 107, 53, 0.3) !important;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
body.dark-theme .welcome-icon {
|
| 210 |
+
background-color: rgba(24, 24, 24, 0.75) !important;
|
| 211 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.3) !important;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
body.dark-theme .welcome-icon span {
|
| 215 |
+
color: #f8fafc !important;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
body.dark-theme .welcome-state h3 {
|
| 219 |
+
color: #f8fafc !important;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/* Sidebar overrides */
|
| 223 |
+
body.dark-theme .session-card {
|
| 224 |
+
background-color: rgba(18, 18, 18, 0.6) !important;
|
| 225 |
+
border: 1px solid rgba(255, 255, 255, 0.03) !important;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
body.dark-theme .session-avatar {
|
| 229 |
+
border-color: rgba(255, 255, 255, 0.1) !important;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
body.dark-theme .session-copy strong {
|
| 233 |
+
color: #f8fafc !important;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
body.dark-theme .sidebar-link {
|
| 237 |
+
color: var(--text-muted);
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
body.dark-theme .sidebar-link:hover {
|
| 241 |
+
color: var(--text-main);
|
| 242 |
+
background-color: rgba(255, 255, 255, 0.03);
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
body.dark-theme .sidebar-link.active {
|
| 246 |
+
background-color: rgba(255, 255, 255, 0.03) !important;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
/* Memory & Dashboard dynamic panels */
|
| 250 |
+
body.dark-theme .dashboard-metric-card,
|
| 251 |
+
body.dark-theme .memory-list-card,
|
| 252 |
+
body.dark-theme .memory-detail-card,
|
| 253 |
+
body.dark-theme .knowledge-card {
|
| 254 |
+
background-color: rgba(18, 18, 18, 0.75) !important;
|
| 255 |
+
border: 1px solid rgba(255, 255, 255, 0.04) !important;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
body.dark-theme .timeline-node {
|
| 259 |
+
background-color: rgba(24, 24, 24, 0.75) !important;
|
| 260 |
+
border: 1px solid rgba(255, 255, 255, 0.03) !important;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
body.dark-theme .sleep-telemetry-card {
|
| 264 |
+
background-color: rgba(18, 18, 18, 0.75) !important;
|
| 265 |
+
border: 1px solid rgba(255, 255, 255, 0.04) !important;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
body.dark-theme .summary-card-combined,
|
| 269 |
+
body.dark-theme .summary-card-inline {
|
| 270 |
+
background-color: rgba(18, 18, 18, 0.85) !important;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
body.dark-theme .telemetry-card-value {
|
| 274 |
+
color: #f8fafc !important;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
/* Graph online indicator */
|
| 278 |
+
body.dark-theme .graph-container-glow {
|
| 279 |
+
border-color: rgba(255, 255, 255, 0.04) !important;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
body.dark-theme .input-area-card {
|
| 283 |
+
background-color: rgba(18, 18, 18, 0.75) !important;
|
| 284 |
+
border: 1px solid rgba(255, 255, 255, 0.04) !important;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
body.dark-theme .text-input-field {
|
| 288 |
+
background-color: rgba(24, 24, 24, 0.75) !important;
|
| 289 |
+
border: 1px solid rgba(255, 255, 255, 0.05) !important;
|
| 290 |
+
color: #f8fafc !important;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
body.dark-theme .theme-toggle-btn {
|
| 294 |
+
border-top-color: rgba(255, 255, 255, 0.08) !important;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
/* ── SOMA Memory Explorer Dark UI Overrides ── */
|
| 298 |
+
body.dark-theme .memory-surface {
|
| 299 |
+
background: rgba(18, 18, 18, 0.75) !important;
|
| 300 |
+
border: 1px solid rgba(255, 255, 255, 0.06) !important;
|
| 301 |
+
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55) !important;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
body.dark-theme .memory-search-wrap {
|
| 305 |
+
background-color: rgba(24, 24, 24, 0.75) !important;
|
| 306 |
+
border: 1px solid rgba(255, 255, 255, 0.05) !important;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
body.dark-theme .memory-search-wrap input {
|
| 310 |
+
color: #f8fafc !important;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
body.dark-theme .filter-tabs {
|
| 314 |
+
background-color: rgba(0, 0, 0, 0.2) !important;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
body.dark-theme .filter-tab {
|
| 318 |
+
color: #737373 !important;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
body.dark-theme .filter-tab.active {
|
| 322 |
+
background-color: rgba(255, 255, 255, 0.08) !important;
|
| 323 |
+
color: #f8fafc !important;
|
| 324 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
body.dark-theme .memory-row {
|
| 328 |
+
color: #a3a3a3 !important;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
body.dark-theme .memory-row:hover {
|
| 332 |
+
background-color: rgba(255, 255, 255, 0.03) !important;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
body.dark-theme .memory-row.active {
|
| 336 |
+
background-color: rgba(255, 255, 255, 0.05) !important;
|
| 337 |
+
border-color: rgba(255, 255, 255, 0.08) !important;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
body.dark-theme .memory-row-copy strong {
|
| 341 |
+
color: #f8fafc !important;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
body.dark-theme .memory-row-meta {
|
| 345 |
+
color: #737373 !important;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
body.dark-theme .memory-id-badge {
|
| 349 |
+
background-color: rgba(255, 255, 255, 0.04) !important;
|
| 350 |
+
color: #a3a3a3 !important;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
body.dark-theme .memory-detail-title {
|
| 354 |
+
color: #f8fafc !important;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
body.dark-theme .memory-content-box {
|
| 358 |
+
background-color: rgba(24, 24, 24, 0.6) !important;
|
| 359 |
+
border: 1px solid rgba(255, 255, 255, 0.04) !important;
|
| 360 |
+
color: #e5e5e5 !important;
|
| 361 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2) !important;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
body.dark-theme .instrument-card {
|
| 365 |
+
background-color: rgba(24, 24, 24, 0.5) !important;
|
| 366 |
+
border: 1px solid rgba(255, 255, 255, 0.04) !important;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
body.dark-theme .instrument-card h4 {
|
| 370 |
+
color: #737373 !important;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
body.dark-theme .meter-track {
|
| 374 |
+
background-color: rgba(255, 255, 255, 0.06) !important;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
body.dark-theme .metric-value {
|
| 378 |
+
color: #f8fafc !important;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
body.dark-theme .neighbor-card {
|
| 382 |
+
background-color: rgba(24, 24, 24, 0.5) !important;
|
| 383 |
+
border: 1px solid rgba(255, 255, 255, 0.04) !important;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
body.dark-theme .neighbor-card strong {
|
| 387 |
+
color: #e2e8f0 !important;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
body.dark-theme .neighbor-card span {
|
| 391 |
+
color: #737373 !important;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
body.dark-theme .metadata-tag {
|
| 395 |
+
background-color: rgba(255, 255, 255, 0.03) !important;
|
| 396 |
+
border-color: rgba(255, 255, 255, 0.04) !important;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
body.dark-theme .metadata-tag span:first-child {
|
| 400 |
+
color: #737373 !important;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
body.dark-theme .metadata-tag span:last-child {
|
| 404 |
+
color: #e2e8f0 !important;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
body.dark-theme .empty-detail {
|
| 408 |
+
color: #737373 !important;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
body.dark-theme .insight-close {
|
| 412 |
+
color: #737373 !important;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
body.dark-theme .neural-insight-modal {
|
| 416 |
+
background-color: #0f1115 !important;
|
| 417 |
+
border: 1px solid rgba(255, 255, 255, 0.06) !important;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
body.dark-theme .insight-header h3 {
|
| 421 |
+
color: #f8fafc !important;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
body.dark-theme .insight-body p {
|
| 425 |
+
color: #a3a3a3 !important;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
body.dark-theme .insight-btn {
|
| 429 |
+
background-color: #ff6b35 !important;
|
| 430 |
+
color: white !important;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
/* ── SOMA Knowledge Graph Dark UI Overrides ── */
|
| 434 |
+
body.dark-theme .graph-network-container {
|
| 435 |
+
background: rgba(18, 18, 18, 0.75) !important;
|
| 436 |
+
border: 1px solid rgba(255, 255, 255, 0.06) !important;
|
| 437 |
+
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55), inset 0 0 80px rgba(255, 255, 255, 0.02) !important;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
body.dark-theme .graph-select {
|
| 441 |
+
background: rgba(24, 24, 24, 0.75) !important;
|
| 442 |
+
border: 1px solid rgba(255, 255, 255, 0.05) !important;
|
| 443 |
+
color: #f8fafc !important;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
body.dark-theme .graph-icon-button {
|
| 447 |
+
background: rgba(24, 24, 24, 0.75) !important;
|
| 448 |
+
border: 1px solid rgba(255, 255, 255, 0.05) !important;
|
| 449 |
+
color: #a3a3a3 !important;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
body.dark-theme .graph-icon-button:hover {
|
| 453 |
+
background: rgba(255, 255, 255, 0.05) !important;
|
| 454 |
+
color: #ff6b35 !important;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
body.dark-theme .graph-legend {
|
| 458 |
+
background: rgba(24, 24, 24, 0.85) !important;
|
| 459 |
+
border: 1px solid rgba(255, 255, 255, 0.05) !important;
|
| 460 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.4) !important;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
body.dark-theme .legend-item {
|
| 464 |
+
color: #a3a3a3 !important;
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
body.dark-theme .hud-panel {
|
| 468 |
+
background: rgba(18, 18, 18, 0.85) !important;
|
| 469 |
+
border: 1px solid rgba(255, 255, 255, 0.05) !important;
|
| 470 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.4) !important;
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
body.dark-theme .hud-header {
|
| 474 |
+
color: #ff8b54 !important;
|
| 475 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.05) !important;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
body.dark-theme .hud-label {
|
| 479 |
+
color: #737373 !important;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
body.dark-theme .hud-value {
|
| 483 |
+
color: #f8fafc !important;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
body.dark-theme .entity-name {
|
| 487 |
+
color: #e5e5e5 !important;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
body.dark-theme .entity-connections {
|
| 491 |
+
color: #ffaa80 !important;
|
| 492 |
+
background: rgba(255, 107, 53, 0.1) !important;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
body.dark-theme .graph-tooltip {
|
| 496 |
+
text-shadow:
|
| 497 |
+
-3px -3px 0 #000,
|
| 498 |
+
3px -3px 0 #000,
|
| 499 |
+
-3px 3px 0 #000,
|
| 500 |
+
3px 3px 0 #000,
|
| 501 |
+
-3px 0px 0 #000,
|
| 502 |
+
3px 0px 0 #000,
|
| 503 |
+
0px -3px 0 #000,
|
| 504 |
+
0px 3px 0 #000 !important;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
body.dark-theme .graph-loading {
|
| 508 |
+
background: rgba(18, 18, 18, 0.9) !important;
|
| 509 |
+
border: 1px solid rgba(255, 255, 255, 0.06) !important;
|
| 510 |
+
color: #ff6b35 !important;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
/* ── SOMA Neural Inscription Dark UI Overrides ── */
|
| 514 |
+
body.dark-theme .inscription-icon {
|
| 515 |
+
background-color: rgba(24, 24, 24, 0.75) !important;
|
| 516 |
+
border: 1px solid rgba(255, 255, 255, 0.05) !important;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
body.dark-theme .inscription-title h3 {
|
| 520 |
+
color: #f8fafc !important;
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
body.dark-theme .inscription-composer {
|
| 524 |
+
background: rgba(18, 18, 18, 0.75) !important;
|
| 525 |
+
border: 1px solid rgba(255, 255, 255, 0.06) !important;
|
| 526 |
+
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55) !important;
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
body.dark-theme .inscription-composer textarea {
|
| 530 |
+
color: #f8fafc !important;
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
body.dark-theme .inscription-composer textarea::placeholder {
|
| 534 |
+
color: #525252 !important;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
body.dark-theme .inscription-footer {
|
| 538 |
+
border-top-color: rgba(255, 255, 255, 0.05) !important;
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
body.dark-theme .analysis-panel {
|
| 542 |
+
background: rgba(18, 18, 18, 0.75) !important;
|
| 543 |
+
border: 1px solid rgba(255, 255, 255, 0.06) !important;
|
| 544 |
+
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55) !important;
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
body.dark-theme .analysis-section label,
|
| 548 |
+
body.dark-theme .analysis-metric label {
|
| 549 |
+
color: #737373 !important;
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
body.dark-theme .mini-meter {
|
| 553 |
+
background-color: rgba(255, 255, 255, 0.06) !important;
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
body.dark-theme .metric-sub {
|
| 557 |
+
color: #737373 !important;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
body.dark-theme .link-tag {
|
| 561 |
+
background-color: rgba(24, 24, 24, 0.5) !important;
|
| 562 |
+
border: 1px solid rgba(255, 255, 255, 0.04) !important;
|
| 563 |
+
color: #e2e8f0 !important;
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
body.dark-theme .link-tag.reinforced {
|
| 567 |
+
border-color: rgba(16, 185, 129, 0.3) !important;
|
| 568 |
+
background: rgba(16, 185, 129, 0.08) !important;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
body.dark-theme .impact-item {
|
| 572 |
+
background-color: rgba(24, 24, 24, 0.5) !important;
|
| 573 |
+
border: 1px solid rgba(255, 255, 255, 0.04) !important;
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
body.dark-theme .impact-val {
|
| 577 |
+
color: #f8fafc !important;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
body.dark-theme .impact-label {
|
| 581 |
+
color: #737373 !important;
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
body.dark-theme .layer-item {
|
| 585 |
+
color: #a3a3a3 !important;
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
body.dark-theme .empty-tag {
|
| 589 |
+
color: #737373 !important;
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
body.dark-theme .existing-link-item {
|
| 593 |
+
background-color: rgba(24, 24, 24, 0.5) !important;
|
| 594 |
+
border: 1px solid rgba(255, 255, 255, 0.04) !important;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
body.dark-theme .link-info strong {
|
| 598 |
+
color: #f8fafc !important;
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
body.dark-theme .link-info span {
|
| 602 |
+
color: #737373 !important;
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
body.dark-theme .panel-footer {
|
| 606 |
+
border-top-color: rgba(255, 255, 255, 0.05) !important;
|
| 607 |
+
color: #737373 !important;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
/* ── SOMA Sleep Page & Consolidation Dark UI Overrides ── */
|
| 611 |
+
body.dark-theme .sleep-page-container {
|
| 612 |
+
background-color: #000000 !important;
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
body.dark-theme .sleep-page-header h2 {
|
| 616 |
+
color: #f8fafc !important;
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
body.dark-theme .sleep-copy-combined h3 {
|
| 620 |
+
color: #f8fafc !important;
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
body.dark-theme .sleep-copy-combined p {
|
| 624 |
+
color: #a3a3a3 !important;
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
body.dark-theme .sleep-step-text {
|
| 628 |
+
color: #737373 !important;
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
body.dark-theme .sleep-step-item-vertical.active .sleep-step-text {
|
| 632 |
+
color: #f8fafc !important;
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
body.dark-theme .sleep-step-circle {
|
| 636 |
+
background-color: #000000 !important;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
body.dark-theme .sleep-step-circle.pending {
|
| 640 |
+
border-color: #404040 !important;
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
body.dark-theme .connector-line {
|
| 644 |
+
background-color: #404040 !important;
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
body.dark-theme .summary-overlay-combined {
|
| 648 |
+
background-color: #000000 !important;
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
body.dark-theme .sleep-page-header-summary h2 {
|
| 652 |
+
color: #f8fafc !important;
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
body.dark-theme .summary-card-combined {
|
| 656 |
+
background-color: rgba(18, 18, 18, 0.85) !important;
|
| 657 |
+
border-color: rgba(255, 255, 255, 0.05) !important;
|
| 658 |
+
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55) !important;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
body.dark-theme .summary-close-combined {
|
| 662 |
+
color: #737373 !important;
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
body.dark-theme .summary-close-combined:hover {
|
| 666 |
+
color: #f8fafc !important;
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
body.dark-theme .summary-header-copy-combined h3 {
|
| 670 |
+
color: #f8fafc !important;
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
body.dark-theme .summary-header-copy-combined p {
|
| 674 |
+
color: #737373 !important;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
body.dark-theme .summary-item-combined {
|
| 678 |
+
background-color: rgba(255, 255, 255, 0.03) !important;
|
| 679 |
+
border-color: rgba(255, 255, 255, 0.04) !important;
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
body.dark-theme .summary-item-copy-combined strong {
|
| 683 |
+
color: #e5e5e5 !important;
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
body.dark-theme .summary-item-copy-combined span {
|
| 687 |
+
color: #737373 !important;
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
body.dark-theme .summary-view-btn-combined {
|
| 691 |
+
background-color: rgba(255, 107, 53, 0.1) !important;
|
| 692 |
+
color: #ff6b35 !important;
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
body.dark-theme .summary-view-btn-combined:hover {
|
| 696 |
+
background-color: rgba(255, 107, 53, 0.15) !important;
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
body.dark-theme .summary-footer-combined p {
|
| 700 |
+
color: #a3a3a3 !important;
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
body.dark-theme .sleep-trigger-btn {
|
| 704 |
+
background: linear-gradient(135deg, #ff6b35, #ff5714) !important;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
body.dark-theme .sleep-trigger-btn:hover {
|
| 708 |
+
box-shadow: 0 15px 40px rgba(255, 107, 53, 0.4) !important;
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
body.dark-theme .sleep-brain-image-asset {
|
| 712 |
+
background: none !important;
|
| 713 |
+
mix-blend-mode: normal !important;
|
| 714 |
+
opacity: 1 !important;
|
| 715 |
+
border-radius: 0 !important;
|
| 716 |
+
}
|
| 717 |
+
|
scratch/check_deps.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
try:
|
| 2 |
+
import fastapi
|
| 3 |
+
print("fastapi ok")
|
| 4 |
+
import uvicorn
|
| 5 |
+
print("uvicorn ok")
|
| 6 |
+
import langgraph
|
| 7 |
+
print("langgraph ok")
|
| 8 |
+
import langchain
|
| 9 |
+
print("langchain ok")
|
| 10 |
+
import chromadb
|
| 11 |
+
print("chromadb ok")
|
| 12 |
+
import neo4j
|
| 13 |
+
print("neo4j ok")
|
| 14 |
+
except ImportError as e:
|
| 15 |
+
print(f"Import failed: {e}")
|
scratch/check_graph_nodes.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.db.neo4j_driver import neo4j_db
|
| 2 |
+
|
| 3 |
+
def check_nodes():
|
| 4 |
+
if not neo4j_db.driver:
|
| 5 |
+
print("Neo4j driver is not active.")
|
| 6 |
+
return
|
| 7 |
+
|
| 8 |
+
try:
|
| 9 |
+
query = "MATCH (n:Entity) RETURN n.name AS name, n.user_id AS user_id"
|
| 10 |
+
results = neo4j_db.query(query)
|
| 11 |
+
print("Nodes found in Neo4j:")
|
| 12 |
+
for r in results:
|
| 13 |
+
print(f"- Name: '{r['name']}', User ID: '{r['user_id']}'")
|
| 14 |
+
except Exception as e:
|
| 15 |
+
print(f"Error querying Neo4j: {e}")
|
| 16 |
+
|
| 17 |
+
if __name__ == "__main__":
|
| 18 |
+
check_nodes()
|
scratch/check_soma.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.db.neo4j_driver import neo4j_db
|
| 2 |
+
|
| 3 |
+
res = neo4j_db.query("""
|
| 4 |
+
MATCH (s)-[r]->(o)
|
| 5 |
+
WHERE toLower(s.name) = 'soma' OR toLower(o.name) = 'soma'
|
| 6 |
+
RETURN s.name as subject, type(r) as relation, o.name as object
|
| 7 |
+
""")
|
| 8 |
+
print("SOMA Relations:")
|
| 9 |
+
for r in res:
|
| 10 |
+
print(f" {r['subject']} --[{r['relation']}]--> {r['object']}")
|
scratch/color_to_alpha.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PIL import Image
|
| 2 |
+
|
| 3 |
+
def color_to_alpha(img_path, output_path, bg_color):
|
| 4 |
+
img = Image.open(img_path).convert("RGBA")
|
| 5 |
+
width, height = img.size
|
| 6 |
+
pixels = img.load()
|
| 7 |
+
|
| 8 |
+
r_bg, g_bg, b_bg = bg_color
|
| 9 |
+
|
| 10 |
+
for y in range(height):
|
| 11 |
+
for x in range(width):
|
| 12 |
+
r, g, b, a = pixels[x, y]
|
| 13 |
+
|
| 14 |
+
# Calculate alpha for each channel based on Color-to-Alpha algorithm
|
| 15 |
+
if r > r_bg:
|
| 16 |
+
a_r = (r - r_bg) / (255.0 - r_bg)
|
| 17 |
+
elif r < r_bg:
|
| 18 |
+
a_r = (r_bg - r) / float(r_bg)
|
| 19 |
+
else:
|
| 20 |
+
a_r = 0.0
|
| 21 |
+
|
| 22 |
+
if g > g_bg:
|
| 23 |
+
a_g = (g - g_bg) / (255.0 - g_bg)
|
| 24 |
+
elif g < g_bg:
|
| 25 |
+
a_g = (g_bg - g) / float(g_bg)
|
| 26 |
+
else:
|
| 27 |
+
a_g = 0.0
|
| 28 |
+
|
| 29 |
+
if b > b_bg:
|
| 30 |
+
a_b = (b - b_bg) / (255.0 - b_bg)
|
| 31 |
+
elif b < b_bg:
|
| 32 |
+
a_b = (b_bg - b) / float(b_bg)
|
| 33 |
+
else:
|
| 34 |
+
a_b = 0.0
|
| 35 |
+
|
| 36 |
+
# The pixel's alpha is the maximum alpha across all channels
|
| 37 |
+
alpha = max(a_r, a_g, a_b)
|
| 38 |
+
|
| 39 |
+
if alpha > 0.001:
|
| 40 |
+
# Calculate new foreground color
|
| 41 |
+
r_fg = int(round((r - (1.0 - alpha) * r_bg) / alpha))
|
| 42 |
+
g_fg = int(round((g - (1.0 - alpha) * g_bg) / alpha))
|
| 43 |
+
b_fg = int(round((b - (1.0 - alpha) * b_bg) / alpha))
|
| 44 |
+
|
| 45 |
+
# Clamp colors to 0-255
|
| 46 |
+
r_fg = max(0, min(255, r_fg))
|
| 47 |
+
g_fg = max(0, min(255, g_fg))
|
| 48 |
+
b_fg = max(0, min(255, b_fg))
|
| 49 |
+
a_val = int(round(alpha * 255))
|
| 50 |
+
|
| 51 |
+
pixels[x, y] = (r_fg, g_fg, b_fg, a_val)
|
| 52 |
+
else:
|
| 53 |
+
pixels[x, y] = (0, 0, 0, 0)
|
| 54 |
+
|
| 55 |
+
img.save(output_path, "PNG")
|
| 56 |
+
print(f"SUCCESS: Converted {img_path} to transparent {output_path} using Color-to-Alpha!")
|
| 57 |
+
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
color_to_alpha(
|
| 60 |
+
r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep.png",
|
| 61 |
+
r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep_nobg.png",
|
| 62 |
+
(234, 234, 234)
|
| 63 |
+
)
|
scratch/copy_alpha.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
|
| 3 |
+
def main():
|
| 4 |
+
# Load original sleep.png (the one with the solid background)
|
| 5 |
+
sleep_img = cv2.imread(r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep.png", cv2.IMREAD_UNCHANGED)
|
| 6 |
+
if sleep_img is None:
|
| 7 |
+
print("Error loading sleep.png")
|
| 8 |
+
return
|
| 9 |
+
|
| 10 |
+
# Load Brain_nobg.png
|
| 11 |
+
brain_nobg = cv2.imread(r"d:\PROJECTS\Soma\frontend\src\assets\brain\Brain_nobg.png", cv2.IMREAD_UNCHANGED)
|
| 12 |
+
if brain_nobg is None:
|
| 13 |
+
print("Error loading Brain_nobg.png")
|
| 14 |
+
return
|
| 15 |
+
|
| 16 |
+
# The brain_nobg image has 4 channels (BGRA)
|
| 17 |
+
alpha_channel = brain_nobg[:, :, 3]
|
| 18 |
+
|
| 19 |
+
# Resize alpha channel to match sleep_img exactly
|
| 20 |
+
h, w = sleep_img.shape[:2]
|
| 21 |
+
alpha_resized = cv2.resize(alpha_channel, (w, h), interpolation=cv2.INTER_LINEAR)
|
| 22 |
+
|
| 23 |
+
# Add alpha channel to sleep_img
|
| 24 |
+
if sleep_img.shape[2] == 3:
|
| 25 |
+
sleep_img = cv2.cvtColor(sleep_img, cv2.COLOR_BGR2BGRA)
|
| 26 |
+
|
| 27 |
+
sleep_img[:, :, 3] = alpha_resized
|
| 28 |
+
|
| 29 |
+
# Save as sleep_nobg.png
|
| 30 |
+
cv2.imwrite(r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep_nobg.png", sleep_img)
|
| 31 |
+
print("SUCCESS: Copied perfect alpha channel from Brain_nobg.png!")
|
| 32 |
+
|
| 33 |
+
if __name__ == "__main__":
|
| 34 |
+
main()
|
scratch/cutout_brain.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import collections
|
| 2 |
+
from PIL import Image
|
| 3 |
+
|
| 4 |
+
def main():
|
| 5 |
+
# Load the original sleep brain image
|
| 6 |
+
img_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep.png"
|
| 7 |
+
img = Image.open(img_path).convert("RGBA")
|
| 8 |
+
width, height = img.size
|
| 9 |
+
|
| 10 |
+
# The background color is exactly #eaeaea, which in RGB is (234, 234, 234)
|
| 11 |
+
bg_color = (234, 234, 234, 255)
|
| 12 |
+
|
| 13 |
+
# Start with all white mask (opaque)
|
| 14 |
+
mask = Image.new("L", (width, height), 255)
|
| 15 |
+
|
| 16 |
+
# Simple BFS flood fill to find background pixels
|
| 17 |
+
visited = set()
|
| 18 |
+
tolerance = 12
|
| 19 |
+
tr, tg, tb = bg_color[:3]
|
| 20 |
+
|
| 21 |
+
# We will start flood fill from the borders of the image (all 4 edges)
|
| 22 |
+
edge_pixels = []
|
| 23 |
+
for x in range(width):
|
| 24 |
+
edge_pixels.append((x, 0))
|
| 25 |
+
edge_pixels.append((x, height - 1))
|
| 26 |
+
for y in range(height):
|
| 27 |
+
edge_pixels.append((0, y))
|
| 28 |
+
edge_pixels.append((width - 1, y))
|
| 29 |
+
|
| 30 |
+
queue = collections.deque(edge_pixels)
|
| 31 |
+
for p in edge_pixels:
|
| 32 |
+
visited.add(p)
|
| 33 |
+
|
| 34 |
+
while queue:
|
| 35 |
+
x, y = queue.popleft()
|
| 36 |
+
|
| 37 |
+
# Get pixel color
|
| 38 |
+
r, g, b, a = img.getpixel((x, y))
|
| 39 |
+
|
| 40 |
+
# If it is close to target background color, it's outer background
|
| 41 |
+
if abs(r - tr) <= tolerance and abs(g - tg) <= tolerance and abs(b - tb) <= tolerance:
|
| 42 |
+
mask.putpixel((x, y), 0) # set to transparent
|
| 43 |
+
|
| 44 |
+
# Neighbors
|
| 45 |
+
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
| 46 |
+
nx, ny = x + dx, y + dy
|
| 47 |
+
if 0 <= nx < width and 0 <= ny < height:
|
| 48 |
+
if (nx, ny) not in visited:
|
| 49 |
+
visited.add((nx, ny))
|
| 50 |
+
queue.append((nx, ny))
|
| 51 |
+
|
| 52 |
+
# Apply the mask back to the image
|
| 53 |
+
for y in range(height):
|
| 54 |
+
for x in range(width):
|
| 55 |
+
r, g, b, a = img.getpixel((x, y))
|
| 56 |
+
m_val = mask.getpixel((x, y))
|
| 57 |
+
if m_val == 0:
|
| 58 |
+
img.putpixel((x, y), (r, g, b, 0))
|
| 59 |
+
|
| 60 |
+
# Save to sleep_nobg.png
|
| 61 |
+
output_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep_nobg.png"
|
| 62 |
+
img.save(output_path, "PNG")
|
| 63 |
+
print("SUCCESS: Perfect transparent cutout sleep_nobg.png created successfully!")
|
| 64 |
+
|
| 65 |
+
if __name__ == "__main__":
|
| 66 |
+
main()
|
scratch/ellipse_cutout.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from PIL import Image
|
| 3 |
+
|
| 4 |
+
def main():
|
| 5 |
+
# Load original sleep image
|
| 6 |
+
img_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep.png"
|
| 7 |
+
img = Image.open(img_path).convert("RGBA")
|
| 8 |
+
width, height = img.size
|
| 9 |
+
pixels = img.load()
|
| 10 |
+
|
| 11 |
+
# Perfect ellipse center and radii matching the sleep brain shape
|
| 12 |
+
cx, cy = 186, 182
|
| 13 |
+
rx, ry = 160, 134
|
| 14 |
+
|
| 15 |
+
final_img = Image.new("RGBA", (width, height))
|
| 16 |
+
final_pixels = final_img.load()
|
| 17 |
+
|
| 18 |
+
for y in range(height):
|
| 19 |
+
for x in range(width):
|
| 20 |
+
r, g, b, a = pixels[x, y]
|
| 21 |
+
|
| 22 |
+
# Calculate distance to ellipse boundary
|
| 23 |
+
dx = x - cx
|
| 24 |
+
dy = y - cy
|
| 25 |
+
|
| 26 |
+
# Elliptical distance ratio
|
| 27 |
+
dist_ratio = math.sqrt((dx/rx)**2 + (dy/ry)**2)
|
| 28 |
+
|
| 29 |
+
# Smooth step anti-aliasing at the boundary:
|
| 30 |
+
# If ratio <= 0.96: 100% opaque (brain interior)
|
| 31 |
+
# If ratio >= 1.00: 100% transparent (background)
|
| 32 |
+
if dist_ratio <= 0.96:
|
| 33 |
+
alpha_val = 255
|
| 34 |
+
elif dist_ratio >= 1.00:
|
| 35 |
+
alpha_val = 0
|
| 36 |
+
else:
|
| 37 |
+
# Smooth interpolation at the edge
|
| 38 |
+
t = (dist_ratio - 0.96) / 0.04
|
| 39 |
+
alpha_val = int(round((1.0 - t) * 255))
|
| 40 |
+
|
| 41 |
+
final_pixels[x, y] = (r, g, b, alpha_val)
|
| 42 |
+
|
| 43 |
+
output_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep_nobg.png"
|
| 44 |
+
final_img.save(output_path, "PNG")
|
| 45 |
+
print("SUCCESS: Perfect mathematical elliptical cutout sleep_nobg.png created successfully!")
|
| 46 |
+
|
| 47 |
+
if __name__ == "__main__":
|
| 48 |
+
main()
|
scratch/fill_holes.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
def main():
|
| 5 |
+
img = cv2.imread(r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep.png", cv2.IMREAD_UNCHANGED)
|
| 6 |
+
if img is None:
|
| 7 |
+
print("Error loading image")
|
| 8 |
+
return
|
| 9 |
+
|
| 10 |
+
if img.shape[2] == 3:
|
| 11 |
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
|
| 12 |
+
|
| 13 |
+
b, g, r = img[:, :, 0], img[:, :, 1], img[:, :, 2]
|
| 14 |
+
|
| 15 |
+
# Target background
|
| 16 |
+
is_bg = (r >= 190) & (r <= 226) & (g >= 190) & (g <= 226) & (b >= 190) & (b <= 226)
|
| 17 |
+
|
| 18 |
+
max_c = np.maximum(np.maximum(r, g), b)
|
| 19 |
+
min_c = np.minimum(np.minimum(r, g), b)
|
| 20 |
+
is_neutral = (max_c - min_c) <= 6
|
| 21 |
+
|
| 22 |
+
bg_mask = is_bg & is_neutral
|
| 23 |
+
|
| 24 |
+
brain_mask = np.zeros_like(r, dtype=np.uint8)
|
| 25 |
+
brain_mask[~bg_mask] = 255
|
| 26 |
+
|
| 27 |
+
# Close gaps
|
| 28 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))
|
| 29 |
+
closed_mask = cv2.morphologyEx(brain_mask, cv2.MORPH_CLOSE, kernel)
|
| 30 |
+
|
| 31 |
+
contours, _ = cv2.findContours(closed_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 32 |
+
|
| 33 |
+
if not contours:
|
| 34 |
+
return
|
| 35 |
+
|
| 36 |
+
largest_contour = max(contours, key=cv2.contourArea)
|
| 37 |
+
|
| 38 |
+
silhouette = np.zeros_like(brain_mask)
|
| 39 |
+
# Draw contour with anti-aliasing
|
| 40 |
+
cv2.drawContours(silhouette, [largest_contour], -1, 255, thickness=cv2.FILLED, lineType=cv2.LINE_AA)
|
| 41 |
+
|
| 42 |
+
# Erode the silhouette slightly to shave off jaggedness and any remaining grey halo
|
| 43 |
+
erode_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
| 44 |
+
silhouette = cv2.erode(silhouette, erode_kernel, iterations=1)
|
| 45 |
+
|
| 46 |
+
# Smooth Gaussian blur to make the edges buttery smooth (feathering)
|
| 47 |
+
silhouette_smoothed = cv2.GaussianBlur(silhouette, (9, 9), 0)
|
| 48 |
+
|
| 49 |
+
img[:, :, 3] = silhouette_smoothed
|
| 50 |
+
|
| 51 |
+
out_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep_nobg.png"
|
| 52 |
+
cv2.imwrite(out_path, img)
|
| 53 |
+
print("SUCCESS: Smooth edges applied!")
|
| 54 |
+
|
| 55 |
+
if __name__ == "__main__":
|
| 56 |
+
main()
|
scratch/key_bg.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PIL import Image
|
| 2 |
+
|
| 3 |
+
def main():
|
| 4 |
+
# Load original sleep image
|
| 5 |
+
img_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep.png"
|
| 6 |
+
img = Image.open(img_path).convert("RGBA")
|
| 7 |
+
width, height = img.size
|
| 8 |
+
pixels = img.load()
|
| 9 |
+
|
| 10 |
+
# We will simply key out the neutral light-grey background color range
|
| 11 |
+
# Background pixels are neutral grey between 195 and 225
|
| 12 |
+
for y in range(height):
|
| 13 |
+
for x in range(width):
|
| 14 |
+
r, g, b, a = pixels[x, y]
|
| 15 |
+
|
| 16 |
+
# If the pixel is in the neutral background color range, make it transparent
|
| 17 |
+
is_bg = (195 <= r <= 225) and (abs(r - g) <= 5) and (abs(g - b) <= 5)
|
| 18 |
+
|
| 19 |
+
if is_bg:
|
| 20 |
+
pixels[x, y] = (r, g, b, 0)
|
| 21 |
+
else:
|
| 22 |
+
pixels[x, y] = (r, g, b, 255)
|
| 23 |
+
|
| 24 |
+
output_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep_nobg.png"
|
| 25 |
+
img.save(output_path, "PNG")
|
| 26 |
+
print("SUCCESS: Keyed out background color successfully!")
|
| 27 |
+
|
| 28 |
+
if __name__ == "__main__":
|
| 29 |
+
main()
|
scratch/morph_cutout.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import collections
|
| 2 |
+
from PIL import Image, ImageFilter
|
| 3 |
+
|
| 4 |
+
def main():
|
| 5 |
+
# Load original sleep image
|
| 6 |
+
img_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep.png"
|
| 7 |
+
img = Image.open(img_path).convert("RGBA")
|
| 8 |
+
width, height = img.size
|
| 9 |
+
pixels = img.load()
|
| 10 |
+
|
| 11 |
+
# 1. Create a binary threshold mask: 255 for non-background pixels, 0 for background pixels
|
| 12 |
+
thresh = Image.new("L", (width, height), 0)
|
| 13 |
+
thresh_pixels = thresh.load()
|
| 14 |
+
for y in range(height):
|
| 15 |
+
for x in range(width):
|
| 16 |
+
r, g, b, a = pixels[x, y]
|
| 17 |
+
# Background is neutral light-grey around (200-223)
|
| 18 |
+
is_bg_color = (200 <= r <= 223) and (abs(r - g) <= 5) and (abs(g - b) <= 5)
|
| 19 |
+
if not is_bg_color:
|
| 20 |
+
thresh_pixels[x, y] = 255
|
| 21 |
+
|
| 22 |
+
# 2. Apply Morphological Closing to completely seal any microscopic boundary gaps!
|
| 23 |
+
# MaxFilter(9) dilates the 255 region by 4 pixels, closing any gaps in the outline.
|
| 24 |
+
dilated = thresh.filter(ImageFilter.MaxFilter(9))
|
| 25 |
+
# MinFilter(9) erodes it back by 4 pixels to restore the exact original boundary size.
|
| 26 |
+
closed = dilated.filter(ImageFilter.MinFilter(9))
|
| 27 |
+
|
| 28 |
+
# 3. Flood fill the outer background on the closed mask starting from all borders.
|
| 29 |
+
# Since the closed mask has no gaps, the flood fill will stay strictly outside the brain!
|
| 30 |
+
visited = set()
|
| 31 |
+
edge_pixels = []
|
| 32 |
+
for x in range(width):
|
| 33 |
+
edge_pixels.append((x, 0))
|
| 34 |
+
edge_pixels.append((x, height - 1))
|
| 35 |
+
for y in range(height):
|
| 36 |
+
edge_pixels.append((0, y))
|
| 37 |
+
edge_pixels.append((width - 1, y))
|
| 38 |
+
|
| 39 |
+
queue = collections.deque(edge_pixels)
|
| 40 |
+
for p in edge_pixels:
|
| 41 |
+
visited.add(p)
|
| 42 |
+
|
| 43 |
+
# The final silhouette mask starts as all 255 (brain). Flood filled pixels become 0 (transparent).
|
| 44 |
+
silhouette = Image.new("L", (width, height), 255)
|
| 45 |
+
sil_pixels = silhouette.load()
|
| 46 |
+
closed_pixels = closed.load()
|
| 47 |
+
|
| 48 |
+
while queue:
|
| 49 |
+
x, y = queue.popleft()
|
| 50 |
+
|
| 51 |
+
# If this pixel in the closed mask is 0 (background), it is outer background
|
| 52 |
+
if closed_pixels[x, y] == 0:
|
| 53 |
+
sil_pixels[x, y] = 0
|
| 54 |
+
|
| 55 |
+
# Propagate
|
| 56 |
+
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
| 57 |
+
nx, ny = x + dx, y + dy
|
| 58 |
+
if 0 <= nx < width and 0 <= ny < height:
|
| 59 |
+
if (nx, ny) not in visited:
|
| 60 |
+
visited.add((nx, ny))
|
| 61 |
+
queue.append((nx, ny))
|
| 62 |
+
|
| 63 |
+
# 4. Smooth the silhouette mask edges with a 1.2px Gaussian Blur for a perfect anti-aliased cut-out.
|
| 64 |
+
smooth_silhouette = silhouette.filter(ImageFilter.GaussianBlur(1.2))
|
| 65 |
+
|
| 66 |
+
# 5. Apply the perfect silhouette to the alpha channel of the original sleep.png image
|
| 67 |
+
final_img = Image.new("RGBA", (width, height))
|
| 68 |
+
final_pixels = final_img.load()
|
| 69 |
+
smooth_pixels = smooth_silhouette.load()
|
| 70 |
+
|
| 71 |
+
for y in range(height):
|
| 72 |
+
for x in range(width):
|
| 73 |
+
r, g, b, a = pixels[x, y]
|
| 74 |
+
alpha_val = smooth_pixels[x, y]
|
| 75 |
+
final_pixels[x, y] = (r, g, b, alpha_val)
|
| 76 |
+
|
| 77 |
+
# 6. Save the perfect cutout back to sleep_nobg.png!
|
| 78 |
+
output_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep_nobg.png"
|
| 79 |
+
final_img.save(output_path, "PNG")
|
| 80 |
+
print("SUCCESS: Perfect morphological cutout created successfully!")
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
main()
|
scratch/morph_cutout_v2.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import collections
|
| 2 |
+
from PIL import Image, ImageFilter
|
| 3 |
+
|
| 4 |
+
def main():
|
| 5 |
+
# Load original sleep image
|
| 6 |
+
img_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep.png"
|
| 7 |
+
img = Image.open(img_path).convert("RGBA")
|
| 8 |
+
width, height = img.size
|
| 9 |
+
pixels = img.load()
|
| 10 |
+
|
| 11 |
+
# 1. Create a binary threshold mask: 255 for non-background pixels, 0 for background pixels
|
| 12 |
+
thresh = Image.new("L", (width, height), 0)
|
| 13 |
+
thresh_pixels = thresh.load()
|
| 14 |
+
for y in range(height):
|
| 15 |
+
for x in range(width):
|
| 16 |
+
r, g, b, a = pixels[x, y]
|
| 17 |
+
# Background is neutral light-grey around (195-224)
|
| 18 |
+
is_bg_color = (195 <= r <= 224) and (abs(r - g) <= 5) and (abs(g - b) <= 5)
|
| 19 |
+
if not is_bg_color:
|
| 20 |
+
thresh_pixels[x, y] = 255
|
| 21 |
+
|
| 22 |
+
# 2. Apply Morphological Closing + Boundary Erosion
|
| 23 |
+
# MaxFilter(5) dilates by 2 pixels to close outline gaps
|
| 24 |
+
dilated = thresh.filter(ImageFilter.MaxFilter(5))
|
| 25 |
+
# MinFilter(9) erodes by 4 pixels, which shrinks the final silhouette strictly inside the brain border
|
| 26 |
+
# to completely shave off the fuzzy grey patch and any boundary halo!
|
| 27 |
+
closed = dilated.filter(ImageFilter.MinFilter(9))
|
| 28 |
+
|
| 29 |
+
# 3. Flood fill the outer background on the closed mask starting from all borders.
|
| 30 |
+
visited = set()
|
| 31 |
+
edge_pixels = []
|
| 32 |
+
for x in range(width):
|
| 33 |
+
edge_pixels.append((x, 0))
|
| 34 |
+
edge_pixels.append((x, height - 1))
|
| 35 |
+
for y in range(height):
|
| 36 |
+
edge_pixels.append((0, y))
|
| 37 |
+
edge_pixels.append((width - 1, y))
|
| 38 |
+
|
| 39 |
+
queue = collections.deque(edge_pixels)
|
| 40 |
+
for p in edge_pixels:
|
| 41 |
+
visited.add(p)
|
| 42 |
+
|
| 43 |
+
# The final silhouette mask starts as all 255 (brain). Flood filled pixels become 0 (transparent).
|
| 44 |
+
silhouette = Image.new("L", (width, height), 255)
|
| 45 |
+
sil_pixels = silhouette.load()
|
| 46 |
+
closed_pixels = closed.load()
|
| 47 |
+
|
| 48 |
+
while queue:
|
| 49 |
+
x, y = queue.popleft()
|
| 50 |
+
|
| 51 |
+
# If this pixel in the closed mask is 0 (background), it is outer background
|
| 52 |
+
if closed_pixels[x, y] == 0:
|
| 53 |
+
sil_pixels[x, y] = 0
|
| 54 |
+
|
| 55 |
+
# Propagate
|
| 56 |
+
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
| 57 |
+
nx, ny = x + dx, y + dy
|
| 58 |
+
if 0 <= nx < width and 0 <= ny < height:
|
| 59 |
+
if (nx, ny) not in visited:
|
| 60 |
+
visited.add((nx, ny))
|
| 61 |
+
queue.append((nx, ny))
|
| 62 |
+
|
| 63 |
+
# 4. Smooth the silhouette mask edges with a 1.2px Gaussian Blur for a perfect anti-aliased cut-out.
|
| 64 |
+
smooth_silhouette = silhouette.filter(ImageFilter.GaussianBlur(1.2))
|
| 65 |
+
|
| 66 |
+
# 5. Apply the perfect silhouette to the alpha channel of the original sleep.png image
|
| 67 |
+
final_img = Image.new("RGBA", (width, height))
|
| 68 |
+
final_pixels = final_img.load()
|
| 69 |
+
smooth_pixels = smooth_silhouette.load()
|
| 70 |
+
|
| 71 |
+
for y in range(height):
|
| 72 |
+
for x in range(width):
|
| 73 |
+
r, g, b, a = pixels[x, y]
|
| 74 |
+
alpha_val = smooth_pixels[x, y]
|
| 75 |
+
final_pixels[x, y] = (r, g, b, alpha_val)
|
| 76 |
+
|
| 77 |
+
# 6. Save the perfect cutout back to sleep_nobg.png!
|
| 78 |
+
output_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep_nobg.png"
|
| 79 |
+
final_img.save(output_path, "PNG")
|
| 80 |
+
print("SUCCESS: Tightly eroded morphological cutout created successfully!")
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
main()
|
scratch/perfect_cutout.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import collections
|
| 2 |
+
from PIL import Image, ImageFilter
|
| 3 |
+
|
| 4 |
+
def main():
|
| 5 |
+
# Load original sleep image
|
| 6 |
+
img_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep.png"
|
| 7 |
+
img = Image.open(img_path).convert("RGBA")
|
| 8 |
+
width, height = img.size
|
| 9 |
+
pixels = img.load()
|
| 10 |
+
|
| 11 |
+
# Target background color
|
| 12 |
+
bg_color = (234, 234, 234)
|
| 13 |
+
tr, tg, tb = bg_color
|
| 14 |
+
tolerance = 15
|
| 15 |
+
|
| 16 |
+
# 1. Create binary mask: 0 for background, 255 for brain
|
| 17 |
+
mask = Image.new("L", (width, height), 255)
|
| 18 |
+
mask_pixels = mask.load()
|
| 19 |
+
|
| 20 |
+
visited = set()
|
| 21 |
+
edge_pixels = []
|
| 22 |
+
for x in range(width):
|
| 23 |
+
edge_pixels.append((x, 0))
|
| 24 |
+
edge_pixels.append((x, height - 1))
|
| 25 |
+
for y in range(height):
|
| 26 |
+
edge_pixels.append((0, y))
|
| 27 |
+
edge_pixels.append((width - 1, y))
|
| 28 |
+
|
| 29 |
+
queue = collections.deque(edge_pixels)
|
| 30 |
+
for p in edge_pixels:
|
| 31 |
+
visited.add(p)
|
| 32 |
+
|
| 33 |
+
while queue:
|
| 34 |
+
x, y = queue.popleft()
|
| 35 |
+
r, g, b, a = pixels[x, y]
|
| 36 |
+
|
| 37 |
+
if abs(r - tr) <= tolerance and abs(g - tg) <= tolerance and abs(b - tb) <= tolerance:
|
| 38 |
+
mask_pixels[x, y] = 0
|
| 39 |
+
|
| 40 |
+
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
| 41 |
+
nx, ny = x + dx, y + dy
|
| 42 |
+
if 0 <= nx < width and 0 <= ny < height:
|
| 43 |
+
if (nx, ny) not in visited:
|
| 44 |
+
visited.add((nx, ny))
|
| 45 |
+
queue.append((nx, ny))
|
| 46 |
+
|
| 47 |
+
# 2. Erode the mask by 1 pixel to completely remove the thin grey boundary halo.
|
| 48 |
+
# MinFilter(3) replaces each pixel with the minimum in its 3x3 neighborhood.
|
| 49 |
+
# Since background is 0 and brain is 255, this expands the 0 (transparent) area by 1 pixel.
|
| 50 |
+
eroded_mask = mask.filter(ImageFilter.MinFilter(3))
|
| 51 |
+
|
| 52 |
+
# 3. Apply a smooth Gaussian Blur of 1.2 pixels to make the cut-out edges beautifully smooth and anti-aliased.
|
| 53 |
+
smooth_mask = eroded_mask.filter(ImageFilter.GaussianBlur(1.2))
|
| 54 |
+
|
| 55 |
+
# 4. Write the smoothed mask into the alpha channel of our original image
|
| 56 |
+
final_img = Image.new("RGBA", (width, height))
|
| 57 |
+
final_pixels = final_img.load()
|
| 58 |
+
smooth_pixels = smooth_mask.load()
|
| 59 |
+
|
| 60 |
+
for y in range(height):
|
| 61 |
+
for x in range(width):
|
| 62 |
+
r, g, b, a = pixels[x, y]
|
| 63 |
+
alpha_val = smooth_pixels[x, y]
|
| 64 |
+
final_pixels[x, y] = (r, g, b, alpha_val)
|
| 65 |
+
|
| 66 |
+
# 5. Save the perfect cutout
|
| 67 |
+
output_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep_nobg.png"
|
| 68 |
+
final_img.save(output_path, "PNG")
|
| 69 |
+
print("SUCCESS: Perfect glassy transparent cutout sleep_nobg.png created successfully!")
|
| 70 |
+
|
| 71 |
+
if __name__ == "__main__":
|
| 72 |
+
main()
|
scratch/perfect_cutout_v2.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import collections
|
| 2 |
+
from PIL import Image, ImageFilter
|
| 3 |
+
|
| 4 |
+
def main():
|
| 5 |
+
# Load original sleep image
|
| 6 |
+
img_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep.png"
|
| 7 |
+
img = Image.open(img_path).convert("RGBA")
|
| 8 |
+
width, height = img.size
|
| 9 |
+
pixels = img.load()
|
| 10 |
+
|
| 11 |
+
# 1. Create binary mask: 0 for background, 255 for brain
|
| 12 |
+
mask = Image.new("L", (width, height), 255)
|
| 13 |
+
mask_pixels = mask.load()
|
| 14 |
+
|
| 15 |
+
# We will perform BFS starting from all 4 borders of the image
|
| 16 |
+
visited = set()
|
| 17 |
+
edge_pixels = []
|
| 18 |
+
for x in range(width):
|
| 19 |
+
edge_pixels.append((x, 0))
|
| 20 |
+
edge_pixels.append((x, height - 1))
|
| 21 |
+
for y in range(height):
|
| 22 |
+
edge_pixels.append((0, y))
|
| 23 |
+
edge_pixels.append((width - 1, y))
|
| 24 |
+
|
| 25 |
+
queue = collections.deque(edge_pixels)
|
| 26 |
+
for p in edge_pixels:
|
| 27 |
+
visited.add(p)
|
| 28 |
+
|
| 29 |
+
while queue:
|
| 30 |
+
x, y = queue.popleft()
|
| 31 |
+
r, g, b, a = pixels[x, y]
|
| 32 |
+
|
| 33 |
+
# Check if the pixel color matches the neutral light-grey background gradient:
|
| 34 |
+
# Background is between 200 and 225, and neutral (R, G, B are very close)
|
| 35 |
+
is_bg = (200 <= r <= 223) and (abs(r - g) <= 5) and (abs(g - b) <= 5)
|
| 36 |
+
|
| 37 |
+
if is_bg:
|
| 38 |
+
mask_pixels[x, y] = 0 # set to transparent
|
| 39 |
+
|
| 40 |
+
# Check neighbors
|
| 41 |
+
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
| 42 |
+
nx, ny = x + dx, y + dy
|
| 43 |
+
if 0 <= nx < width and 0 <= ny < height:
|
| 44 |
+
if (nx, ny) not in visited:
|
| 45 |
+
visited.add((nx, ny))
|
| 46 |
+
queue.append((nx, ny))
|
| 47 |
+
|
| 48 |
+
# 2. Erode the mask by 1 pixel to remove the thin grey boundary outline
|
| 49 |
+
eroded_mask = mask.filter(ImageFilter.MinFilter(3))
|
| 50 |
+
|
| 51 |
+
# 3. Apply a smooth Gaussian Blur of 1.2 pixels to anti-alias the cut-out edge
|
| 52 |
+
smooth_mask = eroded_mask.filter(ImageFilter.GaussianBlur(1.2))
|
| 53 |
+
|
| 54 |
+
# 4. Write the smoothed mask into the alpha channel of our original image
|
| 55 |
+
final_img = Image.new("RGBA", (width, height))
|
| 56 |
+
final_pixels = final_img.load()
|
| 57 |
+
smooth_pixels = smooth_mask.load()
|
| 58 |
+
|
| 59 |
+
for y in range(height):
|
| 60 |
+
for x in range(width):
|
| 61 |
+
r, g, b, a = pixels[x, y]
|
| 62 |
+
alpha_val = smooth_pixels[x, y]
|
| 63 |
+
final_pixels[x, y] = (r, g, b, alpha_val)
|
| 64 |
+
|
| 65 |
+
# 5. Save the perfect cutout
|
| 66 |
+
output_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep_nobg.png"
|
| 67 |
+
final_img.save(output_path, "PNG")
|
| 68 |
+
print("SUCCESS: Perfect glassy transparent cutout sleep_nobg.png created successfully!")
|
| 69 |
+
|
| 70 |
+
if __name__ == "__main__":
|
| 71 |
+
main()
|
scratch/perfect_cutout_v3.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import collections
|
| 2 |
+
import math
|
| 3 |
+
from PIL import Image, ImageFilter
|
| 4 |
+
|
| 5 |
+
def main():
|
| 6 |
+
# Load original sleep image
|
| 7 |
+
img_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep.png"
|
| 8 |
+
img = Image.open(img_path).convert("RGBA")
|
| 9 |
+
width, height = img.size
|
| 10 |
+
pixels = img.load()
|
| 11 |
+
|
| 12 |
+
# 1. Create binary mask: 0 for background, 255 for brain
|
| 13 |
+
mask = Image.new("L", (width, height), 255)
|
| 14 |
+
mask_pixels = mask.load()
|
| 15 |
+
|
| 16 |
+
# Center of the 380x380 image
|
| 17 |
+
cx, cy = 190, 190
|
| 18 |
+
|
| 19 |
+
# BFS queue
|
| 20 |
+
visited = set()
|
| 21 |
+
edge_pixels = []
|
| 22 |
+
for x in range(width):
|
| 23 |
+
edge_pixels.append((x, 0))
|
| 24 |
+
edge_pixels.append((x, height - 1))
|
| 25 |
+
for y in range(height):
|
| 26 |
+
edge_pixels.append((0, y))
|
| 27 |
+
edge_pixels.append((width - 1, y))
|
| 28 |
+
|
| 29 |
+
queue = collections.deque(edge_pixels)
|
| 30 |
+
for p in edge_pixels:
|
| 31 |
+
visited.add(p)
|
| 32 |
+
|
| 33 |
+
while queue:
|
| 34 |
+
x, y = queue.popleft()
|
| 35 |
+
r, g, b, a = pixels[x, y]
|
| 36 |
+
|
| 37 |
+
# Calculate horizontal ellipse distance to protect the brain interior from leaks
|
| 38 |
+
# Brain center is 190, 190. Vertical radius is smaller than horizontal radius.
|
| 39 |
+
dx = x - cx
|
| 40 |
+
dy = y - cy
|
| 41 |
+
|
| 42 |
+
# Scale dy to account for horizontal elliptical shape of the brain
|
| 43 |
+
ellipse_dist = math.sqrt(dx*dx + (dy * 1.25)*(dy * 1.25))
|
| 44 |
+
|
| 45 |
+
# Absolute protection boundary: if we are deep inside the brain area (ellipse_dist <= 145),
|
| 46 |
+
# do not allow BFS to propagate here, preventing any leaks into the brain folds!
|
| 47 |
+
if ellipse_dist <= 145:
|
| 48 |
+
continue
|
| 49 |
+
|
| 50 |
+
# Background criteria: R, G, B are neutral light grey background
|
| 51 |
+
is_bg = (195 <= r <= 224) and (abs(r - g) <= 6) and (abs(g - b) <= 6)
|
| 52 |
+
|
| 53 |
+
if is_bg:
|
| 54 |
+
mask_pixels[x, y] = 0 # set to transparent
|
| 55 |
+
|
| 56 |
+
# Neighbors
|
| 57 |
+
for dx_n, dy_n in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
| 58 |
+
nx, ny = x + dx_n, y + dy_n
|
| 59 |
+
if 0 <= nx < width and 0 <= ny < height:
|
| 60 |
+
if (nx, ny) not in visited:
|
| 61 |
+
visited.add((nx, ny))
|
| 62 |
+
queue.append((nx, ny))
|
| 63 |
+
|
| 64 |
+
# 2. Erode the mask by 1 pixel to remove the thin grey boundary outline
|
| 65 |
+
eroded_mask = mask.filter(ImageFilter.MinFilter(3))
|
| 66 |
+
|
| 67 |
+
# 3. Apply a smooth Gaussian Blur of 1.2 pixels to anti-alias the cut-out edge
|
| 68 |
+
smooth_mask = eroded_mask.filter(ImageFilter.GaussianBlur(1.2))
|
| 69 |
+
|
| 70 |
+
# 4. Write the smoothed mask into the alpha channel of our original image
|
| 71 |
+
final_img = Image.new("RGBA", (width, height))
|
| 72 |
+
final_pixels = final_img.load()
|
| 73 |
+
smooth_pixels = smooth_mask.load()
|
| 74 |
+
|
| 75 |
+
for y in range(height):
|
| 76 |
+
for x in range(width):
|
| 77 |
+
r, g, b, a = pixels[x, y]
|
| 78 |
+
alpha_val = smooth_pixels[x, y]
|
| 79 |
+
final_pixels[x, y] = (r, g, b, alpha_val)
|
| 80 |
+
|
| 81 |
+
# 5. Save the perfect cutout
|
| 82 |
+
output_path = r"d:\PROJECTS\Soma\frontend\src\assets\brain\sleep_nobg.png"
|
| 83 |
+
final_img.save(output_path, "PNG")
|
| 84 |
+
print("SUCCESS: Leak-free transparent cutout sleep_nobg.png created successfully!")
|
| 85 |
+
|
| 86 |
+
if __name__ == "__main__":
|
| 87 |
+
main()
|