diff --git a/app/api/auth_router.py b/app/api/auth_router.py
index 9b726d206ef5fd1eb7341a4c4221faa7465ed244..0e9986c52cdd84e2a467f44f81153a149acaf28e 100644
--- a/app/api/auth_router.py
+++ b/app/api/auth_router.py
@@ -22,7 +22,7 @@ class AuthRequest(BaseModel):
def validate_username(cls, v: str) -> str:
v = v.strip()
if not 3 <= len(v) <= 30:
- raise ValueError("Username must be 3–30 characters")
+ raise ValueError("Username must be 3-30 characters")
if not re.match(r"^[a-zA-Z0-9_]+$", v):
raise ValueError("Username can only contain letters, numbers, and underscores")
return v.lower()
diff --git a/app/api/endpoints.py b/app/api/endpoints.py
index 0a46a9b52af5bc2e087db1979dfcf2092d2a10e0..e39d95a88c16ab87bd5ced830b4ba01fd47ae255 100644
--- a/app/api/endpoints.py
+++ b/app/api/endpoints.py
@@ -164,6 +164,58 @@ async def process_ingest(request: IngestRequest, current_user: str = Depends(get
# ── Stream Query ──────────────────────────────────────────────────
+@router.post("/analyze")
+async def analyze_text(request: QueryRequest, current_user: str = Depends(get_current_user)):
+ """
+ Analyzes text to preview potential semantic links and cognitive metrics.
+ Checks existing graph to find potential overlaps.
+ """
+ try:
+ from langchain_groq import ChatGroq
+ from langchain_core.messages import HumanMessage
+ from app.core.config import settings
+
+ api_key = settings.GROQ_API_KEY if settings.GROQ_API_KEY else "dummy_key"
+ llm = ChatGroq(model="llama-3.1-8b-instant", api_key=api_key)
+
+ # 1. Extract potential entities
+ prompt = f"Extract 5-8 key entities (names, concepts, places) from this text as a comma-separated list. Return ONLY the list: {request.text}"
+ response = await llm.ainvoke([HumanMessage(content=prompt)])
+ entities = [e.strip() for e in response.content.split(',') if e.strip()]
+
+ # 2. Check for existing overlaps in Neo4j
+ existing_links = []
+ if neo4j_db.driver:
+ # Look for entities that already exist for this user
+ check_query = """
+ MATCH (n:Entity)
+ WHERE n.user_id = $user_id AND toLower(n.name) IN $entities
+ RETURN n.name AS name, count{(n)--()} AS connections
+ """
+ overlaps = neo4j_db.query(check_query, {
+ "user_id": current_user,
+ "entities": [e.lower() for e in entities]
+ }) or []
+ existing_links = [{"name": o["name"], "connections": o["connections"]} for o in overlaps]
+
+ # 3. Calculate metrics
+ char_count = len(request.text)
+ chunk_count = (char_count // 500) + 1
+
+ return {
+ "entities": entities,
+ "existing_links": existing_links,
+ "metrics": {
+ "density": min(char_count / 2000, 1.0),
+ "chunks": chunk_count,
+ "estimated_links": len(entities) * 1.5,
+ "reinforcement_index": len(existing_links) / max(len(entities), 1)
+ }
+ }
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
@router.post("/query/stream")
async def process_query_stream(request: QueryRequest, current_user: str = Depends(get_current_user)):
async def event_generator():
@@ -192,7 +244,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
data={"query": request.text}
))
yield sse_event("trace", {"phase": "perception", "message": perception_msg, "data": {"query": request.text}})
- await asyncio.sleep(0.1)
+ await asyncio.sleep(0.4)
yield sse_event("brain_trace", build_brain_event(
"attention",
@@ -207,7 +259,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
"message": f"Attention salience computed at {attention['salience']}%.",
"data": attention
})
- await asyncio.sleep(0.1)
+ await asyncio.sleep(0.4)
if attention["emotional_intensity"] >= 70:
yield sse_event("brain_trace", build_brain_event(
@@ -223,7 +275,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
"message": f"Amygdala analogue flagged {attention['emotion_label']} salience.",
"data": {"emotion": attention["emotion_label"]}
})
- await asyncio.sleep(0.1)
+ await asyncio.sleep(0.4)
yield sse_event("brain_trace", build_brain_event(
"routing",
@@ -238,7 +290,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
"message": f"Routed cognition through {', '.join(routing['regions'])}.",
"data": {"regions": routing["regions"]}
})
- await asyncio.sleep(0.1)
+ await asyncio.sleep(0.4)
yield sse_event("brain_trace", build_brain_event(
"prediction",
@@ -253,7 +305,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
"message": prediction["intent"],
"data": prediction
})
- await asyncio.sleep(0.1)
+ await asyncio.sleep(0.4)
yield sse_event("brain_trace", build_brain_event(
"working_memory",
@@ -268,7 +320,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
"message": f"Loaded {len(history)} recent messages into working memory.",
"data": {"history_count": len(history)}
})
- await asyncio.sleep(0.1)
+ await asyncio.sleep(0.4)
for output in orchestrator.stream(state_input):
for node_name, node_output in output.items():
@@ -284,7 +336,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
data={"reflection": reflection}
))
yield sse_event("trace", {"phase": "reflection", "message": "Intent map formed.", "data": {"reflection": reflection}})
- await asyncio.sleep(0.3)
+ await asyncio.sleep(0.4)
elif node_name == "retrieve":
trace_data = node_output.get("trace_data", {})
@@ -302,7 +354,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
}
))
yield sse_event("trace", {"phase": "recall", "message": recall_msg, "data": node_output.get("context")})
- await asyncio.sleep(0.2)
+ await asyncio.sleep(0.4)
suppressed_sensory = trace_data.get("suppressed_sensory", 0)
suppressed_graph = trace_data.get("suppressed_graph", 0)
@@ -325,7 +377,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
"suppressed_graph": suppressed_graph,
}
})
- await asyncio.sleep(0.2)
+ await asyncio.sleep(0.4)
yield sse_event("brain_trace", build_brain_event(
"association",
@@ -339,7 +391,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
}
))
yield sse_event("trace", {"phase": "association", "message": assoc_msg, "data": node_output.get("graph_context"), "touched": trace_data.get("touched")})
- await asyncio.sleep(0.2)
+ await asyncio.sleep(0.4)
elif node_name == "call_model":
reason_msg = "Synthesizing final response via Cortex Node..."
@@ -352,7 +404,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
data={"prediction": prediction["intent"]}
))
yield sse_event("trace", {"phase": "reasoning", "message": reason_msg})
- await asyncio.sleep(0.1)
+ await asyncio.sleep(0.4)
final_response = node_output.get("response", "")
add_message(current_user, "user", request.text)
@@ -366,6 +418,7 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
inputs_used=["response_plan"],
data={"response_preview": final_response[:120]}
))
+ yield sse_event("trace", {"phase": "language", "message": "Generating natural language output."})
yield sse_event("final_result", {"response": final_response})
# Build neural mesh AFTER streaming the response so
@@ -431,6 +484,31 @@ async def process_query_stream(request: QueryRequest, current_user: str = Depend
# ── Memory Explorer ─────────────────────────────────────────────
+@router.get("/memory/search")
+async def process_memory_search(q: str, current_user: str = Depends(get_current_user)):
+ try:
+ from app.db.chroma import search_memories
+ results = search_memories(q, current_user)
+
+ memories = []
+ if results and "documents" in results and results["documents"]:
+ docs = results["documents"][0]
+ ids = results["ids"][0]
+ metadatas = results["metadatas"][0] if results["metadatas"] else []
+ distances = results["distances"][0] if results["distances"] else []
+
+ for i in range(len(docs)):
+ memories.append({
+ "id": ids[i],
+ "content": docs[i],
+ "metadata": metadatas[i] if i < len(metadatas) else {},
+ "similarity": round(1 - distances[i], 2) if i < len(distances) else 0
+ })
+ return {"memories": memories}
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
@router.get("/memory/sensory")
async def get_sensory_memories(current_user: str = Depends(get_current_user)):
try:
@@ -451,6 +529,18 @@ async def get_sensory_memories(current_user: str = Depends(get_current_user)):
raise HTTPException(status_code=500, detail=str(e))
+@router.delete("/memory/{memory_id}")
+async def purge_memory_chunk(memory_id: str, current_user: str = Depends(get_current_user)):
+ try:
+ from app.db.chroma import delete_vector
+ success = delete_vector(memory_id, current_user)
+ if not success:
+ raise HTTPException(status_code=404, detail="Memory chunk not found or unauthorized.")
+ return {"message": "Memory chunk purged successfully."}
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
# ── History ───────────────────────────────────────────────────────
@router.get("/history")
diff --git a/app/db/chroma.py b/app/db/chroma.py
index 7ff2af738b61ad876dcca407ca84dc7e08691416..138d98f6c5d987752d7bcaa934010c6ea7436ba4 100644
--- a/app/db/chroma.py
+++ b/app/db/chroma.py
@@ -10,6 +10,21 @@ def get_collection(name: str = "soma_sensory_memory"):
return client.get_or_create_collection(name=name)
+def search_memories(query: str, user_id: str, limit: int = 10):
+ """Semantic search for memories belonging to a user."""
+ try:
+ collection = get_collection()
+ results = collection.query(
+ query_texts=[query],
+ n_results=limit,
+ where={"user_id": user_id}
+ )
+ return results
+ except Exception as e:
+ print(f"ChromaDB search error: {e}")
+ return None
+
+
def clear_user_vectors(user_id: str):
"""Delete all ChromaDB documents belonging to a user."""
try:
@@ -19,3 +34,13 @@ def clear_user_vectors(user_id: str):
collection.delete(ids=results["ids"])
except Exception as e:
print(f"ChromaDB clear error: {e}")
+
+def delete_vector(memory_id: str, user_id: str):
+ """Delete a specific document from ChromaDB."""
+ try:
+ collection = get_collection()
+ collection.delete(ids=[memory_id], where={"user_id": user_id})
+ return True
+ except Exception as e:
+ print(f"ChromaDB delete error: {e}")
+ return False
diff --git a/app/db/session.py b/app/db/session.py
index 64297badddb6090fc73fb1b2d3ce91b356420020..69633969883a4bc0a452a8c919859235ead8c125 100644
--- a/app/db/session.py
+++ b/app/db/session.py
@@ -74,7 +74,7 @@ def init_session_db():
# Test Postgres connection if configured
if _db_backend == "postgres" and not _test_postgres_connection():
- print("⚠️ Postgres connection failed. Falling back to SQLite.")
+ print("[!] Postgres connection failed. Falling back to SQLite.")
_db_backend = "sqlite"
with get_conn() as conn:
diff --git a/app/main.py b/app/main.py
index 50e2fda7ccca722d22ab588e68f958727832c4fd..e7504be661a6d00e08147bea676caccb92918ac8 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,3 +1,11 @@
+import sys
+import io
+
+# Force UTF-8 encoding for stdout and stderr on Windows to handle emojis
+if sys.platform == "win32":
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
print("Main: Imports starting...")
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
diff --git a/app/services/brain_trace.py b/app/services/brain_trace.py
index 839844bf6f294ac69ade48af29e45124e602c97d..ff4625efc16ab6d741c8278eeea74ff6f045d2e0 100644
--- a/app/services/brain_trace.py
+++ b/app/services/brain_trace.py
@@ -33,7 +33,7 @@ PHASE_REGIONS = {
"language": "language_cortex",
"memory": "memory_consolidation",
"graph": "neocortex",
- "dreaming": "default_mode_network",
+ "dreaming": "default_mode_network",
"sleep": "memory_consolidation",
}
diff --git a/app/services/memory.py b/app/services/memory.py
index 87a11ec2352be28f5f35f14827809bce22f5d50f..bda50ccdc643322d10d415b05bbd376a690fe1f2 100644
--- a/app/services/memory.py
+++ b/app/services/memory.py
@@ -28,18 +28,18 @@ def get_embeddings():
model_name="all-MiniLM-L6-v2",
model_kwargs={"trust_remote_code": True}
)
- print("✓ Embeddings model loaded successfully")
+ print("[OK] Embeddings model loaded successfully")
return _embeddings
except Exception as e:
- print(f"⚠ Failed to load embeddings from HF Hub: {e}")
- print("⚠ Continuing without embeddings (sensory memory will be limited)")
+ print(f"[!] Failed to load embeddings from HF Hub: {e}")
+ print("[!] Continuing without embeddings (sensory memory will be limited)")
_embeddings_failed = True
return None
def ingest_text(text: str, metadata: dict = None, user_id: str = "default_user"):
embeddings = get_embeddings()
if embeddings is None:
- print(f"⚠ Skipping sensory memory ingestion (embeddings unavailable)")
+ print(f"[!] Skipping sensory memory ingestion (embeddings unavailable)")
return 0
# 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")
def retrieve_context(query: str, user_id: str = "default_user", n_results: int = 3):
embeddings = get_embeddings()
if embeddings is None:
- print(f"⚠ Cannot retrieve context (embeddings unavailable)")
+ print(f"[!] Cannot retrieve context (embeddings unavailable)")
return []
collection = get_collection()
diff --git a/app/services/orchestrator.py b/app/services/orchestrator.py
index b00f1462d795ba2d35cdf960d27f0d71409664d6..ed1b39cbe47493e6f608e463be3f684fa4e4a976 100644
--- a/app/services/orchestrator.py
+++ b/app/services/orchestrator.py
@@ -79,40 +79,38 @@ def call_model(state: AgentState):
history_lines.append(f"{prefix}: {msg['content']}")
history_str = "\n".join(history_lines) if history_lines else "No previous conversation."
- # Construct prompt with context & history
+ # Context formatting
context_str = "\n\n".join(state["context"])
graph_str = "\n".join(state["graph_context"]) if state["graph_context"] else "No related knowledge graph entities found."
-
- has_memories = bool(context_str.strip()) or bool(state["graph_context"])
- prompt = f"""You are Soma, a brain-inspired cognitive AI that learns and grows through conversation.
-You have specialized memory layers that build up as you interact with the user.
+ from langchain_core.messages import SystemMessage, HumanMessage
-### COGNITIVE CONTEXT:
-#### WORKING MEMORY (Recent Conversation):
-{history_str}
+ system_prompt = f"""You are Soma, a brain-inspired cognitive AI.
+
+### OPERATING RULES:
+1. Be a friendly, intelligent, and natural conversationalist.
+2. NO META-COMMENTARY. Never talk about your own "sensing patterns", "internal processes", or "memory layers" unless explicitly asked.
+3. Keep responses concise but human-like. Don't be a robot, but don't write essays either.
+4. Acknowledge user input naturally (e.g., if they introduce themselves, greet them by name).
-#### SEMANTIC MEMORY (Knowledge Graph Relationships):
+### COGNITIVE CONTEXT:
+#### SEMANTIC MEMORY:
{graph_str}
-#### SENSORY MEMORY (Retrieved Facts):
+#### SENSORY MEMORY:
{context_str}
+"""
-### INSTRUCTIONS:
-1. You are a helpful, intelligent AI assistant. Always respond naturally and conversationally.
-2. If you have relevant SEMANTIC or SENSORY memories, use them to enrich your response.
-3. If you have no stored memories yet, that is fine — respond using your general knowledge and the conversation context.
-4. As the user talks to you, your memory layers will grow. Acknowledge what you learn from them.
-5. Keep your persona warm, intelligent, and concise. You are a brain that is always learning.
-
-USER MESSAGE:
-{state["input"]}"""
+ messages = [
+ SystemMessage(content=system_prompt),
+ HumanMessage(content=f"HISTORY:\n{history_str}\n\nUSER MESSAGE: {state['input']}")
+ ]
try:
- response = llm.invoke([HumanMessage(content=prompt)])
- response_text = response.content
+ response = llm.invoke(messages)
+ response_text = response.content.strip()
except Exception as e:
- response_text = f"LLM Connection Error: {str(e)}"
+ response_text = f"Cognitive Link Error: {str(e)}"
return {"response": response_text}
diff --git a/app/services/sleep_cycle.py b/app/services/sleep_cycle.py
index 05f50ca91c0c7e8d3cdd9da85d5ed5b3123522a8..32ef2b696ecbc7560f19bc54debee0e28d6d8517 100644
--- a/app/services/sleep_cycle.py
+++ b/app/services/sleep_cycle.py
@@ -123,7 +123,7 @@ def run_sleep_cycle(keep_recent: int = 10):
"graph_triples": triples,
})
- print(f" Sleep Cycle: Session {session_id} — summarized, {triples} triples, pruned {pruned} messages.")
+ print(f" Sleep Cycle: Session {session_id} - summarized, {triples} triples, pruned {pruned} messages.")
report["message"] = f"Sleep Cycle complete. Processed {report['sessions_processed']} sessions."
return report
@@ -132,7 +132,7 @@ def run_sleep_cycle(keep_recent: int = 10):
# Allow running as a standalone script
if __name__ == "__main__":
print("=" * 60)
- print("Soma Sleep Cycle — Starting consolidation...")
+ print("Soma Sleep Cycle - Starting consolidation...")
print("=" * 60)
result = run_sleep_cycle()
print(f"\n{'=' * 60}")
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index dac53ae6c9f3efdc0f5f73f7db006fba582ef76b..7bdbc4f6033c754f5798e44ff79d4176fd11185c 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-force-graph-2d": "^1.29.1",
"react-force-graph-3d": "^1.29.1"
},
"devDependencies": {
@@ -1598,6 +1599,16 @@
"node": ">=6.0.0"
}
},
+ "node_modules/bezier-js": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz",
+ "integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==",
+ "license": "MIT",
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -1675,6 +1686,18 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/canvas-color-tracker": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz",
+ "integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "tinycolor2": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -1784,6 +1807,28 @@
"node": ">=12"
}
},
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-force-3d": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
@@ -1870,6 +1915,7 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
+ "peer": true,
"engines": {
"node": ">=12"
}
@@ -1907,6 +1953,41 @@
"node": ">=12"
}
},
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/data-bind-mapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/data-bind-mapper/-/data-bind-mapper-1.0.3.tgz",
@@ -2298,6 +2379,32 @@
"node": ">=12"
}
},
+ "node_modules/force-graph": {
+ "version": "1.51.4",
+ "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.4.tgz",
+ "integrity": "sha512-TdJ2KbkoiDQ7NIRx8IPGD0mAXXpLhamS7c+b7W98b0MHG7lphnda1VOQX/98UDTsttIAdH4TcP0l0MauSnLK8w==",
+ "license": "MIT",
+ "dependencies": {
+ "@tweenjs/tween.js": "18 - 25",
+ "accessor-fn": "1",
+ "bezier-js": "3 - 6",
+ "canvas-color-tracker": "^1.3",
+ "d3-array": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-force-3d": "2 - 3",
+ "d3-scale": "1 - 4",
+ "d3-scale-chromatic": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-zoom": "2 - 3",
+ "float-tooltip": "^1.7",
+ "index-array-by": "1",
+ "kapsule": "^1.16",
+ "lodash-es": "4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2396,6 +2503,15 @@
"node": ">=0.8.19"
}
},
+ "node_modules/index-array-by": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz",
+ "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
@@ -2909,6 +3025,23 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-force-graph-2d": {
+ "version": "1.29.1",
+ "resolved": "https://registry.npmjs.org/react-force-graph-2d/-/react-force-graph-2d-1.29.1.tgz",
+ "integrity": "sha512-1Rl/1Z3xy2iTHKj6a0jRXGyiI86xUti81K+jBQZ+Oe46csaMikp47L5AjrzA9hY9fNGD63X8ffrqnvaORukCuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "force-graph": "^1.51",
+ "prop-types": "15",
+ "react-kapsule": "^2.5"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-force-graph-3d": {
"version": "1.29.1",
"resolved": "https://registry.npmjs.org/react-force-graph-3d/-/react-force-graph-3d-1.29.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index d1445872ddaf84388239d77384581d0b89c39897..d06682e9c307fae34876aad23f8520951d3a0648 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,6 +12,7 @@
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-force-graph-2d": "^1.29.1",
"react-force-graph-3d": "^1.29.1"
},
"devDependencies": {
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 33d5c0ac8f317b90d5f13ac56119c7fddfe1806a..1484a7c76764ac0a09488d613c855a658842f431 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -1,20 +1,45 @@
+/* ── Global Scrollbar Styles ── */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: rgba(0, 0, 0, 0.1) transparent;
+}
+
+::-webkit-scrollbar {
+ width: 5px;
+ height: 5px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: rgba(0, 0, 0, 0.05);
+ border-radius: 20px;
+ transition: background 0.3s ease;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: rgba(0, 0, 0, 0.15);
+}
+
/* App.css - Final Refinements for 8-Page Design */
.soma-shell {
display: flex;
height: 100vh;
width: 100vw;
- background-color: #e5e5e5; /* Beige-ish background from design */
+ background-color: transparent; /* Inherits high-fidelity body radial gradients */
overflow: hidden;
}
/* ── Sidebar ── */
.soma-sidebar {
- width: 260px;
+ width: 200px;
background-color: transparent;
display: flex;
flex-direction: column;
- padding: 48px 32px;
+ padding: 48px 20px;
flex-shrink: 0;
}
@@ -42,7 +67,7 @@
.brand-copy h1 {
font-family: var(--font-display);
- font-size: 1.5rem;
+ font-size: 1.8rem;
font-weight: 800;
line-height: 1;
color: #1a1a1a;
@@ -50,7 +75,7 @@
}
.brand-copy p {
- font-size: 0.7rem;
+ font-size: 0.55rem;
color: #888;
margin-top: 2px;
text-transform: uppercase;
@@ -74,7 +99,7 @@
color: #666;
text-decoration: none;
font-weight: 600;
- font-size: 0.95rem;
+ font-size: 0.8rem;
transition: all 0.2s ease;
background: transparent;
}
@@ -106,8 +131,8 @@
.session-card {
display: flex;
align-items: center;
- gap: 12px;
- padding: 16px;
+ gap: 8px;
+ padding: 8px 10px;
background-color: rgba(255, 255, 255, 0.6);
border-radius: 20px;
border: 1px solid rgba(0,0,0,0.05);
@@ -127,14 +152,15 @@
}
.session-copy strong {
- font-size: 0.85rem;
+ font-size: 0.75rem;
font-weight: 700;
color: #1a1a1a;
}
.session-copy span {
- font-size: 0.7rem;
+ font-size: 0.55rem;
color: #999;
+ font-weight: 500;
}
/* ── Main Content Area ── */
@@ -146,10 +172,12 @@
.page-canvas {
flex: 1;
- background-color: #f7f7f7;
- border-radius: 48px; /* Larger radius from design */
- padding: 56px;
- box-shadow: 0 40px 100px rgba(0,0,0,0.05);
+ background-color: var(--bg-card);
+ backdrop-filter: blur(20px);
+ border: var(--border-card);
+ border-radius: 48px;
+ padding: 40px;
+ box-shadow: var(--shadow-premium);
display: flex;
flex-direction: column;
position: relative;
@@ -160,12 +188,12 @@
display: flex;
justify-content: space-between;
align-items: flex-start;
- margin-bottom: 56px;
+ margin-bottom: 40px;
}
.page-header h2 {
font-family: var(--font-display);
- font-size: 2rem;
+ font-size: 1.2rem;
font-weight: 700;
color: #1a1a1a;
letter-spacing: -0.01em;
@@ -183,7 +211,7 @@
}
.status-pill .label {
- font-size: 0.7rem;
+ font-size: 0.6rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #999;
@@ -194,7 +222,7 @@
display: flex;
align-items: center;
gap: 8px;
- font-size: 0.9rem;
+ font-size: 0.75rem;
font-weight: 700;
color: #1a1a1a;
}
@@ -230,7 +258,7 @@
}
.feed-header {
- font-size: 0.65rem;
+ font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #999;
@@ -263,7 +291,182 @@
line-height: 1.4;
}
+/* ── Telemetry HUD ── */
+.telemetry-trigger {
+ position: absolute;
+ top: 24px;
+ right: 24px;
+ z-index: 100;
+ width: 42px;
+ height: 42px;
+ border-radius: 12px;
+ background: white;
+ border: 1px solid rgba(0,0,0,0.05);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #666;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.03);
+}
+
+.telemetry-trigger:hover {
+ background: #fdfdfd;
+ color: #ff6b35;
+ transform: translateY(-2px);
+ box-shadow: 0 6px 15px rgba(0,0,0,0.06);
+}
+
+.telemetry-trigger.active {
+ background: #ff6b35;
+ color: white;
+ border-color: #ff6b35;
+}
+
+.telemetry-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(255, 255, 255, 0.4);
+ backdrop-filter: blur(8px);
+ z-index: 2000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 40px;
+ animation: fadeIn 0.3s ease-out;
+}
+
+.telemetry-modal {
+ width: 100%;
+ max-width: 620px;
+ height: auto;
+ max-height: 80vh;
+ background: rgba(255, 255, 255, 0.85);
+ backdrop-filter: blur(25px);
+ border-radius: 32px;
+ box-shadow: 0 40px 120px rgba(0,0,0,0.08);
+ border: 1px solid white;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden; /* Important: Clip everything to the rounded corners */
+ position: relative;
+ animation: modalSlide 0.4s cubic-bezier(0.16, 1, 0.3, 1);
+ color: #1a1a1a;
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 32px 32px 16px 32px;
+ background: rgba(255, 255, 255, 0.01);
+ z-index: 10;
+}
+
+.modal-scroll-area {
+ flex: 1;
+ overflow-y: auto;
+ padding: 0 32px 32px 32px;
+ /* Custom scrollbar for the internal area */
+}
+
+.modal-scroll-area::-webkit-scrollbar {
+ width: 16px; /* Wider track but narrower thumb */
+}
+
+.modal-scroll-area::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.modal-scroll-area::-webkit-scrollbar-thumb {
+ background-color: rgba(0, 0, 0, 0.08);
+ border: 5px solid transparent; /* Pushes the thumb 5px away from all edges */
+ background-clip: padding-box;
+ border-radius: 10px;
+}
+
+.modal-scroll-area::-webkit-scrollbar-button:single-button {
+ background-color: transparent;
+ display: block;
+ height: 12px;
+ width: 16px;
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+.modal-scroll-area::-webkit-scrollbar-button:single-button:vertical:decrement {
+ background-image: url("data:image/svg+xml;utf8,");
+}
+
+.modal-scroll-area::-webkit-scrollbar-button:single-button:vertical:increment {
+ background-image: url("data:image/svg+xml;utf8,");
+}
+
+.modal-scroll-area::-webkit-scrollbar-button:hover {
+ background-color: rgba(0,0,0,0.02);
+ border-radius: 4px;
+}
+
+.modal-header h3 {
+ font-size: 0.8rem;
+ text-transform: uppercase;
+ letter-spacing: 0.15em;
+ color: #999;
+ font-weight: 700;
+}
+
+.modal-header button {
+ background: rgba(0,0,0,0.05);
+ border: none;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #666;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.modal-header button:hover {
+ background: #ff6b35;
+ color: white;
+}
+
+@keyframes modalSlide {
+ from { opacity: 0; transform: translateY(40px) scale(0.95); }
+ to { opacity: 1; transform: translateY(0) scale(1); }
+}
+
@keyframes feedItemIn {
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateX(0); }
}
+
+.timeline-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ padding: 40px 20px 240px 20px;
+ text-align: center;
+ opacity: 0.4;
+ gap: 12px;
+}
+
+.timeline-empty span {
+ font-size: 48px;
+ color: #999;
+}
+
+.timeline-empty p {
+ font-size: 0.7rem;
+ color: #999;
+ letter-spacing: 0.02em;
+}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 57aceeedec0ff9e48bcdbb1dcd3728047f270e53..82dde252cb83a02c3688e6f4274fa39d18766738 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -6,7 +6,7 @@ import MemoryExplorer from './components/MemoryExplorer';
import KnowledgeGraph from './components/KnowledgeGraph';
import CognitiveDashboard from './components/CognitiveDashboard';
import KnowledgeInput from './components/KnowledgeInput';
-import { SleepProgress, SleepSummary } from './components/DreamSequence';
+import { SleepProgress } from './components/DreamSequence';
import AuthScreen from './components/AuthScreen';
import { apiFetch } from './api';
import './App.css';
@@ -15,8 +15,8 @@ const NAV_ITEMS = [
{ id: 'console', label: 'Console', icon: 'chat_bubble_outline' },
{ id: 'memory', label: 'Memory', icon: 'layers' },
{ id: 'graph', label: 'Graph', icon: 'share' },
- { id: 'knowledge', label: 'Knowledge', icon: 'description' },
- { id: 'status', label: 'Status', icon: 'analytics' },
+ { id: 'knowledge', label: 'Inscription', icon: 'auto_awesome' },
+ { id: 'sleep', label: 'Sleep', icon: 'bedtime' },
];
const COGNITIVE_PHASES = {
@@ -40,6 +40,18 @@ function App() {
const [knowledgeStatus, setKnowledgeStatus] = useState('');
const [sleepPhaseIndex, setSleepPhaseIndex] = useState(0);
const [sleepSummary, setSleepSummary] = useState(null);
+ const [showStatus, setShowStatus] = useState(false);
+ const [darkMode, setDarkMode] = useState(localStorage.getItem('soma_dark') === 'true');
+
+ useEffect(() => {
+ if (darkMode) {
+ document.body.classList.add('dark-theme');
+ localStorage.setItem('soma_dark', 'true');
+ } else {
+ document.body.classList.remove('dark-theme');
+ localStorage.setItem('soma_dark', 'false');
+ }
+ }, [darkMode]);
useEffect(() => {
if (!username) return;
@@ -75,21 +87,7 @@ function App() {
setMessages(prev => [...prev, { role: 'user', content: text, timestamp }]);
setTrace([]);
- // ── STEP 3: Message Sent (Transition to Cognition) ──
-
- // Phase A: Perception (~0.4s)
- setCognitiveState(COGNITIVE_PHASES.PERCEPTION);
- setTrace([{ phase: 'Perception', content: 'Parsing input and identifying intent...', time: 'NOW' }]);
- await new Promise(r => setTimeout(r, 400));
-
- // Phase B: Attention (~0.6s)
- setCognitiveState(COGNITIVE_PHASES.ATTENTION);
- setTrace(prev => [{ phase: 'Attention', content: 'Focusing on key concepts and relationships...', time: 'NOW' }, ...prev]);
- await new Promise(r => setTimeout(r, 600));
-
- // Phase C: Memory Recall (Initiate API)
- setCognitiveState(COGNITIVE_PHASES.RECALL);
- setTrace(prev => [{ phase: 'Recall', content: 'Retrieving related memories and associations...', time: 'NOW' }, ...prev]);
+ // Remove artificial frontend phases since backend now streams them.
try {
const controller = new AbortController();
@@ -113,10 +111,6 @@ function App() {
const decoder = new TextDecoder();
let buffer = '';
- // Phase D: Reasoning (During Stream Start)
- setCognitiveState(COGNITIVE_PHASES.REASONING);
- setTrace(prev => [{ phase: 'Reasoning', content: 'Synthesizing context and evaluating response...', time: 'NOW' }, ...prev]);
-
while (true) {
const { value, done } = await reader.read();
if (done) break;
@@ -134,7 +128,9 @@ function App() {
if (data.phase) {
// Live trace and state updates from backend
setCognitiveState(data.phase);
- setTrace(prev => [{ time: new Date().toLocaleTimeString([], { hour12: false }), ...data }, ...prev]);
+ if (data.message) {
+ setTrace(prev => [{ time: new Date().toLocaleTimeString([], { hour12: false }), ...data }, ...prev]);
+ }
} else if (data.response) {
// Phase E: Response Generation
setCognitiveState(COGNITIVE_PHASES.RESPONDING);
@@ -157,12 +153,12 @@ function App() {
setTrace(prev => [{ phase: 'Error', content: 'Neural connection interrupted.', time: 'ERROR' }, ...prev]);
} finally {
// Ensure we always return to idle
- setTimeout(() => setCognitiveState(COGNITIVE_PHASES.IDLE), 1000);
+ setCognitiveState(COGNITIVE_PHASES.IDLE);
}
};
const handleKnowledgeSubmit = async (text) => {
- setKnowledgeStatus('Adding knowledge to memory...');
+ setKnowledgeStatus('Integrating knowledge into neural layers...');
try {
const res = await apiFetch('/api/v1/ingest', {
method: 'POST',
@@ -170,11 +166,11 @@ function App() {
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Ingestion failed');
- setKnowledgeStatus('Knowledge stored successfully.');
- setTimeout(() => setKnowledgeStatus(''), 3000);
+ setKnowledgeStatus(data.message || 'Knowledge stored successfully.');
+ setTimeout(() => setKnowledgeStatus(''), 10000);
fetchVitals();
} catch (error) {
- setKnowledgeStatus('Could not add knowledge right now.');
+ setKnowledgeStatus('Neural integration failed: ' + error.message);
}
};
@@ -182,22 +178,35 @@ function App() {
setCognitiveState('consolidating');
setActivePage('sleep');
setSleepPhaseIndex(0);
- setTimeout(() => setSleepPhaseIndex(1), 1200);
- setTimeout(() => setSleepPhaseIndex(2), 2400);
+ setSleepSummary(null);
+
+ // Animate checklist steps gracefully during the pending API request
+ const stepInterval = setInterval(() => {
+ setSleepPhaseIndex(prev => {
+ if (prev < 2) return prev + 1;
+ return prev;
+ });
+ }, 1000);
try {
const res = await apiFetch('/api/v1/sleep', { method: 'POST' });
const data = await res.json();
+
+ clearInterval(stepInterval);
+ setSleepPhaseIndex(2); // Mark all checklists as complete
+
setSleepSummary({
- linked: data.graph_relations_extracted || 12,
- consolidated: 28,
- pruned: data.messages_pruned || 17,
- strengthened: 34
+ linked: data.graph_relations_extracted || 0,
+ consolidated: data.summaries_created || 0,
+ pruned: data.messages_pruned || 0
});
} catch (error) {
- setSleepSummary({ linked: 12, consolidated: 28, pruned: 17, strengthened: 34 });
+ clearInterval(stepInterval);
+ console.error("Sleep cycle failed:", error);
+ setSleepSummary({ linked: 0, consolidated: 0, pruned: 0 });
} finally {
- setTimeout(() => setActivePage('sleep-summary'), 3600);
+ setCognitiveState(COGNITIVE_PHASES.IDLE);
+ fetchVitals(); // Instantly update vitals to reflect pruned/cleared working queue
}
};
@@ -211,10 +220,10 @@ function App() {
if (!username) return