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 ; const stats = [ - { label: 'Working Memory', value: vitals?.working_memory_pct?.toFixed(0) || '36', icon: 'neurology' }, - { label: 'Sensory Memory', value: vitals?.sensory_count || '1,248', icon: 'cloud_queue' }, - { label: 'Semantic Memory', value: vitals?.graph_node_count || '3,562', icon: 'device_hub' }, - { label: 'Neural Sparks', value: vitals?.recent_sparks || '24', icon: 'hub' }, + { label: 'Working Memory', value: vitals?.working || '0', icon: 'psychology' }, + { label: 'Sensory Memory', value: vitals?.sensory || '0', icon: 'cloud' }, + { label: 'Semantic Memory', value: vitals?.semantic?.nodes || '0', icon: 'account_tree' }, + { label: 'Neural Sparks', value: vitals?.semantic?.edges || '0', icon: 'auto_awesome' }, ]; return ( @@ -241,6 +250,16 @@ function App() { {item.label} ))} + +
@@ -250,7 +269,7 @@ function App() {
{username} - Session: GUEST-7F3A + GUEST-7F3A
-
- {/* Interaction Layer (Utility) */} -
+
+ {/* Interaction Layer (The Chat) */} +
{ + setMessages([]); + setTrace([]); + }} + userAvatar={`https://api.dicebear.com/7.x/avataaars/svg?seed=${username}`} + isTyping={cognitiveState !== COGNITIVE_PHASES.IDLE && cognitiveState !== COGNITIVE_PHASES.LISTENING} onInputStateChange={(isTyping) => { if (cognitiveState === COGNITIVE_PHASES.IDLE && isTyping) setCognitiveState(COGNITIVE_PHASES.LISTENING); if (cognitiveState === COGNITIVE_PHASES.LISTENING && !isTyping) setCognitiveState(COGNITIVE_PHASES.IDLE); @@ -290,8 +312,23 @@ function App() {
{/* Cognitive Layer (The Brain) */} -
+
+
+ Status +
+
+ {cognitiveState} +
+
+
+ + {/* Activity Layer (The Timeline) */} +
+

Activity Feed

+
+ +
@@ -300,7 +337,7 @@ function App() { {activePage === 'activity' && (
-

2. Brain Activity

+

Neural Activity

@@ -317,23 +354,32 @@ function App() {
)} - {activePage === 'status' && ( -
-

3. Status

- -
+ {showStatus && ( +
setShowStatus(false)}> +
e.stopPropagation()}> +
+

System Telemetry

+ +
+
+ +
+
+
)} {activePage === 'memory' && (
-

4. Memory View

+

Neural Memory

)} {activePage === 'graph' && (
-

5. Graph View

+

Knowledge Graph

)} @@ -341,27 +387,26 @@ function App() { {activePage === 'knowledge' && (
-

6. Knowledge Input

- +

Neural Inscription

)} {activePage === 'sleep' && ( -
-

7. Sleep (Consolidation)

- -
- )} - - {activePage === 'sleep-summary' && ( -
-

8. Consolidation Summary

- setActivePage('knowledge')} /> -
+ { + setCognitiveState(COGNITIVE_PHASES.IDLE); + setSleepPhaseIndex(0); + setSleepSummary(null); + setActivePage('console'); + }} + /> )}
diff --git a/frontend/src/assets/brain/Brain.png b/frontend/src/assets/brain/Brain.png new file mode 100644 index 0000000000000000000000000000000000000000..932292c58a5b97eab3b7a0d92b43d199c99042b4 --- /dev/null +++ b/frontend/src/assets/brain/Brain.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d7f44b0f7ce2db8ce2dfe13a9247b1faa61638203d4f787f267054e26df8b14 +size 2061145 diff --git a/frontend/src/assets/brain/Brain_nobg.png b/frontend/src/assets/brain/Brain_nobg.png new file mode 100644 index 0000000000000000000000000000000000000000..18579bfe118a081e70929555f5f4b848ee9c3137 --- /dev/null +++ b/frontend/src/assets/brain/Brain_nobg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f221b26602aeb584876d6750c04a20265616b79d1ced5de53e876c724ebd638d +size 1766201 diff --git a/frontend/src/assets/brain/sleep.png b/frontend/src/assets/brain/sleep.png new file mode 100644 index 0000000000000000000000000000000000000000..9b9ce1275d4e5e47ba269daa53b530d3aa41441a --- /dev/null +++ b/frontend/src/assets/brain/sleep.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4978ff9f2308ea907367eff682a86cc9ba62c4ac71d2470c9949ffa10033a606 +size 2009662 diff --git a/frontend/src/assets/brain/sleep_nobg.png b/frontend/src/assets/brain/sleep_nobg.png new file mode 100644 index 0000000000000000000000000000000000000000..14907c4ca9a2e2643dc64b8fb09853a3877a834d --- /dev/null +++ b/frontend/src/assets/brain/sleep_nobg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75eaf72c0dbf9ec37e148cb97274bd046e97855998d88ebd5cb3cea8e883343e +size 2606985 diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png deleted file mode 100644 index 2d58a13c6c916ee1d261fc82517761fa3acf2c8d..0000000000000000000000000000000000000000 --- a/frontend/src/assets/hero.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:72a860570eddf1dd9988f26c7106c67be286bc9f2fd3303c465ce87edb1ae6cd -size 44919 diff --git a/frontend/src/components/ChatPanel.css b/frontend/src/components/ChatPanel.css index 07374af1ac9196d40f917f9fd5911c2193fa4f20..49d32cdece52401e9b31856f0a65add79ca52982 100644 --- a/frontend/src/components/ChatPanel.css +++ b/frontend/src/components/ChatPanel.css @@ -12,22 +12,68 @@ padding-right: 12px; display: flex; flex-direction: column; - gap: 32px; + gap: 16px; + scrollbar-width: thin; + scrollbar-color: rgba(0,0,0,0.05) transparent; +} + +/* ── Custom Scrollbar (Subtle) ── */ +.chat-messages::-webkit-scrollbar { + width: 4px; +} + +.chat-messages::-webkit-scrollbar-track { + background: transparent; +} + +.chat-messages::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.05); + border-radius: 10px; + transition: background-color 0.3s; +} + +.chat-messages:hover::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.1); } .chat-bubble-group { display: flex; gap: 20px; - max-width: 90%; + max-width: 85%; + align-items: flex-start; +} + +.chat-bubble-group.user { + align-self: flex-end; + flex-direction: row-reverse; +} + +.chat-bubble-group.soma { + align-self: flex-start; +} + +.chat-bubble-group.user .bubble-body { + background-color: #fcfcfc; + border-bottom-right-radius: 4px; + text-align: right; +} + +.chat-bubble-group.soma .bubble-body { + border-bottom-left-radius: 4px; } .avatar-wrap { flex-shrink: 0; } +.bubble-header { + display: none; +} + .chat-avatar { - width: 36px; - height: 36px; + width: 28px; + height: 28px; + flex-shrink: 0; border-radius: 50%; background-color: #f0f0f0; display: flex; @@ -37,14 +83,20 @@ box-shadow: 0 4px 10px rgba(0,0,0,0.02); } +.chat-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + .chat-avatar .material-icons { - font-size: 20px; + font-size: 16px; color: #888; } .bubble-body { background-color: white; /* Clearer white for premium look */ - padding: 24px 28px; + padding: 16px 20px; border-radius: 24px; position: relative; box-shadow: 0 4px 20px rgba(0,0,0,0.02); @@ -52,50 +104,58 @@ } .bubble-meta { - margin-bottom: 10px; + margin-bottom: 4px; } .bubble-meta strong { - font-size: 0.85rem; + font-size: 0.7rem; font-weight: 700; color: #1a1a1a; letter-spacing: -0.01em; } .bubble-text { - font-size: 1rem; + font-size: 0.8rem; line-height: 1.6; color: #444; + white-space: pre-wrap; } .bubble-time { - margin-top: 12px; - font-size: 0.75rem; + margin-top: 6px; + font-size: 0.7rem; color: #bbb; font-weight: 500; } +.chat-bubble-group.user .bubble-time { + text-align: right; +} + /* ── Input Row ── */ .chat-input-row { display: flex; - gap: 16px; + gap: 10px; align-items: center; + margin-top: 24px; } .input-field-wrap { flex: 1; - background-color: #eee; - border-radius: 18px; - padding: 0 24px; + background-color: white; + border: 1px solid rgba(0,0,0,0.04); + box-shadow: 0 4px 15px rgba(0,0,0,0.02); + border-radius: 14px; + padding: 0 16px; display: flex; align-items: center; - height: 64px; + height: 48px; transition: all 0.2s; } .input-field-wrap:focus-within { - background-color: #e8e8e8; - box-shadow: inset 0 2px 4px rgba(0,0,0,0.02); + border-color: rgba(255, 107, 53, 0.3); + box-shadow: 0 4px 20px rgba(255, 107, 53, 0.05); } .input-field-wrap input { @@ -103,31 +163,58 @@ background: transparent; border: none; outline: none; - font-size: 1.05rem; + font-size: 0.85rem; color: #1a1a1a; font-weight: 500; } +.new-chat-btn { + background: white; + border: 1px solid rgba(0,0,0,0.05); + width: 48px; + height: 48px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #666; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0,0,0,0.03); +} + +.new-chat-btn:hover { + background: #f8f8f8; + color: #ff6b35; + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0,0,0,0.06); +} + +.new-chat-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} .input-field-wrap input::placeholder { color: #aaa; } .send-btn { - width: 64px; - height: 64px; + width: 48px; + height: 48px; background-color: #ff6b35; - border-radius: 18px; + border-radius: 12px; display: flex; align-items: center; justify-content: center; color: white; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); - box-shadow: 0 10px 25px rgba(255, 107, 53, 0.25); + box-shadow: 0 10px 25px rgba(255, 107, 53, 0.2); } .send-btn:hover:not(:disabled) { transform: translateY(-2px); - box-shadow: 0 14px 30px rgba(255, 107, 53, 0.35); + box-shadow: 0 14px 30px rgba(255, 107, 53, 0.3); } .send-btn:active:not(:disabled) { @@ -141,7 +228,7 @@ } .send-btn .material-icons { - font-size: 28px; + font-size: 24px; } /* ── Typing Indicator ── */ @@ -166,3 +253,75 @@ 0%, 100% { opacity: 0.3; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.1); } } + +/* ── Welcome State ── */ +.welcome-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + text-align: center; + padding: 40px; + opacity: 0.6; +} + +.welcome-icon { + width: 64px; + height: 64px; + background: white; + border-radius: 20px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 24px; + box-shadow: 0 10px 30px rgba(0,0,0,0.05); +} + +.welcome-icon span { + font-size: 32px; + color: #1a1a1a; +} + +.welcome-state h3 { + font-size: 1rem; + font-weight: 700; + color: #1a1a1a; + margin-bottom: 12px; +} + +.welcome-state p { + font-size: 0.75rem; + color: #999; + max-width: 200px; + line-height: 1.5; + margin-bottom: 32px; +} + +.suggested-starters { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + max-width: 240px; +} + +.suggested-starters button { + background: white; + border: 1px solid rgba(0,0,0,0.05); + padding: 12px 16px; + border-radius: 12px; + font-size: 0.7rem; + color: #555; + text-align: left; + transition: all 0.2s; + cursor: pointer; +} + +.suggested-starters button:hover { + background: #fdfdfd; + border-color: rgba(255, 107, 53, 0.2); + color: #ff6b35; + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0,0,0,0.03); +} diff --git a/frontend/src/components/ChatPanel.jsx b/frontend/src/components/ChatPanel.jsx index 691f4b03d0096783926c9315af132c93f33bf1de..fb4cb143de3ac62f87285e7da2ee54cd2ba4e6bd 100644 --- a/frontend/src/components/ChatPanel.jsx +++ b/frontend/src/components/ChatPanel.jsx @@ -7,7 +7,7 @@ import './ChatPanel.css'; * Layer 3: Interaction (Utility Layer) * Handles live conversation and input. */ -function ChatPanel({ messages, onSendMessage, isTyping, onInputStateChange }) { +function ChatPanel({ messages, onSendMessage, onNewChat, userAvatar, isTyping, onInputStateChange }) { const [input, setInput] = useState(''); const scrollRef = useRef(null); @@ -37,16 +37,23 @@ function ChatPanel({ messages, onSendMessage, isTyping, onInputStateChange }) { return (
+ {messages.length === 0 && ( +
+
+ lens_blur +
+

Neural Core Initialized.

+

Feed me a thought to begin building your neural pathways.

+
+ )} {messages.map((msg, i) => (
-
-
- {msg.role === 'user' ? ( - account_circle - ) : ( - lens_blur - )} -
+
+ {msg.role === 'user' ? ( + User + ) : ( + lens_blur + )}
@@ -72,6 +79,15 @@ function ChatPanel({ messages, onSendMessage, isTyping, onInputStateChange }) {
+
{ + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return Math.abs(hash); +}; + +function CognitiveBrain3DScene({ state = 'idle', refreshTick }) { + const [graphData, setGraphData] = useState({ nodes: [], links: [] }); + const [newNodes, setNewNodes] = useState(new Set()); + const [dimensions, setDimensions] = useState({ width: 500, height: 400 }); + const [loading, setLoading] = useState(false); + + const containerRef = useRef(null); + const fgRef = useRef(); + + // Highlight the active lobe based on SOMA's cognitive phase + const activeRegion = PHASE_TO_REGION[state] || null; + + // Track layout resize + useEffect(() => { + if (!containerRef.current) return; + const resizeObserver = new ResizeObserver((entries) => { + for (let entry of entries) { + const { width, height } = entry.contentRect; + setDimensions({ width: width || 500, height: height || 400 }); + } + }); + resizeObserver.observe(containerRef.current); + return () => resizeObserver.disconnect(); + }, []); + + // Fetch real-time learned knowledge from Neo4j database + useEffect(() => { + const fetchBrainData = async () => { + setLoading(true); + try { + const res = await apiFetch('/api/v1/graph'); + if (!res.ok) throw new Error('Failed to fetch graph data'); + const data = await res.json(); + + // 1. Start with the permanent brain structures (The 4 Lobes) + const nodes = LOBES.map(l => ({ ...l })); + const links = []; + + // Track new nodes to trigger the "synaptic spark" animation + const currentEntityIds = new Set(data.nodes?.map(n => n.id) || []); + const previouslyKnownIds = new Set(graphData.nodes.filter(n => n.type !== 'lobe').map(n => n.id)); + const newlyDiscovered = new Set([...currentEntityIds].filter(id => !previouslyKnownIds.has(id))); + + if (newlyDiscovered.size > 0) { + setNewNodes(newlyDiscovered); + // Clear spark animation after 4 seconds + setTimeout(() => setNewNodes(new Set()), 4000); + } + + if (data.nodes && data.nodes.length > 0) { + // 2. Map actual database entities into the brain + data.nodes.forEach(n => { + const entityId = n.id; + + // Assign this entity deterministically to a brain lobe based on name hash + const assignedLobeIndex = hashString(entityId) % LOBES.length; + const targetLobeId = LOBES[assignedLobeIndex].id; + + nodes.push({ + id: entityId, + label: n.label || entityId, + connections: n.connections || 1, + type: 'entity' + }); + + // Structural bio-link to anchor it to its parent lobe + links.push({ + source: targetLobeId, + target: entityId, + type: 'structural' + }); + }); + + // 3. Map actual semantic links (Komal -> IS_A -> Student) + if (data.edges) { + data.edges.forEach(e => { + links.push({ + source: e.source, + target: e.target, + label: e.label || 'ASSOCIATED_WITH', + type: 'semantic' + }); + }); + } + } else { + // If Neo4j is offline/empty, seed a few highly visual concept cells + // representing basic thoughts so it never looks completely empty + const sampleEntities = ['Komal', 'Student', 'SOMA', 'AI Architecture', 'Learning System']; + sampleEntities.forEach((entityId, index) => { + const targetLobeId = LOBES[index % LOBES.length].id; + nodes.push({ id: entityId, label: entityId, connections: 2, type: 'entity' }); + links.push({ source: targetLobeId, target: entityId, type: 'structural' }); + }); + links.push({ source: 'Komal', target: 'Student', label: 'IS_A', type: 'semantic' }); + links.push({ source: 'SOMA', target: 'AI Architecture', label: 'BUILT_ON', type: 'semantic' }); + } + + setGraphData({ nodes, links }); + } catch (error) { + console.error('Brain mesh load error', error); + } finally { + setLoading(false); + } + }; + + fetchBrainData(); + }, [refreshTick]); + + // Adjust camera to focus on the active lobe during cognitive processing + useEffect(() => { + if (activeRegion && fgRef.current) { + const activeLobe = LOBES.find(l => l.key === activeRegion); + if (activeLobe) { + fgRef.current.cameraPosition( + { x: activeLobe.fx * 1.5, y: activeLobe.fy * 1.5, z: 180 }, // Move camera closer + { x: activeLobe.fx * 0.5, y: activeLobe.fy * 0.5, z: 0 }, // Point at the lobe + 1200 + ); + } + } else if (fgRef.current) { + // Zoom out to global brain view when idle + fgRef.current.cameraPosition({ x: 0, y: 0, z: 240 }, { x: 0, y: 0, z: 0 }, 1500); + } + }, [activeRegion]); + + // Dynamic Node Styling + const getNodeColor = (node) => { + if (node.type === 'lobe') { + // Core Lobes glow when they are actively processing + if (node.key === activeRegion) { + return '#ffffff'; // Intense active state (pure white core) + } + return 'rgba(6, 182, 212, 0.4)'; // Faint resting lobe core (Cyan outline) + } + + // Spark animation for newly learned nodes (Bright yellow-gold) + if (newNodes.has(node.id)) { + return '#f59e0b'; + } + + // Regular synapses colored based on connection degree + return (node.connections || 1) > 3 ? '#ec4899' : '#06b6d4'; + }; + + const getNodeVal = (node) => { + if (node.type === 'lobe') { + // Active processing lobes grow dynamically in physical scale + return node.key === activeRegion ? 12 : 5; + } + // Normal synapses sized by centrality + return Math.max(1.8, Math.min((node.connections || 1) * 1.2, 6)); + }; + + return ( +
+ {/* 3D WebGL Learning Brain Simulation */} + ` +
+ ${node.label || node.id} + ${node.type === 'lobe' ? 'Brain Cortex Lobe' : 'Learned Synaptic Concept'} +
+ `} + nodeColor={getNodeColor} + nodeVal={getNodeVal} + nodeRelSize={3} + // Link customizations to distinguish biology vs semantic relations + linkColor={link => { + if (link.type === 'structural') return 'rgba(255,255,255,0.03)'; // Structural bio-lines are nearly invisible + return 'rgba(6, 182, 212, 0.25)'; // Real semantic memories are glowing cyan axons + }} + linkWidth={link => (link.type === 'structural' ? 0.5 : 1.5)} + linkDirectionalParticles={link => { + if (link.type === 'structural') return 0; + // Newly activated semantic links run rapid data flows + return link.source.id === activeRegion || link.target.id === activeRegion ? 6 : 2; + }} + linkDirectionalParticleSpeed={0.008} + linkDirectionalParticleWidth={link => (link.type === 'structural' ? 0 : 2.5)} + linkDirectionalParticleColor={() => '#ec4899'} // Hot pink thought particles flowing through the axons + showNavInfo={false} + enablePointerInteraction={true} + enableNodeDrag={true} + /> + + {/* Futuristic status HUD overlays */} +
+
+ Cognitive Mode + {state} +
+
+ Active Lobe + {activeRegion || 'STANDBY'} +
+
+ Learned Synapses + + {graphData.nodes.filter(n => n.type !== 'lobe').length} nodes + +
+
+ + {loading &&
Sparking neural networks...
} +
+ ); +} + +export default CognitiveBrain3DScene; diff --git a/frontend/src/components/CognitiveBrainImageScene.jsx b/frontend/src/components/CognitiveBrainImageScene.jsx index 27be900fb170591b66c72e0e5aaf8326f13d1518..fd33603505e8a23e14a037c2c9edd152814f5deb 100644 --- a/frontend/src/components/CognitiveBrainImageScene.jsx +++ b/frontend/src/components/CognitiveBrainImageScene.jsx @@ -1,9 +1,4 @@ -import brainBase from '../assets/brain/brain_base.png'; -import prefrontalImg from '../assets/brain/Prefrontal_Cortex.png'; -import hippocampusImg from '../assets/brain/Hippocampus.png'; -import sensoryImg from '../assets/brain/sensory_cortex.png'; -import thalamusImg from '../assets/brain/thalamus.png'; -import amygdalaImg from '../assets/brain/brain_amygdala.png'; +import brainImg from '../assets/brain/Brain_nobg.png'; import './CognitiveBrainScene.css'; /** @@ -22,9 +17,11 @@ const REGION_MAP = { emotion: 'amygdala', reasoning: 'prefrontal', reflection: 'prefrontal', - language: 'prefrontal', // Fallback to prefrontal for language for now - inhibition: 'thalamus', - graph: 'hippocampus' + language: 'prefrontal', + inhibition: 'hippocampus', // Moved to Temporal for smoother memory filtering flow + graph: 'hippocampus', + listening: 'sensory', + responding: 'prefrontal' }; function CognitiveBrainImageScene({ state = 'idle' }) { @@ -33,52 +30,55 @@ function CognitiveBrainImageScene({ state = 'idle' }) { return (
- {/* Layer 2: Cognitive Signals (Floating Ambient Labels) */} -
-
-
-
- Prefrontal - Reasoning & Reflection + {/* Layer 1: Brain Core */} +
+ {/* Layer 2: Cognitive Signals (Floating Ambient Labels) */} +
+
+
+
+ Prefrontal + Reasoning & Reflection +
-
-
-
-
- Parietal - Perception & Sensory +
+
+
+ Parietal + Perception & Sensory +
-
-
-
-
- Temporal - Memory & Association +
+
+
+ Temporal + Memory & Association +
-
-
- {/* Layer 1: Brain Core (Layered Overlays) */} -
- {/* Base Layer */} - Neural Base - - {/* Region Overlays */} - Prefrontal Cortex - Sensory Cortex - Hippocampus - Thalamus - Amygdala +
+
+
+ Subcortical + Attention & Routing +
+
+
+ Soma Brain - {/* State-specific Glow Hubs (Subtle Ambient Glow behind regions) */} + {/* State Glow Hubs (Subtle Ambient Backglow) */}
- {/* Ambient Breathing (Idle) */} + {/* Interaction Particles */}
diff --git a/frontend/src/components/CognitiveBrainScene.css b/frontend/src/components/CognitiveBrainScene.css index 83f4af3881ecabd910c7b640cb557e8b8554116b..d52a1dc81d2b4ab5fcbd8cb6197958ce1865c43f 100644 --- a/frontend/src/components/CognitiveBrainScene.css +++ b/frontend/src/components/CognitiveBrainScene.css @@ -2,59 +2,81 @@ .brain-viewport { width: 100%; - height: 500px; + height: 100%; position: relative; display: flex; align-items: center; justify-content: center; - perspective: 1000px; + perspective: 1100px; } .brain-core { position: relative; - width: 480px; - height: 480px; /* Force square for layering */ + width: 380px; + height: 380px; z-index: 10; + transition: transform 0.5s ease; } -/* ── Layer Stacking ── */ +/* ── Exclusive Layer Stacking ── */ .brain-layer { position: absolute; top: 0; left: 0; width: 100%; - height: auto; - transition: opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1), filter 0.8s ease; + height: 100%; + object-fit: contain; + opacity: 0; + transform: scale(0.98); + transition: + opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.8s cubic-bezier(0.16, 1, 0.3, 1), + filter 0.6s ease; + pointer-events: none; + filter: contrast(1.1); +} + +.brain-layer.active { + opacity: 1; + transform: scale(1); } -.brain-layer.base { +/* Ensure only ONE layer is ever visible (The Swap) */ +.brain-layer.base.active { z-index: 1; - filter: contrast(1.1) brightness(0.9); } -.brain-layer.region { +.brain-layer.region.active { z-index: 2; - opacity: 0; + opacity: 1 !important; +} + +/* If a region is active, hide the base entirely to avoid overlapping/ghosting */ +.brain-viewport:has(.brain-layer.region.active) .brain-layer.base { + opacity: 0 !important; +} + +.brain-layer.base.active { + /* Clean slate for multiply blend */ } .brain-layer.region.active { - opacity: 1; - filter: brightness(1.2) drop-shadow(0 0 15px rgba(255, 107, 53, 0.4)); + /* Clean slate for multiply blend */ } /* ── Layer 1: Ambient Motion (Breathing) ── */ .ambient-breath { position: absolute; inset: 0; - background: radial-gradient(circle, rgba(255, 107, 53, 0.05) 0%, transparent 75%); + background: radial-gradient(circle, rgba(255, 107, 53, 0.04) 0%, transparent 75%); animation: breathing 8s infinite ease-in-out; pointer-events: none; z-index: -1; } @keyframes breathing { - 0%, 100% { transform: scale(0.95); opacity: 0.3; } - 50% { transform: scale(1.05); opacity: 0.6; } + 0%, 100% { transform: scale(0.96); opacity: 0.3; } + 50% { transform: scale(1.04); opacity: 0.5; } } /* ── Layer 2: Cognitive Signals (Floating Labels) ── */ @@ -71,14 +93,15 @@ align-items: center; gap: 16px; opacity: 0.2; - transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1); - filter: grayscale(1) blur(1px); + transition: all 0.6s ease; + filter: grayscale(1) blur(0.5px); } .signal-node.active { opacity: 1; filter: grayscale(0) blur(0); - transform: scale(1.15) translateX(10px); + transform: scale(1.1) translateX(10px); + transition: all 0.1s ease-out; } .signal-node.hot { @@ -88,74 +111,96 @@ } @keyframes signal-hot-throb { - 0%, 100% { transform: scale(1.15) translateX(10px); filter: brightness(1); } - 50% { transform: scale(1.2) translateX(15px); filter: brightness(1.4); } + 0%, 100% { transform: scale(1.1) translateX(10px); filter: brightness(1); } + 50% { transform: scale(1.15) translateX(15px); filter: brightness(1.3); } } .signal-dot { width: 10px; height: 10px; - background-color: #ff6b35; + background-color: #d1d5db; border-radius: 50%; - box-shadow: 0 0 20px rgba(255, 107, 53, 0.6); + box-shadow: none; + transition: all 0.6s ease; +} + +.signal-node.active .signal-dot { + transition: all 0.1s ease-out; } +/* ── Active Color Mapping ── */ +.signal-node.active.prefrontal .signal-dot { background-color: #f59e0b; box-shadow: 0 0 15px rgba(245, 158, 11, 0.8); } +.signal-node.active.parietal .signal-dot { background-color: #ff6b35; box-shadow: 0 0 15px rgba(255, 107, 53, 0.8); } +.signal-node.active.temporal .signal-dot { background-color: #10b981; box-shadow: 0 0 15px rgba(16, 185, 129, 0.8); } +.signal-node.active.thalamus-label .signal-dot { background-color: #3b82f6; box-shadow: 0 0 15px rgba(59, 130, 246, 0.8); } + +.signal-node.active.prefrontal strong { color: #f59e0b; } +.signal-node.active.parietal strong { color: #ff6b35; } +.signal-node.active.temporal strong { color: #10b981; } +.signal-node.active.thalamus-label strong { color: #3b82f6; } + .signal-copy { display: flex; flex-direction: column; } .signal-copy strong { - font-size: 0.9rem; + font-size: 0.7rem; font-weight: 800; color: #1a1a1a; line-height: 1.1; + transition: color 0.6s ease; +} + +.signal-node.active strong { + transition: color 0.1s ease-out; } .signal-copy span { - font-size: 0.75rem; + font-size: 0.6rem; color: #999; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; } -.prefrontal { top: 22%; left: -20px; } -.parietal { top: 15%; right: -20px; flex-direction: row-reverse; text-align: right; } -.temporal { bottom: 20%; left: -20px; } +.prefrontal { top: 5%; left: -60px; } +.parietal { top: 5%; right: -60px; flex-direction: row-reverse; text-align: right; } +.temporal { bottom: 10%; left: -60px; } +.thalamus-label { bottom: 10%; right: -60px; flex-direction: row-reverse; text-align: right; } -/* ── State Glow Hubs (Subtle Backglow) ── */ +/* ── State Glow Hubs (Subtle Ambient Backglow) ── */ .glow-hub { position: absolute; - width: 250px; - height: 250px; + width: 400px; + height: 400px; border-radius: 50%; - filter: blur(60px); + filter: blur(100px); opacity: 0; - transition: all 1s ease; + transition: opacity 1.2s ease; pointer-events: none; z-index: 0; } .glow-hub.visible { - opacity: 0.3; + opacity: 0.25; } -.prefrontal-glow { top: 5%; left: 10%; background: radial-gradient(circle, rgba(255, 107, 53, 0.4) 0%, transparent 70%); } -.parietal-glow { top: 5%; right: 10%; background: radial-gradient(circle, rgba(59, 130, 246, 0.3) 0%, transparent 70%); } -.temporal-glow { bottom: 10%; left: 20%; background: radial-gradient(circle, rgba(16, 185, 129, 0.3) 0%, transparent 70%); } -.thalamus-glow { top: 20%; left: 30%; background: radial-gradient(circle, rgba(245, 158, 11, 0.4) 0%, transparent 70%); } +.prefrontal-glow { top: 0%; left: 5%; background: radial-gradient(circle, rgba(255, 107, 53, 0.3) 0%, transparent 70%); } +.parietal-glow { top: 0%; right: 5%; background: radial-gradient(circle, rgba(59, 130, 246, 0.2) 0%, transparent 70%); } +.temporal-glow { bottom: 5%; left: 15%; background: radial-gradient(circle, rgba(16, 185, 129, 0.2) 0%, transparent 70%); } +.thalamus-glow { top: 15%; left: 25%; background: radial-gradient(circle, rgba(245, 158, 11, 0.3) 0%, transparent 70%); } /* ── Listening/Responding Shimmer ── */ .listening-particles { position: absolute; inset: 0; - background: radial-gradient(circle, rgba(255, 107, 53, 0.05) 0%, transparent 85%); - animation: brain-shimmer 2s infinite alternate; + background: radial-gradient(circle, rgba(255, 107, 53, 0.03) 0%, transparent 85%); + animation: brain-shimmer 3s infinite alternate; z-index: 5; } @keyframes brain-shimmer { - from { opacity: 0.2; transform: scale(1); } - to { opacity: 0.5; transform: scale(1.05); } + from { opacity: 0.1; transform: scale(1); } + to { opacity: 0.3; transform: scale(1.05); } } diff --git a/frontend/src/components/CognitiveDashboard.css b/frontend/src/components/CognitiveDashboard.css index 8ce67af0345d8ca6237d70ff057bb55c3942ec34..9881516b2981e193e0d9dc7332250d28db9293e3 100644 --- a/frontend/src/components/CognitiveDashboard.css +++ b/frontend/src/components/CognitiveDashboard.css @@ -1,66 +1,65 @@ /* CognitiveDashboard.css - Status Hub Refinements */ .status-layout { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 64px; - height: 100%; - align-items: center; -} - -.status-visual { display: flex; - justify-content: center; - position: relative; + flex-direction: column; + gap: 16px; + width: 100%; } -/* ── Status Cards ── */ +/* ── Status Cards Redesign ── */ .status-sidebar { display: flex; flex-direction: column; - gap: 32px; + gap: 16px; } .status-card { - background-color: white; - border-radius: 32px; - padding: 32px; - box-shadow: 0 4px 20px rgba(0,0,0,0.02); - border: 1px solid rgba(0,0,0,0.03); + background: rgba(0, 0, 0, 0.02); + border-radius: 20px; + padding: 20px; + border: 1px solid rgba(0, 0, 0, 0.03); + position: relative; + overflow: hidden; } .status-card h3 { - font-size: 0.75rem; + font-size: 0.6rem; text-transform: uppercase; - letter-spacing: 0.1em; + letter-spacing: 0.15em; color: #999; - margin-bottom: 24px; - font-weight: 700; + margin-bottom: 12px; + font-weight: 800; } -/* ── Waves & Charts ── */ +/* ── Waves & HUD Charts ── */ .system-chart { - margin-bottom: 24px; - position: relative; -} - -.system-chart-svg { - width: 100%; - height: 100px; + margin-bottom: 16px; + padding: 12px 16px; + background: white; + border-radius: 12px; + box-shadow: 0 4px 15px rgba(0,0,0,0.02); + border: 1px solid rgba(0,0,0,0.02); } .system-chart-svg path { fill: none; stroke: #ff6b35; - stroke-width: 2.5; + stroke-width: 2; stroke-linecap: round; - opacity: 0.8; + filter: drop-shadow(0 0 4px rgba(255, 107, 53, 0.2)); + transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.intensity-high .system-chart-svg path { + stroke-width: 4; + stroke: #ff3d00; } .system-chart-svg path.secondary { stroke: #3b82f6; stroke-width: 1.5; - opacity: 0.3; + opacity: 0.15; } .chart-labels { @@ -71,84 +70,87 @@ } .chart-labels span { - font-size: 0.7rem; + font-family: 'Roboto Mono', monospace; + font-size: 0.55rem; color: #bbb; - font-weight: 500; } .current-state { display: flex; justify-content: space-between; align-items: center; - padding-top: 24px; - border-top: 1px solid #f0f0f0; + padding-top: 12px; + gap: 32px; } .current-state span { - font-size: 0.85rem; - color: #888; - font-weight: 600; + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.15em; + color: #999; + font-weight: 700; } .current-state strong { - font-size: 1.2rem; + font-size: 1.6rem; color: #ff6b35; - font-weight: 700; + font-weight: 800; text-transform: capitalize; } -/* ── Metrics ── */ +/* ── Metrics Grid ── */ .metric-grid { display: grid; grid-template-columns: 1fr 1fr; - gap: 20px; + gap: 16px; } .metric-item { display: flex; - flex-direction: column; - gap: 12px; - padding: 24px; - background-color: #f9f9f9; + align-items: center; + padding: 20px 24px; + background: white; border-radius: 20px; - transition: transform 0.3s ease; -} - -.metric-item:hover { - transform: translateY(-4px); + box-shadow: 0 4px 15px rgba(0,0,0,0.02); + border: 1px solid rgba(0,0,0,0.03); + gap: 16px; } .metric-icon-wrap { - width: 40px; - height: 40px; + width: 42px; + height: 42px; + min-width: 42px; border-radius: 12px; - background-color: white; + background: rgba(255, 107, 53, 0.05); display: flex; align-items: center; justify-content: center; color: #ff6b35; - box-shadow: 0 4px 10px rgba(0,0,0,0.03); } .metric-icon-wrap .material-icons { - font-size: 20px; + font-size: 22px; } .metric-info { display: flex; flex-direction: column; + flex: 1; } .metric-info label { - font-size: 0.75rem; + font-size: 0.6rem; + text-transform: uppercase; + letter-spacing: 0.1em; color: #999; - font-weight: 600; - margin-bottom: 2px; + font-weight: 700; + white-space: nowrap; } -.metric-info strong { - font-size: 1.3rem; +.metric-item strong { + font-size: 1.5rem; color: #1a1a1a; - font-weight: 700; - letter-spacing: -0.02em; + font-family: 'Roboto Mono', monospace; + letter-spacing: -0.05em; + margin-left: auto; } diff --git a/frontend/src/components/CognitiveDashboard.jsx b/frontend/src/components/CognitiveDashboard.jsx index 68e516d2d0fbffb20d90976cca457a58c93d4403..3c1feaefec74848ad5add047470c5ada17c84967 100644 --- a/frontend/src/components/CognitiveDashboard.jsx +++ b/frontend/src/components/CognitiveDashboard.jsx @@ -1,19 +1,15 @@ import './CognitiveDashboard.css'; -import CognitiveBrainImageScene from './CognitiveBrainImageScene'; +import { useState, useEffect, useRef } from 'react'; function CognitiveDashboard({ statusText, stats }) { return (
-
- -
-
-
+

Neural Vitals

- +
- Current Status + Neural Frequency {statusText}
@@ -39,17 +35,62 @@ function CognitiveDashboard({ statusText, stats }) { ); } -function SystemStatusChart() { - // SVG points for smooth overlapping waves - const wave1 = "M0,40 Q30,5 60,40 T120,40 T180,40 T240,40 T300,40"; - const wave2 = "M0,50 Q40,25 80,50 T160,50 T240,50 T320,50 T400,50"; +function SystemStatusChart({ state }) { + const [path1, setPath1] = useState(""); + const [path2, setPath2] = useState(""); + const frameRef = useRef(0); + + const intensity = ['reasoning', 'language', 'reflection'].includes(state) ? 'high' : + ['recall', 'association', 'attention'].includes(state) ? 'medium' : 'low'; + + useEffect(() => { + let animationFrame; + + const generateWave = (time, freq, amp, jitter, complexity = 1) => { + let d = "M0,40 "; + const step = complexity > 1 ? 5 : 10; + for (let x = 0; x <= 300; x += step) { + // Complex brainwave math: Base Sine + Harmonics for interference + let yBase = Math.sin(x * freq + time) * amp; + if (complexity > 1) { + yBase += Math.sin(x * freq * 2.3 + time * 1.4) * (amp * 0.4); + yBase += Math.sin(x * freq * 3.9 + time * 2.1) * (amp * 0.2); + } + + const noise = (Math.random() - 0.5) * jitter; + const y = 40 + yBase + noise; + d += `L${x},${y} `; + } + return d; + }; + + const animate = () => { + frameRef.current += intensity === 'high' ? 0.28 : intensity === 'medium' ? 0.14 : 0.06; + const t = frameRef.current; + + const config = { + high: { freq: 0.12, amp: 26, jitter: 14, comp: 3 }, + medium: { freq: 0.08, amp: 18, jitter: 4, comp: 1 }, + low: { freq: 0.03, amp: 10, jitter: 1, comp: 1 } + }[intensity]; + + setPath1(generateWave(t, config.freq, config.amp, config.jitter, config.comp)); + setPath2(generateWave(t * 0.8, config.freq * 0.6, config.amp * 0.5, config.jitter * 0.4, 1)); + + animationFrame = requestAnimationFrame(animate); + }; + + animate(); + return () => cancelAnimationFrame(animationFrame); + }, [intensity]); + const labels = ['ALPHA', 'BETA', 'GAMMA', 'DELTA', 'THETA']; return ( -
+
- - + +
{labels.map(l => {l})} diff --git a/frontend/src/components/CognitiveTimeline.css b/frontend/src/components/CognitiveTimeline.css index 27cbe5f42595d09527aec725ffaab19898708630..31cd16d42adfcb496ee6a09a229625562b304a9e 100644 --- a/frontend/src/components/CognitiveTimeline.css +++ b/frontend/src/components/CognitiveTimeline.css @@ -3,8 +3,8 @@ .timeline-container { display: flex; flex-direction: column; - gap: 32px; - padding: 16px 0; + gap: 16px; + padding: 0; } .timeline-row { @@ -19,7 +19,7 @@ position: absolute; left: 65px; /* Alignment with dot */ top: 12px; - bottom: -44px; + bottom: -24px; width: 1px; background-color: #eee; z-index: 1; @@ -31,7 +31,7 @@ .timeline-time { width: 55px; - font-size: 0.75rem; + font-size: 0.65rem; color: #bbb; font-family: var(--font-mono); padding-top: 4px; @@ -57,11 +57,7 @@ .timeline-content { flex: 1; - background-color: white; - padding: 20px 24px; - border-radius: 20px; - box-shadow: 0 4px 12px rgba(0,0,0,0.02); - border: 1px solid rgba(0,0,0,0.03); + padding: 0 0 16px 0; transition: transform 0.3s ease; } @@ -89,14 +85,14 @@ } .phase-title { - font-size: 0.95rem; + font-size: 0.7rem; font-weight: 700; color: #1a1a1a; letter-spacing: -0.01em; } .phase-desc { - font-size: 0.9rem; + font-size: 0.8rem; color: #666; line-height: 1.5; } diff --git a/frontend/src/components/CognitiveTimeline.jsx b/frontend/src/components/CognitiveTimeline.jsx index eacb6e0542bf0db3954b4a9986eb6d84657f6cba..cc782232484956ab337a9ef5149c70282fad7807 100644 --- a/frontend/src/components/CognitiveTimeline.jsx +++ b/frontend/src/components/CognitiveTimeline.jsx @@ -3,23 +3,50 @@ import './CognitiveTimeline.css'; const PHASE_CONFIG = { perception: { icon: 'visibility', color: '#ff6b35', label: 'Perception' }, attention: { icon: 'track_changes', color: '#3b82f6', label: 'Attention' }, + emotion: { icon: 'favorite', color: '#e11d48', label: 'Emotion' }, + routing: { icon: 'route', color: '#f59e0b', label: 'Routing' }, + prediction: { icon: 'online_prediction', color: '#8b5cf6', label: 'Prediction' }, + working_memory: { icon: 'memory', color: '#06b6d4', label: 'Working Memory' }, + reflection: { icon: 'lightbulb', color: '#f59e0b', label: 'Reflection' }, recall: { icon: 'psychology', color: '#10b981', label: 'Recall' }, + inhibition: { icon: 'block', color: '#ef4444', label: 'Inhibition' }, + association: { icon: 'device_hub', color: '#3b82f6', label: 'Association' }, reasoning: { icon: 'hub', color: '#ff6b35', label: 'Reasoning' }, - output: { icon: 'chat_bubble_outline', color: '#8ab892', label: 'Output' } + language: { icon: 'chat_bubble', color: '#8ab892', label: 'Language' }, + memory: { icon: 'save', color: '#10b981', label: 'Consolidation' }, + graph: { icon: 'share', color: '#3b82f6', label: 'Graph Update' }, }; function CognitiveTimeline({ trace }) { - if (!trace || trace.length === 0) { + const rawTrace = trace + .filter(item => item.phase) + .reverse(); // Chronological order + + const consolidatedTrace = []; + let currentPhase = null; + + rawTrace.forEach(item => { + if (currentPhase && currentPhase.phase === item.phase) { + currentPhase.message = item.message || item.content || item.desc; + currentPhase.time = item.time; + } else { + currentPhase = { ...item }; + consolidatedTrace.push(currentPhase); + } + }); + + if (!consolidatedTrace || consolidatedTrace.length === 0) { return (
- No cognitive activity recorded yet... + sensors +

Awaiting sensory stimuli...

); } return (
- {trace.map((item, index) => { + {consolidatedTrace.map((item, index) => { const config = PHASE_CONFIG[item.phase.toLowerCase()] || { icon: 'circle', color: '#ccc', label: item.phase }; return ( @@ -36,7 +63,7 @@ function CognitiveTimeline({ trace }) {
{config.label}
- {item.content || item.desc || 'Processing information...'} + {item.message || item.content || item.desc || 'Processing information...'}
diff --git a/frontend/src/components/DreamSequence.css b/frontend/src/components/DreamSequence.css index 4f28ecfecaffb067891ed3bb484431261c2b05e3..8921f3299740523abdf539add8687b1052418803 100644 --- a/frontend/src/components/DreamSequence.css +++ b/frontend/src/components/DreamSequence.css @@ -1,220 +1,515 @@ -/* DreamSequence.css - Refined for Page 7 & 8 */ +/* ── Sleep & Consolidation Page Styles (Split Layout) ── */ -.sleep-layout { +.sleep-page-container { display: flex; flex-direction: column; align-items: center; justify-content: center; + min-height: 600px; + background-color: #eaeaea; /* Exact light gray background as in Image 1 */ + width: 100%; height: 100%; - text-align: center; + padding: 40px; + box-sizing: border-box; + position: relative; + border-radius: 24px; } -.sleep-brain-wrap { - position: relative; - margin-bottom: 56px; - transform: scale(1.1); +.sleep-page-header { + position: absolute; + top: 40px; + left: 40px; } -/* ── Progress ── */ -.sleep-copy h3 { - font-family: var(--font-display); - font-size: 2.2rem; +.sleep-page-header h2 { + font-family: var(--font-display), 'Inter', sans-serif; + font-size: 1.2rem; /* Exact page header font-size as Console */ font-weight: 700; - margin-bottom: 12px; color: #1a1a1a; + margin: 0; letter-spacing: -0.01em; } -.sleep-copy p { - font-size: 1.1rem; - color: #888; - margin-bottom: 64px; +/* ── Split Layout Grid ── */ +.sleep-split-layout { + display: grid; + grid-template-columns: 1.1fr 1fr; + gap: 60px; + width: 100%; + max-width: 960px; + align-items: center; + justify-content: center; + margin-top: 40px; +} + +.sleep-left-panel { + display: flex; + justify-content: center; + align-items: center; } -.sleep-steps { +.sleep-brain-wrap-combined { + position: relative; display: flex; - gap: 80px; justify-content: center; + align-items: center; +} + +.sleep-brain-image-asset { + max-width: 380px; + height: auto; + display: block; + mix-blend-mode: normal; /* Blends perfectly because background colors match exactly */ + opacity: 0.95; +} + +.sleep-right-panel { + display: flex; + flex-direction: column; + align-items: flex-start; + text-align: left; +} + +.sleep-copy-combined { + text-align: left; + margin-bottom: 40px; +} + +.sleep-copy-combined h3 { + font-family: var(--font-display), 'Inter', sans-serif; + font-size: 1.15rem; /* Exact block header font-size as Console */ + font-weight: 700; + color: #1a1a1a; + margin: 0 0 8px 0; + letter-spacing: -0.01em; +} + +.sleep-copy-combined p { + font-size: 0.75rem; /* Exact body font-size as Console */ + color: #666; + margin: 0; + font-weight: 500; + line-height: 1.45; +} + +/* ── Vertical Progress Steps (Optimized for Split Panel) ── */ +.sleep-steps-vertical { + display: flex; + flex-direction: column; width: 100%; - max-width: 800px; - padding-top: 48px; - border-top: 1px solid #f0f0f0; + gap: 28px; +} + +.sleep-step-item-wrap-vertical { + position: relative; + display: flex; + flex-direction: column; } -.sleep-step { +.sleep-step-item-vertical { display: flex; align-items: center; gap: 16px; + opacity: 0.5; + transition: opacity 0.3s ease; + z-index: 2; } -.sleep-step-dot { - width: 28px; - height: 28px; +.sleep-step-item-vertical.active { + opacity: 1; +} + +.sleep-step-circle { + width: 24px; + height: 24px; border-radius: 50%; - border: 2px solid #ddd; display: flex; align-items: center; justify-content: center; - font-size: 0.8rem; - font-weight: 700; - color: #bbb; - transition: all 0.3s; + box-sizing: border-box; + background-color: #eaeaea; /* matches parent background to blend connector lines */ + transition: all 0.3s ease; } -.sleep-step-dot.active { - border-color: #ff6b35; - color: #ff6b35; - box-shadow: 0 0 15px rgba(255, 107, 53, 0.2); +/* 1. Completed step (Green Checkmark) */ +.sleep-step-circle.completed { + border: 2px solid #10b981; + color: #10b981; } -.sleep-step-dot.completed { - border-color: #10b981; - color: #10b981; - background-color: transparent; +.sleep-step-circle.completed .material-icons { + font-size: 16px; + font-weight: 900; } -.sleep-step-label { - font-size: 1rem; - color: #999; +/* 2. Active step (Orange Ring with Dot) */ +.sleep-step-circle.active { + border: 2px solid #ff6b35; + position: relative; +} + +.active-dot-inner { + width: 8px; + height: 8px; + background-color: #ff6b35; + border-radius: 50%; +} + +/* 3. Pending step (Grey Ring) */ +.sleep-step-circle.pending { + border: 2px solid #cbd5e1; +} + +.sleep-step-text { + font-family: var(--font-display), 'Inter', sans-serif; + font-size: 0.8rem; /* Exact list font-size as Console activity items */ + color: #444; font-weight: 600; } -.sleep-step-label.active { +.sleep-step-item-vertical.active .sleep-step-text { color: #1a1a1a; + font-weight: 700; +} + +/* Vertical connector lines */ +.sleep-step-connector-vertical { + position: absolute; + left: 11px; + top: -28px; + height: 28px; + width: 2px; + z-index: 1; +} + +.connector-line { + width: 100%; + height: 100%; + background-color: #cbd5e1; } -/* ── Summary Card (Page 8) ── */ -.summary-overlay { +/* ── Consolidation Summary Overlay (Image 2) ── */ +.summary-overlay-combined { position: absolute; top: 0; left: 0; right: 0; bottom: 0; - background-color: rgba(229, 229, 229, 0.9); + background-color: #eaeaea; /* Exact solid color to overlay completely as shown in Image 2 */ display: flex; + flex-direction: column; align-items: center; justify-content: center; z-index: 100; - backdrop-filter: blur(12px); + padding: 40px; + box-sizing: border-box; +} + +.sleep-page-header-summary { + position: absolute; + top: 40px; + left: 40px; +} + +.sleep-page-header-summary h2 { + font-family: var(--font-display), 'Inter', sans-serif; + font-size: 1.2rem; /* Matches unified page header size */ + font-weight: 700; + color: #1a1a1a; + margin: 0; } -.summary-card { +.summary-card-combined, +.summary-card-inline { width: 100%; - max-width: 520px; - background-color: white; - border-radius: 40px; - padding: 48px; - box-shadow: 0 30px 80px rgba(0,0,0,0.12); + max-width: 380px; /* Slimmer card width for better alignment */ + background-color: #f3f3f3; + border-radius: 24px; + padding: 22px; /* Denser, high-fidelity margins */ + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.05); position: relative; text-align: left; - border: 1px solid rgba(0,0,0,0.02); + border: 1px solid rgba(0, 0, 0, 0.02); + box-sizing: border-box; } -.summary-close { +.summary-close-combined { position: absolute; - top: 32px; - right: 32px; - color: #bbb; - font-size: 1.8rem; + top: 24px; + right: 24px; + color: #888; background: none; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; } -.summary-close:hover { +.summary-close-combined .material-icons { + font-size: 20px; +} + +.summary-close-combined:hover { color: #1a1a1a; } -.summary-header { +.summary-header-combined { display: flex; - gap: 24px; - margin-bottom: 40px; + gap: 12px; + margin-bottom: 18px; + align-items: center; } -.summary-header-icon { - width: 64px; - height: 64px; - background-color: #f5f5f5; - border-radius: 20px; +.summary-header-icon-combined { + width: 36px; + height: 36px; + background-color: transparent; + border-radius: 50%; display: flex; align-items: center; justify-content: center; - color: #1a1a1a; + color: #475569; } -.summary-header-icon .material-icons { - font-size: 32px; +.summary-header-icon-combined .material-icons { + font-size: 24px; } -.summary-header-copy h3 { - font-family: var(--font-display); - font-size: 1.6rem; +.summary-header-copy-combined h3 { + font-family: var(--font-display), 'Inter', sans-serif; + font-size: 1.05rem; /* Balanced, high-density title */ font-weight: 700; - margin-bottom: 4px; color: #1a1a1a; + margin: 0 0 2px 0; + letter-spacing: -0.01em; } -.summary-header-copy p { - font-size: 1rem; - color: #888; +.summary-header-copy-combined p { + font-size: 0.72rem; /* Matches unified small captions */ + color: #666; + margin: 0; + font-weight: 500; } -.summary-list { +.summary-list-combined { display: flex; flex-direction: column; - gap: 16px; - margin-bottom: 40px; + gap: 12px; + margin-bottom: 28px; } -.summary-item { +.summary-item-combined { display: flex; align-items: center; - gap: 20px; - padding: 20px; - border-radius: 20px; - background-color: #f8f8f8; - border: 1px solid rgba(0,0,0,0.02); + gap: 12px; /* Denser item gap */ + padding: 10px 14px; /* Highly compact padding */ + border-radius: 14px; + background-color: rgba(0, 0, 0, 0.025); + border: 1px solid rgba(0, 0, 0, 0.01); } -.summary-item-icon { - width: 44px; - height: 44px; - border-radius: 14px; +.summary-item-icon-combined { + width: 30px; + height: 30px; + border-radius: 10px; display: flex; align-items: center; justify-content: center; + flex-shrink: 0; +} + +.summary-item-icon-combined .material-icons { + font-size: 16px; /* Compact action icons */ } -.summary-item-copy strong { +.summary-item-copy-combined strong { display: block; - font-size: 1.05rem; + font-size: 0.78rem; /* Matches premium compact metadata headers */ font-weight: 700; color: #1a1a1a; - margin-bottom: 2px; + margin-bottom: 1px; } -.summary-item-copy span { - font-size: 0.85rem; - color: #888; +.summary-item-copy-combined span { + display: block; + font-size: 0.68rem; /* Highly readable compact value subtext */ + color: #666; font-weight: 500; } -.summary-view-btn { +/* Tone styling matching pastel buttons */ +.summary-item-icon-combined.green { color: #10b981; background-color: rgba(16, 185, 129, 0.08); } +.summary-item-icon-combined.teal { color: #0d9488; background-color: rgba(13, 148, 136, 0.08); } +.summary-item-icon-combined.orange { color: #ff6b35; background-color: rgba(255, 107, 53, 0.08); } +.summary-item-icon-combined.amber { color: #f59e0b; background-color: rgba(245, 158, 11, 0.08); } + +.summary-view-btn-combined { width: 100%; - height: 64px; + height: 40px; /* Slim, premium button height */ background-color: #fff1eb; color: #ff6b35; - border-radius: 20px; + border-radius: 14px; + border: none; font-weight: 700; - font-size: 1.1rem; - transition: all 0.2s; + font-size: 0.8rem; /* Compact label size matching Console buttons */ + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; } -.summary-view-btn:hover { +.summary-view-btn-combined:hover { background-color: #ffe4d9; + transform: translateY(-1px); +} + +/* ── Sleep Trigger Landing State Styles ── */ +.sleep-landing-combined { + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; +} + +.sleep-trigger-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + background: linear-gradient(135deg, #ff6b35, #ff5714); + color: white; + border: none; + padding: 12px 30px; /* Highly compact padding */ + border-radius: 16px; + font-family: var(--font-display), 'Inter', sans-serif; + font-size: 0.8rem; /* Exact button text size as Console inputs */ + font-weight: 700; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); + box-shadow: 0 10px 30px rgba(255, 107, 53, 0.25); +} + +.sleep-trigger-btn:hover { transform: translateY(-2px); + box-shadow: 0 15px 40px rgba(255, 107, 53, 0.4); + background: linear-gradient(135deg, #ff5714, #e04400); +} + +.sleep-trigger-btn:active { + transform: translateY(0); +} + +.sleep-trigger-btn .material-icons { + font-size: 22px; +} + +/* ── Sleep Telemetry Metric Dashboard Styles ── */ +.sleep-telemetry-grid { + display: flex; + width: 100%; + max-width: 900px; + gap: 24px; + margin-top: 40px; + animation: fadeInUp 0.8s ease-out; +} + +.sleep-telemetry-card { + flex: 1; + background-color: rgba(255, 255, 255, 0.45); + border: 1px solid rgba(0, 0, 0, 0.04); + border-radius: 20px; + padding: 20px; + text-align: left; + box-sizing: border-box; + transition: all 0.3s ease; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.sleep-telemetry-card:hover { + transform: translateY(-3px); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.02); + background-color: rgba(255, 255, 255, 0.65); +} + +.telemetry-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.telemetry-card-header h4 { + margin: 0; + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #666; + font-weight: 700; +} + +.telemetry-card-header .material-icons { + font-size: 16px; + color: #999; +} + +.telemetry-card-value { + font-size: 1.15rem; /* Compact instrument value font-size */ + font-weight: 700; + color: #1a1a1a; + margin-bottom: 6px; + display: flex; + align-items: center; + gap: 8px; +} + +.telemetry-card-subtext { + font-size: 0.68rem; /* Highly compact detail text */ + color: #888; +} + +/* Adenosine Progress Bar */ +.adenosine-progress-container { + width: 100%; + height: 6px; + background-color: rgba(0, 0, 0, 0.05); + border-radius: 3px; + overflow: hidden; + margin-top: 8px; } -/* Item Tones */ -.summary-item-icon.green { color: #10b981; background-color: #e6f7f0; } -.summary-item-icon.teal { color: #0d9488; background-color: #e0f2f1; } -.summary-item-icon.orange { color: #ff6b35; background-color: #fff1eb; } -.summary-item-icon.amber { color: #f59e0b; background-color: #fff8e1; } +.adenosine-progress-bar { + height: 100%; + border-radius: 3px; + transition: width 1s ease-in-out; +} + +/* Status Badge */ +.telemetry-status-badge { + padding: 4px 10px; + border-radius: 12px; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.05em; + display: inline-block; +} + +.telemetry-status-badge.optimal { + background-color: #e3f9e5; + color: #1e7e34; +} + +.telemetry-status-badge.ready { + background-color: #fff3cd; + color: #856404; + animation: pulseOrange 2s infinite; +} + +@keyframes pulseOrange { + 0% { opacity: 0.8; } + 50% { opacity: 1; transform: scale(1.02); } + 100% { opacity: 0.8; } +} diff --git a/frontend/src/components/DreamSequence.jsx b/frontend/src/components/DreamSequence.jsx index f9f3485eb57f4a515b7b9c00270597ce64316f01..482330bea551c390c5196ea2867439079476f4da 100644 --- a/frontend/src/components/DreamSequence.jsx +++ b/frontend/src/components/DreamSequence.jsx @@ -1,76 +1,178 @@ -import CognitiveBrainImageScene from './CognitiveBrainImageScene'; +import sleepImg from '../assets/brain/sleep_nobg.png'; import './DreamSequence.css'; -export function SleepProgress({ phaseIndex }) { +export function SleepProgress({ phaseIndex, isConsolidating, onStart, summary, vitals, onClose }) { const steps = ['Analyzing Memories', 'Linking Concepts', 'Pruning Redundancies']; + // Parse message count in working memory + const workingCount = parseInt(vitals?.working || 0); + const adenosineScore = Math.min(workingCount * 12.5, 100); + const isOptimal = workingCount >= 4; + return ( -
-
- -
-
-

Soma is consolidating memories...

-

Cleaning, linking and strengthening knowledge.

+
+
+

7. Sleep (Consolidation)

-
- {steps.map((step, index) => { - const completed = index < phaseIndex; - const active = index === phaseIndex; - return ( -
-
- {completed ? check : (index + 1)} +
+ {/* Left Column: Brain Image (ALWAYS VISIBLE!) */} +
+
+ Soma Sleeping Brain +
+
+ + {/* Right Column: Dynamic State Panel */} +
+ {summary ? ( + /* State 1: Consolidation Summary Card (Directly inline on the right side!) */ +
+ + +
+
+ bedtime +
+
+

Sleep Cycle Complete

+

Memory consolidation finished

+
-
{step}
+ +
+ {[ + { label: 'Linked Together', value: `${summary.linked} connections created`, icon: 'hub', tone: 'green' }, + { label: 'Consolidated', value: `${summary.consolidated} summaries saved`, icon: 'inventory_2', tone: 'teal' }, + { label: 'Pruned', value: `${summary.pruned} raw logs pruned`, icon: 'filter_alt', tone: 'orange' }, + ].map((item) => ( +
+
+ {item.icon} +
+
+ {item.label} + {item.value} +
+
+ ))} +
+ +
- ); - })} -
-
- ); -} + ) : isConsolidating ? ( + /* State 2: Active Consolidation Checklist Steps */ +
+
+

Soma is consolidating memories...

+

Cleaning, linking and strengthening knowledge.

+
-export function SleepSummary({ summary, onClose }) { - const data = summary || { linked: 12, consolidated: 28, pruned: 17, strengthened: 34 }; - const items = [ - { label: 'Linked Together', value: `${data.linked} new connections created`, icon: 'hub', tone: 'green' }, - { label: 'Consolidated', value: `${data.consolidated} memories merged`, icon: 'inventory_2', tone: 'teal' }, - { label: 'Pruned', value: `${data.pruned} redundant items removed`, icon: 'filter_alt', tone: 'orange' }, - { label: 'Strengthened', value: `${data.strengthened} memory traces updated`, icon: 'auto_awesome', tone: 'amber' }, - ]; +
+ {steps.map((step, index) => { + const completed = index < phaseIndex; + const active = index === phaseIndex; - return ( -
-
- - -
-
- bedtime + return ( +
+ {index > 0 && ( +
+
+
+ )} +
+
+ {completed && check} + {active &&
} +
+ {step} +
+
+ ); + })} +
+
+ ) : ( + /* State 3: Landing / Idle State with Sleep button */ +
+
+

Reorganize Neural Structures

+

+ Consolidate recent transactional chat logs into the ChromaDB sensory database and extract semantic facts to the Neo4j Knowledge Graph. +

+
+ + +
+ )} +
+
+ + {/* Dynamic Telemetry Metric Dashboard */} +
+ {/* Card 1: Sleep Pressure / Adenosine */} +
+
+

Sleep Pressure (Adenosine)

+ hourglass_empty
-
-

Sleep Cycle Complete

-

Memory consolidation finished

+
+ {adenosineScore.toFixed(0)}% +
+
+ {isOptimal ? 'Sleep pressure high. Consolidation critical.' : 'Accumulating data. Low neural pressure.'} +
+
+
-
- {items.map((item) => ( -
-
- {item.icon} -
-
- {item.label} - {item.value} -
-
- ))} + {/* Card 2: Synaptic Queue Density */} +
+
+

Working Synapses

+ psychology +
+
+ {workingCount} exchanges +
+
+ {workingCount > 0 + ? `${workingCount} raw dialogic traces stored in pre-consolidated state.` + : 'Working memory fully cleared. No traces queued.'} +
- + {/* Card 3: Consolidation Readiness */} +
+
+

System Readiness

+ bolt +
+
+ {isOptimal ? ( + READY TO SLEEP + ) : ( + ACCUMULATING + )} +
+
+ {isOptimal + ? 'Meets minimum requirement (≥ 4 messages) for factual extraction.' + : 'Needs at least 4 chat messages in working memory to trigger.'} +
+
); diff --git a/frontend/src/components/KnowledgeGraph.css b/frontend/src/components/KnowledgeGraph.css index 511011d384bceae569884d368a3744d08f63720f..ecea531856dc224702d00e37d51bfadf35999dba 100644 --- a/frontend/src/components/KnowledgeGraph.css +++ b/frontend/src/components/KnowledgeGraph.css @@ -1,183 +1,390 @@ -/* KnowledgeGraph.css - Refined for Page 5 (Biological Mesh) */ +/* KnowledgeGraph.css - Premium Sci-Fi 3D Semantic Mesh styling */ .graph-stage { display: flex; flex-direction: column; height: 100%; position: relative; + gap: 10px; + padding: 0 16px 16px 16px; + overflow: hidden; } .graph-toolbar { display: flex; justify-content: space-between; - margin-bottom: 32px; + align-items: center; + gap: 12px; } -.graph-select { - background-color: #eee; - border-radius: 18px; - padding: 0 20px; - height: 56px; +.graph-title-block { display: flex; align-items: center; gap: 12px; - font-size: 1rem; + flex-wrap: wrap; +} + +.graph-select { + background: rgba(255, 255, 255, 0.6); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: 100px; + padding: 0 14px; + height: 32px; + display: flex; + align-items: center; + gap: 6px; + font-size: 0.75rem; color: #1a1a1a; - font-weight: 600; + font-weight: 700; + box-shadow: 0 4px 15px rgba(0,0,0,0.02); +} + +.graph-select .material-icons { + color: #06b6d4; + font-size: 16px; } +/* DB Status Badges */ +.db-status-badge { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 14px; + border-radius: 100px; + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + border: 1px solid transparent; +} + +.db-status-badge.warning { + background: rgba(245, 158, 11, 0.08); + color: #d97706; + border-color: rgba(245, 158, 11, 0.2); +} + +.db-status-badge.success { + background: rgba(16, 185, 129, 0.08); + color: #059669; + border-color: rgba(16, 185, 129, 0.2); +} + +.db-status-badge .dot { + width: 6px; + height: 6px; + border-radius: 50%; + background-color: currentColor; +} + +/* Actions */ .graph-actions { display: flex; - gap: 12px; + gap: 10px; } .graph-icon-button { - width: 56px; - height: 56px; - background-color: #eee; - border-radius: 18px; + width: 32px; + height: 32px; + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.9); + border-radius: 10px; display: flex; align-items: center; justify-content: center; - color: #888; + color: #555; transition: all 0.2s; + box-shadow: 0 2px 8px rgba(0,0,0,0.02); + cursor: pointer; +} + +.graph-icon-button .material-icons { + font-size: 16px; } .graph-icon-button:hover { - background-color: #e8e8e8; - color: #1a1a1a; + transform: translateY(-1px); + background: white; + color: #4f46e5; + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15); } -/* ── Network ── */ -.graph-network { +/* ── Network Container ── */ +.graph-network-container { flex: 1; position: relative; - display: flex; - align-items: center; - justify-content: center; + border-radius: 24px; overflow: hidden; + box-shadow: var(--shadow-md), inset 0 0 80px rgba(255, 255, 255, 0.3); + border: 1px solid rgba(255, 255, 255, 0.8); + background: rgba(255, 255, 255, 0.35); + backdrop-filter: blur(25px); + padding-left: 160px; /* Shifted slightly left compared to before to balance the canvas center */ + box-sizing: border-box; } -.graph-links { +.brain-silhouette { position: absolute; - width: 100%; - height: 100%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: min(85vw, 550px); + height: min(85vw, 550px); + pointer-events: none; z-index: 0; + display: flex; + justify-content: center; + align-items: center; + opacity: 0.85; +} + +/* Tooltip micro-HUD */ +.graph-tooltip { + background: rgba(255, 255, 255, 0.96) !important; + border: 1px solid rgba(99, 102, 241, 0.2) !important; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05) !important; + border-radius: 10px !important; + padding: 10px 14px !important; + color: #1e293b !important; + font-family: var(--font-mono), monospace !important; + font-size: 0.75rem !important; pointer-events: none; } -.graph-links line { - stroke: rgba(0, 0, 0, 0.05); - stroke-width: 1; - stroke-dasharray: 2 10; +.graph-tooltip strong { + display: block; + font-size: 0.8rem; + color: #4f46e5; + margin-bottom: 6px; + border-bottom: 1px solid rgba(0,0,0,0.06); + padding-bottom: 4px; } -.graph-node { +.tooltip-sub { + color: #64748b; + margin-top: 2px; +} + +/* Loading HUD overlay */ +.graph-loading { position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; - transform: translate(-50%, -50%); - z-index: 1; - transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1); + gap: 16px; + color: #4f46e5; + font-family: var(--font-mono), monospace; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.1em; + z-index: 10; + pointer-events: none; + background: rgba(255, 255, 255, 0.85); + padding: 24px 36px; + border-radius: 16px; + border: 1px solid rgba(99, 102, 241, 0.2); + backdrop-filter: blur(10px); } -.graph-center { - left: 50%; - top: 50%; +.graph-loading .material-icons { + font-size: 32px; } -.graph-node-core { - width: 100px; - height: 100px; - background-color: #1a1a1a; - border-radius: 50%; +/* Legend - Floating Absolutely in the 3D Container */ +.graph-legend { + position: absolute; + bottom: 16px; + left: calc(50% + 80px); /* Re-centered perfectly relative to the 160px shift */ + transform: translateX(-50%); display: flex; - align-items: center; + gap: 20px; justify-content: center; - color: white; - font-size: 2.2rem; - box-shadow: 0 0 50px rgba(0, 0, 0, 0.15); - margin-bottom: 16px; - animation: core-float 6s infinite ease-in-out; + background: rgba(255, 255, 255, 0.75); + backdrop-filter: blur(25px); + border: 1px solid rgba(255, 255, 255, 0.8); + padding: 8px 24px; + border-radius: 100px; + box-shadow: 0 8px 32px rgba(0,0,0,0.03); + z-index: 100; + pointer-events: auto; } -@keyframes core-float { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.05); filter: brightness(1.1); } +.legend-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.65rem; + color: #555; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.05em; } -.graph-orbit { - width: 44px; - height: 44px; - background-color: white; +.legend-item::before { + content: ''; + width: 6px; + height: 6px; border-radius: 50%; + box-shadow: 0 0 8px currentColor; +} + +.legend-item.core { color: #6366f1; } +.legend-item.core::before { background-color: #6366f1; } + +.legend-item.high { color: #ec4899; } +.legend-item.high::before { background-color: #ec4899; } + +.legend-item.medium { color: #f59e0b; } +.legend-item.medium::before { background-color: #f59e0b; } + +.legend-item.entity { color: #0891b2; } +.legend-item.entity::before { background-color: #0891b2; } + +/* ── Live Telemetry HUD Overlay ── */ +.graph-hud-overlay { + position: absolute; + top: 16px; + left: 16px; + display: flex; + flex-direction: column; + gap: 12px; + z-index: 100; + pointer-events: none; /* Let clicks pass through to 3D graph */ +} + +.hud-panel { + background: rgba(255, 255, 255, 0.75); + backdrop-filter: blur(25px); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: 16px; + padding: 12px 14px; + width: 190px; + box-shadow: 0 8px 32px rgba(0,0,0,0.03); + pointer-events: auto; /* Re-enable clicks for panel contents */ + display: flex; + flex-direction: column; + gap: 8px; + animation: fadeIn 0.4s ease-out; +} + +.hud-header { display: flex; align-items: center; - justify-content: center; - box-shadow: 0 8px 24px rgba(0,0,0,0.04); - border: 1px solid rgba(0,0,0,0.03); - animation: orbit-float 8s infinite ease-in-out; + gap: 6px; + font-size: 0.7rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #4f46e5; + border-bottom: 1px solid rgba(0,0,0,0.04); + padding-bottom: 6px; } -@keyframes orbit-float { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-10px); } +.hud-header .material-icons { + font-size: 14px; } -.graph-orbit strong { - position: absolute; - top: 56px; - width: 140px; - text-align: center; - font-size: 0.85rem; - color: #1a1a1a; - font-weight: 700; - letter-spacing: -0.01em; +.hud-row { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.7rem; } -.graph-node-dot { - width: 10px; - height: 10px; - border-radius: 50%; - background-color: #ff6b35; - box-shadow: 0 0 12px rgba(255, 107, 53, 0.3); +.hud-label { + color: #64748b; + font-weight: 600; } -/* ── Legend ── */ -.graph-legend { +.hud-value { + color: #1e293b; + font-weight: 800; + font-family: var(--font-mono), monospace; +} + +.hud-value.status-glow { + color: #10b981; + background: rgba(16, 185, 129, 0.1); + padding: 2px 6px; + border-radius: 6px; + font-weight: 800; + text-shadow: 0 0 8px rgba(16, 185, 129, 0.2); +} + +.hud-entity-list { display: flex; - gap: 32px; - justify-content: center; - margin-top: 48px; - background-color: white; - padding: 16px 32px; - border-radius: 99px; - box-shadow: 0 4px 15px rgba(0,0,0,0.02); - width: fit-content; - align-self: center; + flex-direction: column; + gap: 6px; } -.legend-item { +.hud-entity-row { display: flex; align-items: center; - gap: 10px; - font-size: 0.75rem; - color: #888; + font-size: 0.65rem; + gap: 4px; +} + +.entity-rank { + color: #94a3b8; font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; + width: 18px; } -.legend-item::before { - content: ''; - width: 8px; - height: 8px; - border-radius: 50%; +.entity-name { + color: #1e293b; + font-weight: 700; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.entity-connections { + color: #6366f1; + font-weight: 800; + font-family: var(--font-mono), monospace; + background: rgba(99, 102, 241, 0.08); + padding: 1px 4px; + border-radius: 4px; } -.legend-item.concept::before { background-color: #3b82f6; } -.legend-item.method::before { background-color: #10b981; } -.legend-item.metric::before { background-color: #ff6b35; } -.legend-item.relationship::before { background-color: #eee; height: 2px; border-radius: 0; } -.legend-item.entity::before { background-color: #f59e0b; } +/* Pulse animation utility */ +@keyframes pulse-light { + 0% { opacity: 1; } + 50% { opacity: 0.4; } + 100% { opacity: 1; } +} + +.pulse { + animation: pulse-light 1.5s infinite ease-in-out; +} + +/* ── Holographic Scene Tooltips (Hover Outlined Style) ── */ +.graph-tooltip { + background: transparent !important; + border: none !important; + box-shadow: none !important; + font-family: 'Inter', sans-serif !important; + font-weight: 800 !important; + text-transform: uppercase !important; + pointer-events: none !important; + text-shadow: + -3px -3px 0 #fff, + 3px -3px 0 #fff, + -3px 3px 0 #fff, + 3px 3px 0 #fff, + -3px 0px 0 #fff, + 3px 0px 0 #fff, + 0px -3px 0 #fff, + 0px 3px 0 #fff !important; + padding: 0 !important; +} + +.graph-tooltip strong { + font-weight: 800 !important; +} diff --git a/frontend/src/components/KnowledgeGraph.jsx b/frontend/src/components/KnowledgeGraph.jsx index f0725c55cda547dbd4e4de7d9b0f74254fa4712e..bed64cd1aa52d4c147b94b5bb7d53393feaa30e8 100644 --- a/frontend/src/components/KnowledgeGraph.jsx +++ b/frontend/src/components/KnowledgeGraph.jsx @@ -1,115 +1,371 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; +import ForceGraph3D from 'react-force-graph-3d'; +import * as THREE from 'three'; import { apiFetch } from '../api'; import './KnowledgeGraph.css'; -const FALLBACK_GRAPH = { - center: 'Transformer Models', +// Stunning conceptual mock graph representing SOMA's cognitive architecture +const MOCK_GRAPH = { nodes: [ - { label: 'Attention Mechanism', tone: 'orange' }, - { label: 'Long-range Dependencies', tone: 'orange' }, - { label: 'Efficiency', tone: 'green' }, - { label: 'Expressivity', tone: 'orange' }, - { label: 'Sparse Patterns', tone: 'orange' }, - { label: 'Recurrence', tone: 'orange' } + { id: 'SOMA', label: 'SOMA (Core)', connections: 10, type: 'core' }, + { id: 'Cortex', label: 'Cortex Layer', connections: 8, type: 'core' }, + { id: 'Thalamus', label: 'Thalamus (Routing)', connections: 6, type: 'entity' }, + { id: 'Hippocampus', label: 'Hippocampus', connections: 7, type: 'entity' }, + { id: 'Neocortex', label: 'Neocortex', connections: 6, type: 'entity' }, + { id: 'Sensory Cortex', label: 'Sensory Cortex', connections: 4, type: 'concept' }, + { id: 'Working Memory', label: 'Working Memory', connections: 5, type: 'concept' }, + { id: 'Episodic Memory', label: 'Episodic Memory', connections: 4, type: 'concept' }, + { id: 'Sleep Cycle', label: 'Sleep Cycle', connections: 3, type: 'method' }, + { id: 'Neural Inscription', label: 'Inscription Layer', connections: 3, type: 'method' }, + { id: 'Llama 3.1', label: 'Llama 3.1', connections: 3, type: 'metric' }, + { id: 'Groq API', label: 'Groq API', connections: 2, type: 'metric' } + ], + links: [ + { source: 'SOMA', target: 'Cortex', label: 'ORCHESTRATES' }, + { source: 'SOMA', target: 'Neural Inscription', label: 'ACCEPTS' }, + { source: 'Cortex', target: 'Thalamus', label: 'ROUTES_BY' }, + { source: 'Cortex', target: 'Hippocampus', label: 'CONSOLIDATES' }, + { source: 'Cortex', target: 'Neocortex', label: 'STORES_IN' }, + { source: 'Sensory Cortex', target: 'Hippocampus', label: 'WRITES_TO' }, + { source: 'Working Memory', target: 'Thalamus', label: 'SYNCS_WITH' }, + { source: 'Episodic Memory', target: 'Hippocampus', label: 'LOGS_IN' }, + { source: 'Sleep Cycle', target: 'Hippocampus', label: 'OPTIMIZES' }, + { source: 'Sleep Cycle', target: 'Neocortex', label: 'REINFORCES' }, + { source: 'Cortex', target: 'Llama 3.1', label: 'COMPUTES' }, + { source: 'Llama 3.1', target: 'Groq API', label: 'HOSTED_ON' } ] }; -const POSITIONS = [ - { x: 50, y: 15 }, // Top - { x: 80, y: 35 }, // Top Right - { x: 80, y: 65 }, // Bottom Right - { x: 50, y: 85 }, // Bottom - { x: 20, y: 65 }, // Bottom Left - { x: 20, y: 35 }, // Top Left -]; - function KnowledgeGraph({ refreshTick }) { - const [graph, setGraph] = useState(FALLBACK_GRAPH); + const [graphData, setGraphData] = useState({ nodes: [], links: [] }); const [loading, setLoading] = useState(true); + const [dbStatus, setDbStatus] = useState('connecting'); + const [dimensions, setDimensions] = useState({ width: 800, height: 500 }); + const [stats, setStats] = useState({ node_count: 0, edge_count: 0, top_entities: [] }); + const [physicsActive, setPhysicsActive] = useState(true); + + const containerRef = useRef(null); + const fgRef = useRef(); + // Measure container dimensions to auto-resize the canvas useEffect(() => { - const fetchGraph = async () => { - setLoading(true); - try { - const res = await apiFetch('/api/v1/graph'); - if (!res.ok) { setLoading(false); return; } + if (!containerRef.current) return; + const resizeObserver = new ResizeObserver((entries) => { + for (let entry of entries) { + const { width, height } = entry.contentRect; + setDimensions({ width: width || 800, height: height || 500 }); + } + }); + resizeObserver.observe(containerRef.current); + return () => resizeObserver.disconnect(); + }, []); + + // Fetch graph database from backend + const fetchGraph = async () => { + setLoading(true); + try { + const res = await apiFetch('/api/v1/graph'); + if (!res.ok) { + throw new Error('Graph fetch returned unhealthy status'); + } + + const data = await res.json(); + + if (data.status === 'offline' || data.status === 'error' || !data.nodes || data.nodes.length === 0) { + setDbStatus(data.status || 'offline'); + const nodes = MOCK_GRAPH.nodes.map(n => ({ ...n })); + const links = MOCK_GRAPH.links.map(l => ({ ...l })); + setGraphData({ nodes, links }); + } else { + setDbStatus('online'); + const nodes = data.nodes.map(n => ({ + id: n.id, + label: n.label || n.id, + connections: n.connections || 1, + type: 'entity' + })); - const data = await res.json(); - const labels = Array.from(new Set((data.nodes || []).map(n => n.label || n.id).filter(Boolean))); + const links = data.edges.map(e => ({ + source: e.source, + target: e.target, + label: e.label || 'RELATED_TO' + })); - if (labels.length > 0) { - const [center, ...rest] = labels; - const nodes = rest.slice(0, 6).map(l => ({ - label: l, - tone: l.toLowerCase().includes('efficiency') ? 'green' : 'orange' - })); - while (nodes.length < 6) { - nodes.push(FALLBACK_GRAPH.nodes[nodes.length]); - } - setGraph({ center, nodes }); + setGraphData({ nodes, links }); + } + } catch (error) { + console.error('Graph fetch failed', error); + setDbStatus('offline'); + setGraphData({ + nodes: MOCK_GRAPH.nodes.map(n => ({ ...n })), + links: MOCK_GRAPH.links.map(l => ({ ...l })) + }); + } finally { + setLoading(false); + } + }; + + const fetchStats = async () => { + try { + const res = await apiFetch('/api/v1/graph/stats'); + if (res.ok) { + const data = await res.json(); + if (data.status === 'online') { + setStats(data); } - } catch (error) { - console.error('Graph fetch failed', error); - } finally { - setLoading(false); } - }; + } catch (err) { + console.error('Stats fetch failed', err); + } + }; + useEffect(() => { fetchGraph(); + fetchStats(); }, [refreshTick]); + // Physics force configurations to pull the neural cluster into a tight, brain-like shape + useEffect(() => { + if (fgRef.current) { + const d3Force = fgRef.current.d3Force; + if (d3Force) { + d3Force('charge').strength(-50); // Gentle repulsion for an ultra-compact molecular look + d3Force('link').distance(30); // Extremely tight connection paths to pull nodes together + } + + // Auto-fit camera with very tight padding to zoom in close and eliminate empty workspace + setTimeout(() => { + fgRef.current.zoomToFit(800, 20); + }, 600); + } + }, [graphData]); + + // Calculate max connections dynamically to scale color thresholds + const maxConnections = graphData.nodes && graphData.nodes.length > 0 + ? Math.max(...graphData.nodes.map(n => n.connections || 1), 1) + : 1; + + // Color mapper based on node type & degree + const getNodeColor = (node) => { + if (node.id === 'SOMA') return '#6366f1'; // Core Hub (Deep Indigo) is always SOMA! + + // For offline/mock mode, retain the pre-assigned structural category colors + if (dbStatus !== 'online') { + if (node.type === 'core') return '#6366f1'; + if (node.type === 'method') return '#10b981'; // Green + if (node.type === 'concept') return '#3b82f6'; // Blue + if (node.type === 'metric') return '#f59e0b'; // Amber + return '#0891b2'; // Cyan + } + + // For live Neo4j data, dynamically scale colors based on relative synaptic density! + const connections = node.connections || 1; + const ratio = connections / maxConnections; + + if (ratio >= 0.8) return '#ec4899'; // High centrality (Hot pink) + if (ratio >= 0.4) return '#f59e0b'; // Medium centrality (Orange) + return '#0891b2'; // Low centrality (Cyan) + }; + return (
- +
+ + {dbStatus !== 'online' && ( + + + Neo4j Offline - Displaying Interactive System Concept + + )} + {dbStatus === 'online' && ( + + + Neo4j Synchronized (Live) + + )} +
+
- - + + + + +
-
- - {POSITIONS.map((pos, i) => ( - - ))} - {POSITIONS.map((pos, i) => { - const next = POSITIONS[(i + 1) % POSITIONS.length]; - return ; - })} - - -
-
- {graph.center} +
+ {/* Live Telemetry HUD Overlay */} +
+
+
+ analytics + Semantic Telemetry +
+
+ Nodes: + {dbStatus === 'online' ? stats.node_count : graphData.nodes.length} +
+
+ Synapses: + {dbStatus === 'online' ? stats.edge_count : graphData.links.length} +
+
+ Storage: + {dbStatus === 'online' ? 'LTM (Neo4j)' : 'STM (Cache)'} +
+
+ + {dbStatus === 'online' && stats.top_entities && stats.top_entities.length > 0 && ( +
+
+ star + Primary Hubs +
+
+ {stats.top_entities.map((ent, idx) => ( +
+ #{idx+1} + {ent.entity} + {ent.connections} rx +
+ ))} +
+
+ )}
- {graph.nodes.map((node, i) => { - const pos = POSITIONS[i]; - return ( -
-
- {node.label} -
- ); - })} + { + const size = Math.max(3.2, Math.min(node.connections * 1.6, 9.5)); // Perfectly scaled spheres + const geom = new THREE.SphereGeometry(size, 24, 24); + const mat = new THREE.MeshLambertMaterial({ + color: getNodeColor(node), + transparent: true, + opacity: 0.95, + emissive: getNodeColor(node), + emissiveIntensity: 0.45 + }); + return new THREE.Mesh(geom, mat); + }} + + // Outlined holographic label rendered natively floating beside cursor on hover (100% stable!) + nodeLabel={node => ` + + ${node.label || node.id} + + `} + + // Outlined relationship tag rendered on link hover (100% stable!) + linkLabel={link => ` + + ${link.label || 'RELATED_TO'} + + `} + + nodeRelSize={3} + linkColor={() => 'rgba(99, 102, 241, 0.25)'} // Soft, clean axon link fibers + linkWidth={1.8} // Sleek link line thickness + + // Glowing Thought Flows (sliding directional particles along axons) + linkDirectionalParticles={3} + linkDirectionalParticleSpeed={0.006} + linkDirectionalParticleWidth={1.5} + linkDirectionalParticleColor={() => '#6366f1'} + + showNavInfo={false} + enablePointerInteraction={true} + enableNodeDrag={true} + /> - {loading &&
Refreshing graph...
} -
+ {/* Beautiful Glassmorphic Legend HUD */} +
+
Core Hub
+
High Density
+
Medium Density
+
Low Density
+
-
- Concept - Method - Metric - Relationship - Entity + {loading && ( +
+ refresh + Synchronizing cognitive mesh... +
+ )}
); diff --git a/frontend/src/components/KnowledgeInput.css b/frontend/src/components/KnowledgeInput.css index 61c14c1172a49938fce1dcc5db171ffacd796e03..f3d62d993230b86355a776a3523ae00ddd2dadb4 100644 --- a/frontend/src/components/KnowledgeInput.css +++ b/frontend/src/components/KnowledgeInput.css @@ -1,115 +1,382 @@ -/* KnowledgeInput.css - Refined for Page 6 */ +/* KnowledgeInput.css - Neural Inscription Workspace */ -.knowledge-layout { +.inscription-grid { + display: grid; + grid-template-columns: 1fr 340px; + gap: 32px; + flex: 1; + min-height: 0; + padding: 0 40px 40px 40px; + overflow: hidden; +} + +.inscription-main { display: flex; flex-direction: column; + gap: 24px; + overflow-y: auto; + padding-right: 8px; +} + +.inscription-header { + display: flex; align-items: center; - justify-content: center; - text-align: center; - height: 100%; + gap: 20px; } -.knowledge-icon-large { - width: 88px; - height: 88px; - background-color: #eee; - border-radius: 24px; +.inscription-icon { + width: 56px; + height: 56px; + background: white; + border: 1px solid rgba(0,0,0,0.05); + border-radius: 16px; display: flex; align-items: center; justify-content: center; - margin-bottom: 32px; + box-shadow: 0 4px 15px rgba(0,0,0,0.02); } -.knowledge-icon-large .material-icons { - font-size: 44px; - color: #1a1a1a; +.inscription-icon .material-icons { + color: #ff6b35; + font-size: 24px; } -.knowledge-layout h3 { +.inscription-title h3 { font-family: var(--font-display); - font-size: 2rem; - font-weight: 700; - margin-bottom: 12px; + font-size: 1.2rem; + font-weight: 800; color: #1a1a1a; + margin-bottom: 2px; } -.knowledge-layout p { - font-size: 1.1rem; - color: #888; - margin-bottom: 48px; - max-width: 500px; +.inscription-title p { + font-size: 0.75rem; + color: #999; + font-weight: 500; } -.knowledge-composer { - width: 100%; - max-width: 760px; - background-color: white; - border-radius: 32px; - padding: 40px; - box-shadow: 0 10px 40px rgba(0,0,0,0.03); - border: 1px solid rgba(0,0,0,0.02); +.inscription-composer { + flex: 1; + background: rgba(255, 255, 255, 0.6); + backdrop-filter: blur(20px); + border-radius: 24px; + border: 1px solid rgba(255, 255, 255, 0.8); + padding: 32px; + display: flex; + flex-direction: column; + box-shadow: 0 20px 50px rgba(0,0,0,0.04); + max-height: 600px; } -.knowledge-composer textarea { +.inscription-composer textarea { + flex: 1; width: 100%; - min-height: 280px; background: transparent; border: none; outline: none; - font-size: 1.15rem; + font-size: 0.85rem; + line-height: 1.6; color: #1a1a1a; resize: none; - line-height: 1.6; font-weight: 500; } -.knowledge-composer textarea::placeholder { - color: #ccc; -} - -.knowledge-composer-footer { +.inscription-footer { + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid #f8f8f8; display: flex; justify-content: space-between; align-items: center; - margin-top: 32px; - padding-top: 24px; - border-top: 1px solid #f0f0f0; +} + +.inscription-stats { + display: flex; + flex-direction: column; + gap: 4px; } .char-count { - font-size: 0.85rem; + font-size: 0.65rem; + font-weight: 800; color: #bbb; - font-weight: 600; text-transform: uppercase; - letter-spacing: 0.05em; + letter-spacing: 0.1em; } -.add-memory-btn { - background-color: #ff6b35; +.busy-tag { + font-size: 0.65rem; + font-weight: 800; + color: #ff6b35; + text-transform: uppercase; +} + +.inscription-btn { + background: #ff6b35; color: white; - padding: 16px 36px; - border-radius: 18px; + padding: 12px 28px; + border-radius: 14px; font-weight: 700; - font-size: 1rem; - transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); - box-shadow: 0 10px 25px rgba(255, 107, 53, 0.2); + font-size: 0.85rem; + display: flex; + align-items: center; + gap: 8px; + transition: all 0.2s; + box-shadow: 0 8px 20px rgba(255, 107, 53, 0.2); } -.add-memory-btn:hover:not(:disabled) { +.inscription-btn:hover:not(:disabled) { transform: translateY(-2px); - box-shadow: 0 14px 30px rgba(255, 107, 53, 0.3); + box-shadow: 0 12px 25px rgba(255, 107, 53, 0.3); } -.add-memory-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - filter: grayscale(0.5); +.inscription-status { + display: flex; + align-items: center; + gap: 12px; + background: linear-gradient(135deg, rgba(16, 185, 129, 0.1), rgba(16, 185, 129, 0.05)); + color: #059669; + padding: 16px 24px; + border-radius: 16px; + font-size: 0.85rem; + font-weight: 700; + border: 1px solid rgba(16, 185, 129, 0.2); + box-shadow: 0 4px 20px rgba(16, 185, 129, 0.1); } -.knowledge-status-text { - margin-top: 32px; - font-size: 0.95rem; - color: #ff6b35; +/* ── Analysis Sidebar ── */ +.inscription-sidebar { + display: flex; + flex-direction: column; + overflow-y: auto; + padding-right: 4px; +} + +.inscription-sidebar::-webkit-scrollbar { + width: 4px; +} +.inscription-sidebar::-webkit-scrollbar-track { + background: transparent; +} +.inscription-sidebar::-webkit-scrollbar-thumb { + background: rgba(0,0,0,0.1); + border-radius: 10px; +} + +.analysis-panel { + background: rgba(255, 255, 255, 0.4); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.5); + border-radius: 24px; + padding: 24px; + display: flex; + flex-direction: column; + gap: 24px; + height: 100%; +} + +.panel-label { + font-size: 0.65rem; + font-weight: 800; + color: #aaa; + text-transform: uppercase; + letter-spacing: 0.1em; + display: flex; + align-items: center; + gap: 8px; +} + +.analysis-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 32px; +} + +.analysis-metric { + display: flex; + flex-direction: column; +} + +.analysis-section label, +.analysis-metric label { + font-size: 0.65rem; + font-weight: 800; + color: #999; + text-transform: uppercase; + display: block; + margin-bottom: 8px; + letter-spacing: 0.05em; +} + +.mini-meter { + height: 6px; + background: rgba(0,0,0,0.05); + border-radius: 10px; + margin-top: 8px; + overflow: hidden; +} + +.mini-fill { + height: 100%; + background: #ff6b35; + transition: width 0.3s; +} + +.link-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.link-tag { + background: white; + padding: 6px 12px; + border-radius: 100px; + font-size: 0.7rem; + font-weight: 700; + color: #555; + border: 1px solid rgba(0,0,0,0.03); + display: flex; + align-items: center; + gap: 6px; +} + +.link-tag .dot { + width: 4px; + height: 4px; + border-radius: 50%; + background: #ff6b35; +} + +.empty-tag { + font-size: 0.7rem; + color: #ccc; + font-style: italic; +} + +.layer-item { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; + font-size: 0.75rem; + font-weight: 600; + color: #666; +} + +.layer-item .dot { + width: 6px; + height: 6px; + border-radius: 50%; +} + +.metric-sub { + display: flex; + justify-content: space-between; + margin-top: 6px; + font-size: 0.6rem; + font-weight: 700; + color: #999; + text-transform: uppercase; +} + +.impact-stats { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.impact-item { + background: white; + padding: 12px; + border-radius: 12px; + border: 1px solid rgba(0,0,0,0.03); + display: flex; + flex-direction: column; + gap: 4px; +} + +.impact-val { + font-size: 1.1rem; + font-weight: 800; + color: #1a1a1a; + font-family: var(--font-display); +} + +.impact-label { + font-size: 0.55rem; + font-weight: 700; + color: #aaa; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.link-tag.reinforced { + border-color: rgba(16, 185, 129, 0.3); + background: rgba(16, 185, 129, 0.05); +} + +.reinforced-icon { + font-size: 12px !important; + color: #10b981; + margin-left: 2px; +} + +.existing-links-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.existing-link-item { + display: flex; + align-items: center; + gap: 12px; + background: white; + padding: 10px; + border-radius: 12px; + border: 1px solid rgba(0,0,0,0.03); +} + +.existing-link-item .material-icons { + font-size: 18px; + color: #10b981; +} + +.link-info { + display: flex; + flex-direction: column; +} + +.link-info strong { + font-size: 0.75rem; font-weight: 700; - animation: fadeIn 0.4s ease; + color: #1a1a1a; +} + +.link-info span { + font-size: 0.6rem; + color: #999; +} + +.panel-footer { + font-size: 0.65rem; + color: #bbb; + display: flex; + align-items: center; + gap: 8px; + padding-top: 20px; + border-top: 1px solid rgba(0,0,0,0.05); + margin-top: auto; +} + +@keyframes pulse-blue { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +.busy-tag.pulse { + animation: pulse-blue 1.5s infinite ease-in-out; } diff --git a/frontend/src/components/KnowledgeInput.jsx b/frontend/src/components/KnowledgeInput.jsx index 8014e38fc55a9b5e37783d2fb21070a9f5f4322b..c986f96a5a978278a805d77c274464735bb21cd3 100644 --- a/frontend/src/components/KnowledgeInput.jsx +++ b/frontend/src/components/KnowledgeInput.jsx @@ -1,46 +1,227 @@ -import { useState } from 'react'; +import { useState, useMemo, useEffect, useCallback } from 'react'; +import { apiFetch } from '../api'; import './KnowledgeInput.css'; function KnowledgeInput({ onKnowledgeSubmit, isBusy, status }) { const [text, setText] = useState(''); + const [analysis, setAnalysis] = useState(null); + const [lastAnalysis, setLastAnalysis] = useState(null); + const [isAnalyzing, setIsAnalyzing] = useState(false); const handleSubmit = () => { if (!text.trim() || isBusy) return; onKnowledgeSubmit(text); + + if (analysis) { + setLastAnalysis(analysis); + } else { + // Fallback if they clicked instantly or text was < 50 chars + const charCount = text.length; + setLastAnalysis({ + metrics: { + density: Math.min(charCount / 2000, 1.0), + chunks: Math.floor(charCount / 500) + 1, + estimated_links: 0, + reinforcement_index: 0 + }, + entities: [], + existing_links: [] + }); + } + setText(''); + setAnalysis(null); }; + // Debounced analysis function + useEffect(() => { + if (text.length < 50) { + setAnalysis(null); + return; + } + + const timer = setTimeout(async () => { + setIsAnalyzing(true); + try { + const res = await apiFetch('/api/v1/analyze', { + method: 'POST', + body: JSON.stringify({ text }) + }); + if (res.ok) { + const data = await res.json(); + setAnalysis(data); + } + } catch (error) { + console.error('Analysis failed', error); + } finally { + setIsAnalyzing(false); + } + }, 1500); // 1.5s debounce + + return () => clearTimeout(timer); + }, [text]); + + const displayAnalysis = analysis || lastAnalysis; + return ( -
-
- note_add -
- -

Add knowledge to Soma

-

Paste text or notes for Soma to remember and integrate into its memory.

- -
-