Komalpreet Kaur commited on
Commit
adfa6d0
·
unverified ·
1 Parent(s): 035ec20

feat: implement memory consolidation service with sleep cycle, add frontend visualization components, and integrate backend database orchestration.

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. app/api/auth_router.py +1 -1
  2. app/api/endpoints.py +101 -11
  3. app/db/chroma.py +25 -0
  4. app/db/session.py +1 -1
  5. app/main.py +8 -0
  6. app/services/brain_trace.py +1 -1
  7. app/services/memory.py +5 -5
  8. app/services/orchestrator.py +20 -22
  9. app/services/sleep_cycle.py +2 -2
  10. frontend/package-lock.json +133 -0
  11. frontend/package.json +1 -0
  12. frontend/src/App.css +222 -19
  13. frontend/src/App.jsx +125 -80
  14. frontend/src/assets/{hero.png → brain/Brain.png} +2 -2
  15. frontend/src/assets/brain/Brain_nobg.png +3 -0
  16. frontend/src/assets/brain/sleep.png +3 -0
  17. frontend/src/assets/brain/sleep_nobg.png +3 -0
  18. frontend/src/components/ChatPanel.css +184 -25
  19. frontend/src/components/ChatPanel.jsx +25 -9
  20. frontend/src/components/CognitiveBrain3DScene.css +110 -0
  21. frontend/src/components/CognitiveBrain3DScene.jsx +256 -0
  22. frontend/src/components/CognitiveBrainImageScene.jsx +43 -43
  23. frontend/src/components/CognitiveBrainScene.css +87 -42
  24. frontend/src/components/CognitiveDashboard.css +66 -64
  25. frontend/src/components/CognitiveDashboard.jsx +56 -15
  26. frontend/src/components/CognitiveTimeline.css +7 -11
  27. frontend/src/components/CognitiveTimeline.jsx +32 -5
  28. frontend/src/components/DreamSequence.css +402 -107
  29. frontend/src/components/DreamSequence.jsx +158 -56
  30. frontend/src/components/KnowledgeGraph.css +310 -103
  31. frontend/src/components/KnowledgeGraph.jsx +338 -82
  32. frontend/src/components/KnowledgeInput.css +331 -64
  33. frontend/src/components/KnowledgeInput.jsx +207 -26
  34. frontend/src/components/MemoryExplorer.css +405 -88
  35. frontend/src/components/MemoryExplorer.jsx +309 -108
  36. frontend/src/index.css +647 -1
  37. scratch/check_deps.py +15 -0
  38. scratch/check_graph_nodes.py +18 -0
  39. scratch/check_soma.py +10 -0
  40. scratch/color_to_alpha.py +63 -0
  41. scratch/copy_alpha.py +34 -0
  42. scratch/cutout_brain.py +66 -0
  43. scratch/ellipse_cutout.py +48 -0
  44. scratch/fill_holes.py +56 -0
  45. scratch/key_bg.py +29 -0
  46. scratch/morph_cutout.py +83 -0
  47. scratch/morph_cutout_v2.py +83 -0
  48. scratch/perfect_cutout.py +72 -0
  49. scratch/perfect_cutout_v2.py +71 -0
  50. 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 330 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()
 
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.1)
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.1)
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.1)
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.1)
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.1)
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.1)
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.3)
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.2)
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.2)
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.2)
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.1)
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("⚠️ Postgres connection failed. Falling back to SQLite.")
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(" 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,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" Cannot retrieve context (embeddings unavailable)")
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
- # Construct prompt with context & history
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
- prompt = f"""You are Soma, a brain-inspired cognitive AI that learns and grows through conversation.
89
- You have specialized memory layers that build up as you interact with the user.
90
 
91
- ### COGNITIVE CONTEXT:
92
- #### WORKING MEMORY (Recent Conversation):
93
- {history_str}
 
 
 
 
94
 
95
- #### SEMANTIC MEMORY (Knowledge Graph Relationships):
 
96
  {graph_str}
97
 
98
- #### SENSORY MEMORY (Retrieved Facts):
99
  {context_str}
 
100
 
101
- ### INSTRUCTIONS:
102
- 1. You are a helpful, intelligent AI assistant. Always respond naturally and conversationally.
103
- 2. If you have relevant SEMANTIC or SENSORY memories, use them to enrich your response.
104
- 3. If you have no stored memories yet, that is fine — respond using your general knowledge and the conversation context.
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([HumanMessage(content=prompt)])
113
- response_text = response.content
114
  except Exception as e:
115
- response_text = f"LLM Connection Error: {str(e)}"
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} summarized, {triples} triples, pruned {pruned} messages.")
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 Starting consolidation...")
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: #e5e5e5; /* Beige-ish background from design */
8
  overflow: hidden;
9
  }
10
 
11
  /* ── Sidebar ── */
12
  .soma-sidebar {
13
- width: 260px;
14
  background-color: transparent;
15
  display: flex;
16
  flex-direction: column;
17
- padding: 48px 32px;
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.5rem;
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.7rem;
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.95rem;
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: 12px;
110
- padding: 16px;
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.85rem;
131
  font-weight: 700;
132
  color: #1a1a1a;
133
  }
134
 
135
  .session-copy span {
136
- font-size: 0.7rem;
137
  color: #999;
 
138
  }
139
 
140
  /* ── Main Content Area ── */
@@ -146,10 +172,12 @@
146
 
147
  .page-canvas {
148
  flex: 1;
149
- background-color: #f7f7f7;
150
- border-radius: 48px; /* Larger radius from design */
151
- padding: 56px;
152
- box-shadow: 0 40px 100px rgba(0,0,0,0.05);
 
 
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: 56px;
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.7rem;
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.9rem;
198
  font-weight: 700;
199
  color: #1a1a1a;
200
  }
@@ -230,7 +258,7 @@
230
  }
231
 
232
  .feed-header {
233
- font-size: 0.65rem;
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, SleepSummary } from './components/DreamSequence';
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: 'Knowledge', icon: 'description' },
19
- { id: 'status', label: 'Status', icon: 'analytics' },
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
- // ── STEP 3: Message Sent (Transition to Cognition) ──
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
- setTrace(prev => [{ time: new Date().toLocaleTimeString([], { hour12: false }), ...data }, ...prev]);
 
 
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
- setTimeout(() => setCognitiveState(COGNITIVE_PHASES.IDLE), 1000);
161
  }
162
  };
163
 
164
  const handleKnowledgeSubmit = async (text) => {
165
- setKnowledgeStatus('Adding knowledge to memory...');
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(''), 3000);
175
  fetchVitals();
176
  } catch (error) {
177
- setKnowledgeStatus('Could not add knowledge right now.');
178
  }
179
  };
180
 
@@ -182,22 +178,35 @@ function App() {
182
  setCognitiveState('consolidating');
183
  setActivePage('sleep');
184
  setSleepPhaseIndex(0);
185
- setTimeout(() => setSleepPhaseIndex(1), 1200);
186
- setTimeout(() => setSleepPhaseIndex(2), 2400);
 
 
 
 
 
 
 
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 || 12,
193
- consolidated: 28,
194
- pruned: data.messages_pruned || 17,
195
- strengthened: 34
196
  });
197
  } catch (error) {
198
- setSleepSummary({ linked: 12, consolidated: 28, pruned: 17, strengthened: 34 });
 
 
199
  } finally {
200
- setTimeout(() => setActivePage('sleep-summary'), 3600);
 
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?.working_memory_pct?.toFixed(0) || '36', icon: 'neurology' },
215
- { label: 'Sensory Memory', value: vitals?.sensory_count || '1,248', icon: 'cloud_queue' },
216
- { label: 'Semantic Memory', value: vitals?.graph_node_count || '3,562', icon: 'device_hub' },
217
- { label: 'Neural Sparks', value: vitals?.recent_sparks || '24', icon: 'hub' },
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>Session: GUEST-7F3A</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>1. Console (Chat)</h2>
267
- <div style={{display: 'flex', alignItems: 'center', gap: '20px'}}>
268
- <div className="status-pill">
269
- <span className="label">Status</span>
270
- <div className="value">
271
- <div className={`status-dot ${cognitiveState !== COGNITIVE_PHASES.IDLE ? 'pulse' : ''}`} />
272
- <span style={{textTransform: 'capitalize'}}>{cognitiveState}</span>
273
- </div>
274
- </div>
275
- </div>
276
  </div>
277
 
278
- <div style={{display: 'flex', gap: '48px', flex: 1, position: 'relative', minHeight: 0}}>
279
- {/* Interaction Layer (Utility) */}
280
- <div style={{flex: 1.2, display: 'flex', flexDirection: 'column', zIndex: 50}}>
281
  <ChatPanel
282
  messages={messages}
283
  onSendMessage={handleSendMessage}
284
- isTyping={[COGNITIVE_PHASES.PERCEPTION, COGNITIVE_PHASES.ATTENTION, COGNITIVE_PHASES.RECALL, COGNITIVE_PHASES.REASONING, COGNITIVE_PHASES.RESPONDING].includes(cognitiveState)}
 
 
 
 
 
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', paddingBottom: '80px'}}>
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>2. Brain Activity</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
- {activePage === 'status' && (
321
- <section className="page-canvas fade-in">
322
- <div className="page-header"><h2>3. Status</h2></div>
323
- <CognitiveDashboard statusText={cognitiveState} stats={stats} />
324
- </section>
 
 
 
 
 
 
 
 
 
325
  )}
326
 
327
  {activePage === 'memory' && (
328
  <section className="page-canvas fade-in">
329
- <div className="page-header"><h2>4. Memory View</h2></div>
330
  <MemoryExplorer />
331
  </section>
332
  )}
333
 
334
  {activePage === 'graph' && (
335
  <section className="page-canvas fade-in">
336
- <div className="page-header"><h2>5. Graph View</h2></div>
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>6. Knowledge Input</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
- <section className="page-canvas fade-in">
355
- <div className="page-header"><h2>7. Sleep (Consolidation)</h2></div>
356
- <SleepProgress phaseIndex={sleepPhaseIndex} />
357
- </section>
358
- )}
359
-
360
- {activePage === 'sleep-summary' && (
361
- <section className="page-canvas fade-in">
362
- <div className="page-header"><h2>8. Consolidation Summary</h2></div>
363
- <SleepSummary summary={sleepSummary} onClose={() => setActivePage('knowledge')} />
364
- </section>
 
 
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

  • SHA256: f221b26602aeb584876d6750c04a20265616b79d1ced5de53e876c724ebd638d
  • Pointer size: 132 Bytes
  • Size of remote file: 1.77 MB
frontend/src/assets/brain/sleep.png ADDED

Git LFS Details

  • SHA256: 4978ff9f2308ea907367eff682a86cc9ba62c4ac71d2470c9949ffa10033a606
  • Pointer size: 132 Bytes
  • Size of remote file: 2.01 MB
frontend/src/assets/brain/sleep_nobg.png ADDED

Git LFS Details

  • SHA256: 75eaf72c0dbf9ec37e148cb97274bd046e97855998d88ebd5cb3cea8e883343e
  • Pointer size: 132 Bytes
  • Size of remote file: 2.61 MB
frontend/src/components/ChatPanel.css CHANGED
@@ -12,22 +12,68 @@
12
  padding-right: 12px;
13
  display: flex;
14
  flex-direction: column;
15
- gap: 32px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
 
18
  .chat-bubble-group {
19
  display: flex;
20
  gap: 20px;
21
- max-width: 90%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
24
  .avatar-wrap {
25
  flex-shrink: 0;
26
  }
27
 
 
 
 
 
28
  .chat-avatar {
29
- width: 36px;
30
- height: 36px;
 
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: 20px;
42
  color: #888;
43
  }
44
 
45
  .bubble-body {
46
  background-color: white; /* Clearer white for premium look */
47
- padding: 24px 28px;
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: 10px;
56
  }
57
 
58
  .bubble-meta strong {
59
- font-size: 0.85rem;
60
  font-weight: 700;
61
  color: #1a1a1a;
62
  letter-spacing: -0.01em;
63
  }
64
 
65
  .bubble-text {
66
- font-size: 1rem;
67
  line-height: 1.6;
68
  color: #444;
 
69
  }
70
 
71
  .bubble-time {
72
- margin-top: 12px;
73
- font-size: 0.75rem;
74
  color: #bbb;
75
  font-weight: 500;
76
  }
77
 
 
 
 
 
78
  /* ── Input Row ── */
79
  .chat-input-row {
80
  display: flex;
81
- gap: 16px;
82
  align-items: center;
 
83
  }
84
 
85
  .input-field-wrap {
86
  flex: 1;
87
- background-color: #eee;
88
- border-radius: 18px;
89
- padding: 0 24px;
 
 
90
  display: flex;
91
  align-items: center;
92
- height: 64px;
93
  transition: all 0.2s;
94
  }
95
 
96
  .input-field-wrap:focus-within {
97
- background-color: #e8e8e8;
98
- box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
99
  }
100
 
101
  .input-field-wrap input {
@@ -103,31 +163,58 @@
103
  background: transparent;
104
  border: none;
105
  outline: none;
106
- font-size: 1.05rem;
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: 64px;
117
- height: 64px;
118
  background-color: #ff6b35;
119
- border-radius: 18px;
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.25);
126
  }
127
 
128
  .send-btn:hover:not(:disabled) {
129
  transform: translateY(-2px);
130
- box-shadow: 0 14px 30px rgba(255, 107, 53, 0.35);
131
  }
132
 
133
  .send-btn:active:not(:disabled) {
@@ -141,7 +228,7 @@
141
  }
142
 
143
  .send-btn .material-icons {
144
- font-size: 28px;
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="avatar-wrap">
43
- <div className="chat-avatar">
44
- {msg.role === 'user' ? (
45
- <span className="material-icons">account_circle</span>
46
- ) : (
47
- <span className="material-icons">lens_blur</span>
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 brainBase from '../assets/brain/brain_base.png';
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', // Fallback to prefrontal for language for now
26
- inhibition: 'thalamus',
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 2: Cognitive Signals (Floating Ambient Labels) */}
37
- <div className="neural-signals">
38
- <div className={`signal-node prefrontal ${activeRegion === 'prefrontal' ? 'active' : ''} ${isState('reasoning') ? 'hot' : ''}`}>
39
- <div className="signal-dot"></div>
40
- <div className="signal-copy">
41
- <strong>Prefrontal</strong>
42
- <span>Reasoning & Reflection</span>
 
 
 
43
  </div>
44
- </div>
45
 
46
- <div className={`signal-node parietal ${activeRegion === 'sensory' ? 'active' : ''}`}>
47
- <div className="signal-dot"></div>
48
- <div className="signal-copy">
49
- <strong>Parietal</strong>
50
- <span>Perception & Sensory</span>
 
51
  </div>
52
- </div>
53
 
54
- <div className={`signal-node temporal ${activeRegion === 'hippocampus' ? 'active' : ''}`}>
55
- <div className="signal-dot"></div>
56
- <div className="signal-copy">
57
- <strong>Temporal</strong>
58
- <span>Memory & Association</span>
 
59
  </div>
60
- </div>
61
- </div>
62
 
63
- {/* Layer 1: Brain Core (Layered Overlays) */}
64
- <div className="brain-core">
65
- {/* Base Layer */}
66
- <img src={brainBase} className="brain-layer base" alt="Neural Base" />
67
-
68
- {/* Region Overlays */}
69
- <img src={prefrontalImg} className={`brain-layer region ${activeRegion === 'prefrontal' ? 'active' : ''}`} alt="Prefrontal Cortex" />
70
- <img src={sensoryImg} className={`brain-layer region ${activeRegion === 'sensory' ? 'active' : ''}`} alt="Sensory Cortex" />
71
- <img src={hippocampusImg} className={`brain-layer region ${activeRegion === 'hippocampus' ? 'active' : ''}`} alt="Hippocampus" />
72
- <img src={thalamusImg} className={`brain-layer region ${activeRegion === 'thalamus' ? 'active' : ''}`} alt="Thalamus" />
73
- <img src={amygdalaImg} className={`brain-layer region ${activeRegion === 'amygdala' ? 'active' : ''}`} alt="Amygdala" />
 
 
74
 
75
- {/* State-specific Glow Hubs (Subtle Ambient Glow behind regions) */}
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
- {/* Ambient Breathing (Idle) */}
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: 500px;
6
  position: relative;
7
  display: flex;
8
  align-items: center;
9
  justify-content: center;
10
- perspective: 1000px;
11
  }
12
 
13
  .brain-core {
14
  position: relative;
15
- width: 480px;
16
- height: 480px; /* Force square for layering */
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: auto;
27
- transition: opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1), filter 0.8s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
29
 
30
- .brain-layer.base {
 
31
  z-index: 1;
32
- filter: contrast(1.1) brightness(0.9);
33
  }
34
 
35
- .brain-layer.region {
36
  z-index: 2;
37
- opacity: 0;
 
 
 
 
 
 
 
 
 
38
  }
39
 
40
  .brain-layer.region.active {
41
- opacity: 1;
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.05) 0%, transparent 75%);
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.95); opacity: 0.3; }
57
- 50% { transform: scale(1.05); opacity: 0.6; }
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.5s cubic-bezier(0.16, 1, 0.3, 1);
75
- filter: grayscale(1) blur(1px);
76
  }
77
 
78
  .signal-node.active {
79
  opacity: 1;
80
  filter: grayscale(0) blur(0);
81
- transform: scale(1.15) translateX(10px);
 
82
  }
83
 
84
  .signal-node.hot {
@@ -88,74 +111,96 @@
88
  }
89
 
90
  @keyframes signal-hot-throb {
91
- 0%, 100% { transform: scale(1.15) translateX(10px); filter: brightness(1); }
92
- 50% { transform: scale(1.2) translateX(15px); filter: brightness(1.4); }
93
  }
94
 
95
  .signal-dot {
96
  width: 10px;
97
  height: 10px;
98
- background-color: #ff6b35;
99
  border-radius: 50%;
100
- box-shadow: 0 0 20px rgba(255, 107, 53, 0.6);
 
 
 
 
 
101
  }
102
 
 
 
 
 
 
 
 
 
 
 
 
103
  .signal-copy {
104
  display: flex;
105
  flex-direction: column;
106
  }
107
 
108
  .signal-copy strong {
109
- font-size: 0.9rem;
110
  font-weight: 800;
111
  color: #1a1a1a;
112
  line-height: 1.1;
 
 
 
 
 
113
  }
114
 
115
  .signal-copy span {
116
- font-size: 0.75rem;
117
  color: #999;
118
  text-transform: uppercase;
119
  letter-spacing: 0.08em;
120
  font-weight: 600;
121
  }
122
 
123
- .prefrontal { top: 22%; left: -20px; }
124
- .parietal { top: 15%; right: -20px; flex-direction: row-reverse; text-align: right; }
125
- .temporal { bottom: 20%; left: -20px; }
 
126
 
127
- /* ── State Glow Hubs (Subtle Backglow) ── */
128
  .glow-hub {
129
  position: absolute;
130
- width: 250px;
131
- height: 250px;
132
  border-radius: 50%;
133
- filter: blur(60px);
134
  opacity: 0;
135
- transition: all 1s ease;
136
  pointer-events: none;
137
  z-index: 0;
138
  }
139
 
140
  .glow-hub.visible {
141
- opacity: 0.3;
142
  }
143
 
144
- .prefrontal-glow { top: 5%; left: 10%; background: radial-gradient(circle, rgba(255, 107, 53, 0.4) 0%, transparent 70%); }
145
- .parietal-glow { top: 5%; right: 10%; background: radial-gradient(circle, rgba(59, 130, 246, 0.3) 0%, transparent 70%); }
146
- .temporal-glow { bottom: 10%; left: 20%; background: radial-gradient(circle, rgba(16, 185, 129, 0.3) 0%, transparent 70%); }
147
- .thalamus-glow { top: 20%; left: 30%; background: radial-gradient(circle, rgba(245, 158, 11, 0.4) 0%, transparent 70%); }
148
 
149
  /* ── Listening/Responding Shimmer ── */
150
  .listening-particles {
151
  position: absolute;
152
  inset: 0;
153
- background: radial-gradient(circle, rgba(255, 107, 53, 0.05) 0%, transparent 85%);
154
- animation: brain-shimmer 2s infinite alternate;
155
  z-index: 5;
156
  }
157
 
158
  @keyframes brain-shimmer {
159
- from { opacity: 0.2; transform: scale(1); }
160
- to { opacity: 0.5; transform: scale(1.05); }
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
- justify-content: center;
14
- position: relative;
 
15
  }
16
 
17
- /* ── Status Cards ── */
18
  .status-sidebar {
19
  display: flex;
20
  flex-direction: column;
21
- gap: 32px;
22
  }
23
 
24
  .status-card {
25
- background-color: white;
26
- border-radius: 32px;
27
- padding: 32px;
28
- box-shadow: 0 4px 20px rgba(0,0,0,0.02);
29
- border: 1px solid rgba(0,0,0,0.03);
 
30
  }
31
 
32
  .status-card h3 {
33
- font-size: 0.75rem;
34
  text-transform: uppercase;
35
- letter-spacing: 0.1em;
36
  color: #999;
37
- margin-bottom: 24px;
38
- font-weight: 700;
39
  }
40
 
41
- /* ── Waves & Charts ── */
42
  .system-chart {
43
- margin-bottom: 24px;
44
- position: relative;
45
- }
46
-
47
- .system-chart-svg {
48
- width: 100%;
49
- height: 100px;
50
  }
51
 
52
  .system-chart-svg path {
53
  fill: none;
54
  stroke: #ff6b35;
55
- stroke-width: 2.5;
56
  stroke-linecap: round;
57
- opacity: 0.8;
 
 
 
 
 
 
58
  }
59
 
60
  .system-chart-svg path.secondary {
61
  stroke: #3b82f6;
62
  stroke-width: 1.5;
63
- opacity: 0.3;
64
  }
65
 
66
  .chart-labels {
@@ -71,84 +70,87 @@
71
  }
72
 
73
  .chart-labels span {
74
- font-size: 0.7rem;
 
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: 24px;
84
- border-top: 1px solid #f0f0f0;
85
  }
86
 
87
  .current-state span {
88
- font-size: 0.85rem;
89
- color: #888;
90
- font-weight: 600;
 
 
91
  }
92
 
93
  .current-state strong {
94
- font-size: 1.2rem;
95
  color: #ff6b35;
96
- font-weight: 700;
97
  text-transform: capitalize;
98
  }
99
 
100
- /* ── Metrics ── */
101
  .metric-grid {
102
  display: grid;
103
  grid-template-columns: 1fr 1fr;
104
- gap: 20px;
105
  }
106
 
107
  .metric-item {
108
  display: flex;
109
- flex-direction: column;
110
- gap: 12px;
111
- padding: 24px;
112
- background-color: #f9f9f9;
113
  border-radius: 20px;
114
- transition: transform 0.3s ease;
115
- }
116
-
117
- .metric-item:hover {
118
- transform: translateY(-4px);
119
  }
120
 
121
  .metric-icon-wrap {
122
- width: 40px;
123
- height: 40px;
 
124
  border-radius: 12px;
125
- background-color: white;
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: 20px;
135
  }
136
 
137
  .metric-info {
138
  display: flex;
139
  flex-direction: column;
 
140
  }
141
 
142
  .metric-info label {
143
- font-size: 0.75rem;
 
 
144
  color: #999;
145
- font-weight: 600;
146
- margin-bottom: 2px;
147
  }
148
 
149
- .metric-info strong {
150
- font-size: 1.3rem;
151
  color: #1a1a1a;
152
- font-weight: 700;
153
- letter-spacing: -0.02em;
 
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 CognitiveBrainImageScene from './CognitiveBrainImageScene';
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>Current Status</span>
17
  <strong>{statusText}</strong>
18
  </div>
19
  </div>
@@ -39,17 +35,62 @@ function CognitiveDashboard({ statusText, stats }) {
39
  );
40
  }
41
 
42
- function SystemStatusChart() {
43
- // SVG points for smooth overlapping waves
44
- const wave1 = "M0,40 Q30,5 60,40 T120,40 T180,40 T240,40 T300,40";
45
- const wave2 = "M0,50 Q40,25 80,50 T160,50 T240,50 T320,50 T400,50";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  const labels = ['ALPHA', 'BETA', 'GAMMA', 'DELTA', 'THETA'];
47
 
48
  return (
49
- <div className="system-chart">
50
  <svg viewBox="0 0 300 80" className="system-chart-svg">
51
- <path d={wave2} className="secondary" />
52
- <path d={wave1} />
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: 32px;
7
- padding: 16px 0;
8
  }
9
 
10
  .timeline-row {
@@ -19,7 +19,7 @@
19
  position: absolute;
20
  left: 65px; /* Alignment with dot */
21
  top: 12px;
22
- bottom: -44px;
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.75rem;
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
- background-color: white;
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.95rem;
93
  font-weight: 700;
94
  color: #1a1a1a;
95
  letter-spacing: -0.01em;
96
  }
97
 
98
  .phase-desc {
99
- font-size: 0.9rem;
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
- output: { icon: 'chat_bubble_outline', color: '#8ab892', label: 'Output' }
 
 
9
  };
10
 
11
  function CognitiveTimeline({ trace }) {
12
- if (!trace || trace.length === 0) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  return (
14
  <div className="timeline-empty">
15
- No cognitive activity recorded yet...
 
16
  </div>
17
  );
18
  }
19
 
20
  return (
21
  <div className="timeline-container fade-in">
22
- {trace.map((item, index) => {
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
- /* DreamSequence.css - Refined for Page 7 & 8 */
2
 
3
- .sleep-layout {
4
  display: flex;
5
  flex-direction: column;
6
  align-items: center;
7
  justify-content: center;
 
 
 
8
  height: 100%;
9
- text-align: center;
 
 
 
10
  }
11
 
12
- .sleep-brain-wrap {
13
- position: relative;
14
- margin-bottom: 56px;
15
- transform: scale(1.1);
16
  }
17
 
18
- /* ── Progress ── */
19
- .sleep-copy h3 {
20
- font-family: var(--font-display);
21
- font-size: 2.2rem;
22
  font-weight: 700;
23
- margin-bottom: 12px;
24
  color: #1a1a1a;
 
25
  letter-spacing: -0.01em;
26
  }
27
 
28
- .sleep-copy p {
29
- font-size: 1.1rem;
30
- color: #888;
31
- margin-bottom: 64px;
 
 
 
 
 
 
 
 
 
 
 
 
32
  }
33
 
34
- .sleep-steps {
 
35
  display: flex;
36
- gap: 80px;
37
  justify-content: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  width: 100%;
39
- max-width: 800px;
40
- padding-top: 48px;
41
- border-top: 1px solid #f0f0f0;
 
 
 
 
42
  }
43
 
44
- .sleep-step {
45
  display: flex;
46
  align-items: center;
47
  gap: 16px;
 
 
 
48
  }
49
 
50
- .sleep-step-dot {
51
- width: 28px;
52
- height: 28px;
 
 
 
 
53
  border-radius: 50%;
54
- border: 2px solid #ddd;
55
  display: flex;
56
  align-items: center;
57
  justify-content: center;
58
- font-size: 0.8rem;
59
- font-weight: 700;
60
- color: #bbb;
61
- transition: all 0.3s;
62
  }
63
 
64
- .sleep-step-dot.active {
65
- border-color: #ff6b35;
66
- color: #ff6b35;
67
- box-shadow: 0 0 15px rgba(255, 107, 53, 0.2);
68
  }
69
 
70
- .sleep-step-dot.completed {
71
- border-color: #10b981;
72
- color: #10b981;
73
- background-color: transparent;
74
  }
75
 
76
- .sleep-step-label {
77
- font-size: 1rem;
78
- color: #999;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  font-weight: 600;
80
  }
81
 
82
- .sleep-step-label.active {
83
  color: #1a1a1a;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  }
85
 
86
- /* ── Summary Card (Page 8) ── */
87
- .summary-overlay {
88
  position: absolute;
89
  top: 0;
90
  left: 0;
91
  right: 0;
92
  bottom: 0;
93
- background-color: rgba(229, 229, 229, 0.9);
94
  display: flex;
 
95
  align-items: center;
96
  justify-content: center;
97
  z-index: 100;
98
- backdrop-filter: blur(12px);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
 
101
- .summary-card {
 
102
  width: 100%;
103
- max-width: 520px;
104
- background-color: white;
105
- border-radius: 40px;
106
- padding: 48px;
107
- box-shadow: 0 30px 80px rgba(0,0,0,0.12);
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: 32px;
116
- right: 32px;
117
- color: #bbb;
118
- font-size: 1.8rem;
119
  background: none;
 
 
 
 
 
120
  }
121
 
122
- .summary-close:hover {
 
 
 
 
123
  color: #1a1a1a;
124
  }
125
 
126
- .summary-header {
127
  display: flex;
128
- gap: 24px;
129
- margin-bottom: 40px;
 
130
  }
131
 
132
- .summary-header-icon {
133
- width: 64px;
134
- height: 64px;
135
- background-color: #f5f5f5;
136
- border-radius: 20px;
137
  display: flex;
138
  align-items: center;
139
  justify-content: center;
140
- color: #1a1a1a;
141
  }
142
 
143
- .summary-header-icon .material-icons {
144
- font-size: 32px;
145
  }
146
 
147
- .summary-header-copy h3 {
148
- font-family: var(--font-display);
149
- font-size: 1.6rem;
150
  font-weight: 700;
151
- margin-bottom: 4px;
152
  color: #1a1a1a;
 
 
153
  }
154
 
155
- .summary-header-copy p {
156
- font-size: 1rem;
157
- color: #888;
 
 
158
  }
159
 
160
- .summary-list {
161
  display: flex;
162
  flex-direction: column;
163
- gap: 16px;
164
- margin-bottom: 40px;
165
  }
166
 
167
- .summary-item {
168
  display: flex;
169
  align-items: center;
170
- gap: 20px;
171
- padding: 20px;
172
- border-radius: 20px;
173
- background-color: #f8f8f8;
174
- border: 1px solid rgba(0,0,0,0.02);
175
  }
176
 
177
- .summary-item-icon {
178
- width: 44px;
179
- height: 44px;
180
- border-radius: 14px;
181
  display: flex;
182
  align-items: center;
183
  justify-content: center;
 
 
 
 
 
184
  }
185
 
186
- .summary-item-copy strong {
187
  display: block;
188
- font-size: 1.05rem;
189
  font-weight: 700;
190
  color: #1a1a1a;
191
- margin-bottom: 2px;
192
  }
193
 
194
- .summary-item-copy span {
195
- font-size: 0.85rem;
196
- color: #888;
 
197
  font-weight: 500;
198
  }
199
 
200
- .summary-view-btn {
 
 
 
 
 
 
201
  width: 100%;
202
- height: 64px;
203
  background-color: #fff1eb;
204
  color: #ff6b35;
205
- border-radius: 20px;
 
206
  font-weight: 700;
207
- font-size: 1.1rem;
208
- transition: all 0.2s;
 
 
 
 
209
  }
210
 
211
- .summary-view-btn:hover {
212
  background-color: #ffe4d9;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  transform: translateY(-2px);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  }
215
 
216
- /* Item Tones */
217
- .summary-item-icon.green { color: #10b981; background-color: #e6f7f0; }
218
- .summary-item-icon.teal { color: #0d9488; background-color: #e0f2f1; }
219
- .summary-item-icon.orange { color: #ff6b35; background-color: #fff1eb; }
220
- .summary-item-icon.amber { color: #f59e0b; background-color: #fff8e1; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 CognitiveBrainImageScene from './CognitiveBrainImageScene';
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-layout fade-in">
9
- <div className="sleep-brain-wrap">
10
- <CognitiveBrainImageScene scale={1.3} />
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
- return (
22
- <div key={step} className="sleep-step">
23
- <div className={`sleep-step-dot ${completed ? 'completed' : active ? 'active' : ''}`}>
24
- {completed ? <span className="material-icons" style={{fontSize: '14px'}}>check</span> : (index + 1)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  </div>
26
- <div className={`sleep-step-label ${active ? 'active' : ''}`}>{step}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  </div>
28
- );
29
- })}
30
- </div>
31
- </div>
32
- );
33
- }
 
34
 
35
- export function SleepSummary({ summary, onClose }) {
36
- const data = summary || { linked: 12, consolidated: 28, pruned: 17, strengthened: 34 };
37
- const items = [
38
- { label: 'Linked Together', value: `${data.linked} new connections created`, icon: 'hub', tone: 'green' },
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
- return (
45
- <div className="summary-overlay fade-in">
46
- <div className="summary-card">
47
- <button className="summary-close" onClick={onClose}><span className="material-icons">close</span></button>
48
-
49
- <div className="summary-header">
50
- <div className="summary-header-icon">
51
- <span className="material-icons">bedtime</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  </div>
53
- <div className="summary-header-copy">
54
- <h3>Sleep Cycle Complete</h3>
55
- <p>Memory consolidation finished</p>
 
 
 
 
 
 
 
 
 
 
 
56
  </div>
57
  </div>
58
 
59
- <div className="summary-list">
60
- {items.map((item) => (
61
- <div key={item.label} className="summary-item">
62
- <div className={`summary-item-icon ${item.tone}`}>
63
- <span className="material-icons">{item.icon}</span>
64
- </div>
65
- <div className="summary-item-copy">
66
- <strong>{item.label}</strong>
67
- <span>{item.value}</span>
68
- </div>
69
- </div>
70
- ))}
 
 
71
  </div>
72
 
73
- <button className="summary-view-btn" onClick={onClose}>View Summary</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 - Refined for Page 5 (Biological Mesh) */
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
- margin-bottom: 32px;
 
14
  }
15
 
16
- .graph-select {
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
- font-size: 1rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  color: #1a1a1a;
26
- font-weight: 600;
 
 
 
 
 
 
27
  }
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  .graph-actions {
30
  display: flex;
31
- gap: 12px;
32
  }
33
 
34
  .graph-icon-button {
35
- width: 56px;
36
- height: 56px;
37
- background-color: #eee;
38
- border-radius: 18px;
 
 
39
  display: flex;
40
  align-items: center;
41
  justify-content: center;
42
- color: #888;
43
  transition: all 0.2s;
 
 
 
 
 
 
44
  }
45
 
46
  .graph-icon-button:hover {
47
- background-color: #e8e8e8;
48
- color: #1a1a1a;
 
 
49
  }
50
 
51
- /* ── Network ── */
52
- .graph-network {
53
  flex: 1;
54
  position: relative;
55
- display: flex;
56
- align-items: center;
57
- justify-content: center;
58
  overflow: hidden;
 
 
 
 
 
 
59
  }
60
 
61
- .graph-links {
62
  position: absolute;
63
- width: 100%;
64
- height: 100%;
 
 
 
 
65
  z-index: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  pointer-events: none;
67
  }
68
 
69
- .graph-links line {
70
- stroke: rgba(0, 0, 0, 0.05);
71
- stroke-width: 1;
72
- stroke-dasharray: 2 10;
 
 
 
73
  }
74
 
75
- .graph-node {
 
 
 
 
 
 
76
  position: absolute;
 
 
 
77
  display: flex;
78
  flex-direction: column;
79
  align-items: center;
80
- transform: translate(-50%, -50%);
81
- z-index: 1;
82
- transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1);
 
 
 
 
 
 
 
 
 
 
83
  }
84
 
85
- .graph-center {
86
- left: 50%;
87
- top: 50%;
88
  }
89
 
90
- .graph-node-core {
91
- width: 100px;
92
- height: 100px;
93
- background-color: #1a1a1a;
94
- border-radius: 50%;
 
95
  display: flex;
96
- align-items: center;
97
  justify-content: center;
98
- color: white;
99
- font-size: 2.2rem;
100
- box-shadow: 0 0 50px rgba(0, 0, 0, 0.15);
101
- margin-bottom: 16px;
102
- animation: core-float 6s infinite ease-in-out;
 
 
 
103
  }
104
 
105
- @keyframes core-float {
106
- 0%, 100% { transform: scale(1); }
107
- 50% { transform: scale(1.05); filter: brightness(1.1); }
 
 
 
 
 
 
108
  }
109
 
110
- .graph-orbit {
111
- width: 44px;
112
- height: 44px;
113
- background-color: white;
114
  border-radius: 50%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  display: flex;
116
  align-items: center;
117
- justify-content: center;
118
- box-shadow: 0 8px 24px rgba(0,0,0,0.04);
119
- border: 1px solid rgba(0,0,0,0.03);
120
- animation: orbit-float 8s infinite ease-in-out;
 
 
 
 
121
  }
122
 
123
- @keyframes orbit-float {
124
- 0%, 100% { transform: translateY(0); }
125
- 50% { transform: translateY(-10px); }
126
  }
127
 
128
- .graph-orbit strong {
129
- position: absolute;
130
- top: 56px;
131
- width: 140px;
132
- text-align: center;
133
- font-size: 0.85rem;
134
- color: #1a1a1a;
135
- font-weight: 700;
136
- letter-spacing: -0.01em;
137
  }
138
 
139
- .graph-node-dot {
140
- width: 10px;
141
- height: 10px;
142
- border-radius: 50%;
143
- background-color: #ff6b35;
144
- box-shadow: 0 0 12px rgba(255, 107, 53, 0.3);
145
  }
146
 
147
- /* ── Legend ── */
148
- .graph-legend {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  display: flex;
150
- gap: 32px;
151
- justify-content: center;
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
- .legend-item {
162
  display: flex;
163
  align-items: center;
164
- gap: 10px;
165
- font-size: 0.75rem;
166
- color: #888;
 
 
 
167
  font-weight: 700;
168
- text-transform: uppercase;
169
- letter-spacing: 0.05em;
170
  }
171
 
172
- .legend-item::before {
173
- content: '';
174
- width: 8px;
175
- height: 8px;
176
- border-radius: 50%;
 
 
 
 
 
 
 
 
 
 
 
177
  }
178
 
179
- .legend-item.concept::before { background-color: #3b82f6; }
180
- .legend-item.method::before { background-color: #10b981; }
181
- .legend-item.metric::before { background-color: #ff6b35; }
182
- .legend-item.relationship::before { background-color: #eee; height: 2px; border-radius: 0; }
183
- .legend-item.entity::before { background-color: #f59e0b; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- const FALLBACK_GRAPH = {
6
- center: 'Transformer Models',
7
  nodes: [
8
- { label: 'Attention Mechanism', tone: 'orange' },
9
- { label: 'Long-range Dependencies', tone: 'orange' },
10
- { label: 'Efficiency', tone: 'green' },
11
- { label: 'Expressivity', tone: 'orange' },
12
- { label: 'Sparse Patterns', tone: 'orange' },
13
- { label: 'Recurrence', tone: 'orange' }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 [graph, setGraph] = useState(FALLBACK_GRAPH);
28
  const [loading, setLoading] = useState(true);
 
 
 
 
 
 
 
29
 
 
30
  useEffect(() => {
31
- const fetchGraph = async () => {
32
- setLoading(true);
33
- try {
34
- const res = await apiFetch('/api/v1/graph');
35
- if (!res.ok) { setLoading(false); return; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- const data = await res.json();
38
- const labels = Array.from(new Set((data.nodes || []).map(n => n.label || n.id).filter(Boolean)));
 
 
 
39
 
40
- if (labels.length > 0) {
41
- const [center, ...rest] = labels;
42
- const nodes = rest.slice(0, 6).map(l => ({
43
- label: l,
44
- tone: l.toLowerCase().includes('efficiency') ? 'green' : 'orange'
45
- }));
46
- while (nodes.length < 6) {
47
- nodes.push(FALLBACK_GRAPH.nodes[nodes.length]);
48
- }
49
- setGraph({ center, nodes });
 
 
 
 
 
 
 
 
 
 
 
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
- <button className="graph-select">
65
- <span>Concept Map</span>
66
- <span className="material-icons">expand_more</span>
67
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  <div className="graph-actions">
69
- <button className="graph-icon-button"><span className="material-icons">open_in_full</span></button>
70
- <button className="graph-icon-button"><span className="material-icons">fit_screen</span></button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  </div>
72
  </div>
73
 
74
- <div className="graph-network">
75
- <svg viewBox="0 0 100 100" className="graph-links">
76
- {POSITIONS.map((pos, i) => (
77
- <line key={i} x1="50" y1="50" x2={pos.x} y2={pos.y} />
78
- ))}
79
- {POSITIONS.map((pos, i) => {
80
- const next = POSITIONS[(i + 1) % POSITIONS.length];
81
- return <line key={`r-${i}`} x1={pos.x} y1={pos.y} x2={next.x} y2={next.y} className="ring-line" />;
82
- })}
83
- </svg>
84
-
85
- <div className="graph-node graph-center">
86
- <div className="graph-node-core">∿</div>
87
- <strong>{graph.center}</strong>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  </div>
89
 
90
- {graph.nodes.map((node, i) => {
91
- const pos = POSITIONS[i];
92
- return (
93
- <div
94
- key={i}
95
- className={`graph-node graph-orbit ${node.tone}`}
96
- style={{ left: `${pos.x}%`, top: `${pos.y}%` }}
97
- >
98
- <div className="graph-node-dot" />
99
- <strong>{node.label}</strong>
100
- </div>
101
- );
102
- })}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- {loading && <div className="graph-loading" style={{ position: 'absolute', bottom: '10px', fontSize: '0.7rem' }}>Refreshing graph...</div>}
105
- </div>
 
 
 
 
 
106
 
107
- <div className="graph-legend">
108
- <span className="legend-item concept">Concept</span>
109
- <span className="legend-item method">Method</span>
110
- <span className="legend-item metric">Metric</span>
111
- <span className="legend-item relationship">Relationship</span>
112
- <span className="legend-item entity">Entity</span>
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 - Refined for Page 6 */
2
 
3
- .knowledge-layout {
 
 
 
 
 
 
 
 
 
 
4
  display: flex;
5
  flex-direction: column;
 
 
 
 
 
 
 
6
  align-items: center;
7
- justify-content: center;
8
- text-align: center;
9
- height: 100%;
10
  }
11
 
12
- .knowledge-icon-large {
13
- width: 88px;
14
- height: 88px;
15
- background-color: #eee;
16
- border-radius: 24px;
 
17
  display: flex;
18
  align-items: center;
19
  justify-content: center;
20
- margin-bottom: 32px;
21
  }
22
 
23
- .knowledge-icon-large .material-icons {
24
- font-size: 44px;
25
- color: #1a1a1a;
26
  }
27
 
28
- .knowledge-layout h3 {
29
  font-family: var(--font-display);
30
- font-size: 2rem;
31
- font-weight: 700;
32
- margin-bottom: 12px;
33
  color: #1a1a1a;
 
34
  }
35
 
36
- .knowledge-layout p {
37
- font-size: 1.1rem;
38
- color: #888;
39
- margin-bottom: 48px;
40
- max-width: 500px;
41
  }
42
 
43
- .knowledge-composer {
44
- width: 100%;
45
- max-width: 760px;
46
- background-color: white;
47
- border-radius: 32px;
48
- padding: 40px;
49
- box-shadow: 0 10px 40px rgba(0,0,0,0.03);
50
- border: 1px solid rgba(0,0,0,0.02);
 
 
 
51
  }
52
 
53
- .knowledge-composer textarea {
 
54
  width: 100%;
55
- min-height: 280px;
56
  background: transparent;
57
  border: none;
58
  outline: none;
59
- font-size: 1.15rem;
 
60
  color: #1a1a1a;
61
  resize: none;
62
- line-height: 1.6;
63
  font-weight: 500;
64
  }
65
 
66
- .knowledge-composer textarea::placeholder {
67
- color: #ccc;
68
- }
69
-
70
- .knowledge-composer-footer {
71
  display: flex;
72
  justify-content: space-between;
73
  align-items: center;
74
- margin-top: 32px;
75
- padding-top: 24px;
76
- border-top: 1px solid #f0f0f0;
 
 
 
77
  }
78
 
79
  .char-count {
80
- font-size: 0.85rem;
 
81
  color: #bbb;
82
- font-weight: 600;
83
  text-transform: uppercase;
84
- letter-spacing: 0.05em;
85
  }
86
 
87
- .add-memory-btn {
88
- background-color: #ff6b35;
 
 
 
 
 
 
 
89
  color: white;
90
- padding: 16px 36px;
91
- border-radius: 18px;
92
  font-weight: 700;
93
- font-size: 1rem;
94
- transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
95
- box-shadow: 0 10px 25px rgba(255, 107, 53, 0.2);
 
 
 
96
  }
97
 
98
- .add-memory-btn:hover:not(:disabled) {
99
  transform: translateY(-2px);
100
- box-shadow: 0 14px 30px rgba(255, 107, 53, 0.3);
101
  }
102
 
103
- .add-memory-btn:disabled {
104
- opacity: 0.5;
105
- cursor: not-allowed;
106
- filter: grayscale(0.5);
 
 
 
 
 
 
 
 
107
  }
108
 
109
- .knowledge-status-text {
110
- margin-top: 32px;
111
- font-size: 0.95rem;
112
- color: #ff6b35;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  font-weight: 700;
114
- animation: fadeIn 0.4s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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="knowledge-layout fade-in">
15
- <div className="knowledge-icon-large">
16
- <span className="material-icons">note_add</span>
17
- </div>
18
-
19
- <h3>Add knowledge to Soma</h3>
20
- <p>Paste text or notes for Soma to remember and integrate into its memory.</p>
21
-
22
- <div className="knowledge-composer">
23
- <textarea
24
- value={text}
25
- onChange={(e) => setText(e.target.value)}
26
- placeholder="Paste anything here... (articles, notes, ideas, research)"
27
- maxLength={10000}
28
- />
29
- <div className="knowledge-composer-footer">
30
- <span className="char-count">{text.length} / 10000</span>
31
- <button
32
- className="add-memory-btn"
33
- onClick={handleSubmit}
34
- disabled={!text.trim() || isBusy}
35
- >
36
- Add to Memory
37
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  </div>
39
  </div>
40
 
41
- {status && <div className="knowledge-status-text">{status}</div>}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 - Refined for Page 4 */
2
 
3
  .memory-view-grid {
4
  display: grid;
5
- grid-template-columns: 1fr 1.2fr;
6
- gap: 48px;
7
- height: 100%;
 
 
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
- gap: 16px;
20
- margin-bottom: 32px;
 
21
  }
22
 
23
- .memory-search {
24
- flex: 1;
25
- background-color: #eee;
26
- border-radius: 18px;
27
- padding: 0 20px;
28
  display: flex;
29
  align-items: center;
30
- gap: 12px;
31
- height: 56px;
 
32
  }
33
 
34
- .memory-search input {
 
 
 
 
35
  flex: 1;
36
  background: transparent;
37
  border: none;
38
  outline: none;
39
- font-size: 1rem;
40
  color: #1a1a1a;
41
  font-weight: 500;
42
  }
43
 
44
- .memory-search .material-icons {
45
- color: #aaa;
 
46
  }
47
 
48
- .memory-filter {
49
- background-color: #eee;
50
- border-radius: 18px;
51
- padding: 0 20px;
52
  display: flex;
53
- align-items: center;
54
- gap: 8px;
55
- height: 56px;
56
- font-size: 0.95rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  color: #1a1a1a;
58
- font-weight: 600;
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
- .memory-row {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  display: flex;
72
- align-items: flex-start;
73
  gap: 20px;
74
- padding: 24px;
75
- border-radius: 24px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  text-align: left;
77
- transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
78
  border: 1px solid transparent;
 
 
79
  }
80
 
81
  .memory-row:hover {
82
- background-color: rgba(0, 0, 0, 0.02);
83
  }
84
 
85
  .memory-row.active {
86
- background-color: white;
87
- box-shadow: 0 10px 30px rgba(0,0,0,0.04);
88
- border-color: rgba(0,0,0,0.03);
89
- transform: translateX(8px);
90
  }
91
 
92
  .memory-dot {
93
- width: 10px;
94
- height: 10px;
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: 1rem;
107
  font-weight: 700;
108
  color: #1a1a1a;
109
- margin-bottom: 6px;
110
- line-height: 1.3;
 
 
 
111
  }
112
 
113
  .memory-row-meta {
114
  display: flex;
115
- gap: 12px;
116
- font-size: 0.8rem;
117
- color: #999;
118
- font-weight: 500;
 
 
119
  }
120
 
121
- .memory-tag {
122
- font-size: 0.75rem;
123
- color: #bbb;
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  text-transform: uppercase;
125
- letter-spacing: 0.05em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  font-weight: 700;
127
- padding-top: 6px;
 
 
 
128
  }
129
 
130
- /* ── Detail ── */
131
- .detail-surface {
132
- background-color: white;
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
- .memory-detail-body h3 {
 
 
 
 
 
140
  font-family: var(--font-display);
141
- font-size: 1.8rem;
142
- font-weight: 700;
143
- margin-bottom: 12px;
144
  color: #1a1a1a;
145
- line-height: 1.2;
146
  }
147
 
148
  .memory-detail-date {
149
- font-size: 0.9rem;
150
- color: #999;
151
  margin-bottom: 32px;
152
- font-weight: 500;
 
 
 
153
  }
154
 
155
- .memory-detail-summary {
156
- font-size: 1.1rem;
157
- line-height: 1.7;
158
- color: #555;
159
  margin-bottom: 40px;
 
 
 
 
 
 
160
  }
161
 
162
- .memory-detail-section {
 
 
 
 
163
  margin-bottom: 40px;
164
  }
165
 
166
- .memory-detail-section h4 {
167
- font-size: 0.75rem;
 
 
 
 
 
 
 
168
  text-transform: uppercase;
169
  letter-spacing: 0.1em;
170
- color: #bbb;
171
- margin-bottom: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  font-weight: 700;
 
 
 
 
 
 
173
  }
174
 
175
- .entity-group, .source-group {
 
 
 
 
 
 
176
  display: flex;
177
- flex-wrap: wrap;
 
 
 
 
 
 
178
  gap: 12px;
179
  }
180
 
181
- .entity-pill, .source-pill {
182
- background-color: #f5f5f5;
183
- padding: 10px 20px;
184
  border-radius: 14px;
185
- font-size: 0.9rem;
186
- color: #444;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  font-weight: 600;
188
- transition: all 0.2s;
189
  }
190
 
191
- .entity-pill:hover {
192
- background-color: #eee;
193
- transform: translateY(-2px);
 
 
 
 
 
 
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 [selectedId, setSelectedId] = useState(MEMORIES[0].id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
- const filteredMemories = MEMORIES.filter(memory =>
67
- memory.title.toLowerCase().includes(search.toLowerCase())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  );
69
 
70
- const selectedMemory = filteredMemories.find(m => m.id === selectedId) || filteredMemories[0] || MEMORIES[0];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  return (
73
  <div className="memory-view-grid fade-in">
 
74
  <section className="memory-surface list-surface">
75
  <div className="memory-toolbar">
76
- <label className="memory-search">
77
- <span className="material-icons">search</span>
78
  <input
79
  type="text"
80
  value={search}
81
  onChange={(e) => setSearch(e.target.value)}
82
- placeholder="Search memories..."
83
  />
84
- </label>
85
- <button className="memory-filter">
86
- <span>All Types</span>
87
- <span className="material-icons">expand_more</span>
88
- </button>
 
 
 
 
 
 
 
 
 
 
 
89
  </div>
90
 
91
  <div className="memory-list">
92
- {filteredMemories.map((memory) => (
93
- <button
94
- key={memory.id}
95
- className={`memory-row ${memory.id === selectedId ? 'active' : ''}`}
96
- onClick={() => setSelectedId(memory.id)}
97
- >
98
- <div className="memory-dot" style={{ backgroundColor: memory.dot }} />
99
- <div className="memory-row-copy">
100
- <strong>{memory.title}</strong>
101
- <div className="memory-row-meta">
102
- <span>{memory.date}</span>
103
- <span>•</span>
104
- <span>{memory.similarity} similarity</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
106
  </div>
107
- <span className="memory-tag">{memory.type}</span>
108
- </button>
109
- ))}
110
- </div>
111
 
112
- <button className="memory-link-button">View All Memories</button>
113
- </section>
 
 
 
 
 
 
 
 
 
114
 
115
- <section className="memory-surface detail-surface">
116
- <div className="memory-detail-body">
117
- <div className="memory-tag" style={{ marginBottom: '12px', textTransform: 'capitalize' }}>{selectedMemory.type}</div>
118
- <h3>{selectedMemory.title}</h3>
119
- <p className="memory-detail-date">{selectedMemory.date}</p>
120
- <p className="memory-detail-summary">{selectedMemory.summary}</p>
121
-
122
- <div className="memory-detail-section">
123
- <h4>Related Entities</h4>
124
- <div className="entity-group">
125
- {selectedMemory.entities.map(e => <span key={e} className="entity-pill">{e}</span>)}
 
 
 
 
 
 
 
 
 
 
126
  </div>
127
- </div>
128
 
129
- <div className="memory-detail-section">
130
- <h4>Source</h4>
131
- <div className="source-group">
132
- <span className="source-pill">{selectedMemory.source.memories} Memories</span>
133
- <span className="source-pill">{selectedMemory.source.entities} Entities</span>
 
 
 
 
 
 
 
 
 
134
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  </div>
136
- </div>
 
 
 
 
 
 
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: radial-gradient(circle at top, rgba(255, 255, 255, 0.55), transparent 28%), var(--bg-app);
 
 
 
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()