diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0197991..e22fa86 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+- Multimodal perception pipeline: Basirah (vision) + Nutq (voice) wired into QCA engine
+- Auditory-first processing (Sam' before Basar) per Quran 16:78/17:36
+- Perception-Qalb integration: emotional context modulates vision and voice analysis
+- `POST /api/perception/analyze` endpoint for multimodal analysis
+- WebSocket `multimodal` message type for real-time perception
+- DNA-inspired quaternary encoding module (`backend/memory/quaternary.py`)
+- Quaternary checksum as 4th integrity layer in Lawh al-Mahfuz
+- VectorStore (ChromaDB) integration in Living Memory novelty gate
+- Hybrid text + semantic similarity in Living Memory `_find_best_match()`
+- `search_entities()` method in KnowledgeGraph (fixes MemoryPyramid layer 4)
+- Perception page in frontend UI with image/audio upload
+- Image and audio attachment support in chat interface
+- `BasirahEngine` and `NutqEngine` exports in perception `__init__.py`
+- NutqEngine: language detection (Arabic, Urdu, English) via Unicode analysis
+- NutqEngine: 8+ intent types (question, command, greeting, farewell, confirmation, negation, request, statement)
+
### Fixed
+- BasirahEngine: JSON-structured LLM output parsing (was returning empty extracted_text/key_elements)
+- BasirahEngine: category detection bug (`"error" in raw_lower and "text"` evaluated boolean inside list)
+- NutqEngine: `_adjust_for_tone()` was a no-op, now implements warm/patient/focused text transforms
+- KnowledgeGraph: missing `search_entities()` caused MemoryPyramid layer 4 to silently fail
- Replace deprecated `datetime.utcnow()` with `datetime.now(timezone.utc)` across all modules
- Fix Pydantic `class Config` deprecation in `SkillExecuteRequest` (use `model_config` dict)
- Fix `check_provider_health()` called without `await` in settings endpoint
diff --git a/README.md b/README.md
index 83c3c89..deb863e 100644
--- a/README.md
+++ b/README.md
@@ -102,6 +102,7 @@ make dev # Start backend + frontend
|---------|---------------|
| **Chat** | Talk to your AI in the browser or terminal |
| **Browse the web** | AI can search Google, read websites, extract information |
+| **Analyze images & voice** | Upload images for vision analysis, audio for transcription |
| **Run code** | AI writes and executes Python, bash scripts |
| **Manage files** | Read, write, organize files on your computer |
| **Remember things** | Remembers your conversations and preferences |
@@ -114,8 +115,11 @@ make dev # Start backend + frontend
| Feature | What It Means |
|---------|---------------|
| **QALB-7 Cognitive Pipeline** | 7-layer architecture: ethics → deliberation → emotion → conviction → metacognition |
+| **Multimodal Perception** | Sam' (hearing) + Basar (sight) → Fu'ad integration, with Qalb-aware context |
| **Developmental Stages** | Agents grow from Nutfah (5 tools, 5 turns) to Khalq Akhar (all tools, 25 turns) |
+| **Living Memory** | Novelty gate with hybrid text+vector similarity — never re-stores 1+1=2 |
| **5-Layer Memory Pyramid** | Unified query across episodic, semantic, neural pathways, vectors, and knowledge graph |
+| **DNA Integrity** | Quaternary (ACGT) checksums with Hamming distance verification in Lawh al-Mahfuz |
| **Causal Reasoning** | Pearl's 3-rung causal ladder: observation, intervention, counterfactual |
| **Plugin system** | Add new abilities with a simple Python file |
| **Event bus + Hooks** | Decoupled communication — modify data at any point in the pipeline |
@@ -282,6 +286,9 @@ Each agent processes every task through these cognitive layers:
| Module | Arabic | Purpose | File |
|--------|--------|---------|------|
+| **Multimodal Perception** | سمع+بصر | Sam' (hearing) first, then Basar (sight), Qalb-aware context | `perception/basirah.py`, `perception/nutq.py` |
+| **Living Memory** | ذاكرة حية | Novelty gate + hybrid text/vector similarity + Dhikr daemon | `memory/living_memory.py` |
+| **Quaternary Encoding** | تشفير رباعي | DNA-inspired ACGT checksums with Hamming distance verification | `memory/quaternary.py` |
| **Lawwama Self-Healing** | لوّامة | Immune memory, health metrics, adaptive checkpoint intervals | `core/self_healing.py` |
| **Parallel Agents** | — | Concurrent task scheduling + skill transfer between agents | `core/parallel_agents.py` |
| **Imagination** | تصوير | Predictive coding — simulate outcomes before acting | `core/imagination.py` |
@@ -296,14 +303,19 @@ All memory layers are queried through a unified `MemoryPyramid`:
| Layer | Module | Purpose |
|-------|--------|---------|
+| **Living Memory** | `memory/living_memory.py` | Novelty gate (hybrid text + vector similarity), importance scoring, Dhikr daemon |
| **Dhikr** | `memory/dhikr.py` | Three-tier persistent memory (episodic, semantic, procedural) |
| **Masalik** | `memory/masalik.py` | Neural pathway network with spreading activation |
-| **Lawh al-Mahfuz** | `memory/lawh_mahfuz.py` | Immutable core memory with triple-checksum integrity |
-| **VectorStore** | `memory/vector_store.py` | Semantic embedding search (ChromaDB) |
-| **KnowledgeGraph** | `memory/knowledge_graph.py` | Entity-relationship graph (SQLite) |
+| **VectorStore** | `memory/vector_store.py` | Semantic embedding search (ChromaDB) — also used by Living Memory |
+| **KnowledgeGraph** | `memory/knowledge_graph.py` | Entity-relationship graph with full-text search (SQLite) |
+| **Lawh al-Mahfuz** | `memory/lawh_mahfuz.py` | Immutable memory with 4-layer integrity: SHA-256 + CRC-32 + length + quaternary checksum |
Unified query: `memory/memory_pyramid.py` merges, deduplicates, and ranks results by relevance x certainty x recency.
+**Living Memory** solves the 1+1=2 problem: seeing the same information again doesn't create a new trace — it activates the existing one. New information enriches existing traces, related info gets linked, and only genuinely novel content is stored.
+
+**Quaternary Encoding** (`memory/quaternary.py`) provides DNA-inspired error detection: binary data is encoded using a 4-symbol alphabet (A, C, G, T), chunked into codons (triplets), and verified using XOR parity and Hamming distance.
+
### Developmental Stages (Nafs Levels 1–7)
Agents grow through seven stages, each unlocking new capabilities:
@@ -441,6 +453,45 @@ GET /api/tasks/history Get task history
POST /api/memory/query Search memories
POST /api/memory/store Store a memory
POST /api/memory/consolidate Prune old memories
+GET /api/memory/list List recent memories
+```
+
+### Perception (Sam' + Basar)
+```
+POST /api/perception/analyze Multimodal analysis (text + base64 image + base64 audio)
+```
+
+Accepts `MultimodalInput` with fields: `text`, `image_base64`, `audio_base64`, `media_type`, `qalb_state`.
+Processes Sam' (audio) first, then Basar (image), integrates via Fu'ad.
+
+### Cognitive Pipeline
+```
+POST /api/qalb/analyze Analyze emotional state from text
+GET /api/qalb/trend/{user_id} Get emotional trend over time
+POST /api/cognitive/route Route to best cognitive method
+POST /api/yaqin/tag Tag knowledge with certainty level
+GET /api/yaqin/stats Get Yaqin statistics
+```
+
+### Federation
+```
+GET /api/federation/status Federation network status
+POST /api/federation/discover Discover agents by capability
+POST /api/federation/route Route task to best agent
+```
+
+### Nafs & Ruh
+```
+GET /api/nafs/tiers Get all 7 Nafs tier definitions
+GET /api/nafs/{agent_id} Get agent Nafs level and progress
+GET /api/ruh/{agent_id} Get agent Ruh energy level
+```
+
+### Knowledge
+```
+POST /api/knowledge/ingest Ingest from URL or YouTube
+POST /api/knowledge/upload Upload PDF for knowledge extraction
+GET /api/knowledge/sources List ingested knowledge sources
```
### Plugins & Extensibility
@@ -490,6 +541,10 @@ POST /api/shura Multi-agent consultation
WS /ws/{client_id} WebSocket connection
```
+**WebSocket message types**: `chat`, `task`, `command`, `multimodal`, `ping`
+
+The `multimodal` type accepts: `{ type: "multimodal", content: "text", image_base64: "...", audio_base64: "...", media_type: "image/png", qalb_state: "neutral" }` and returns a `perception_result` message with full QCA analysis.
+
### Channels
```
POST /api/channels/{name}/start Start a channel adapter
diff --git a/docs/index.html b/docs/index.html
index 5ca01cd..7b2d744 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1148,6 +1148,9 @@
Extension Modules
Module Arabic Purpose File
+ Multimodal Perception سمع+بصر Sam' (hearing) first, then Basar (sight), Qalb-aware context modulation perception/basirah.py, perception/nutq.py
+ Living Memory ذاكرة حية Novelty gate + hybrid text/vector similarity + Dhikr maintenance daemon memory/living_memory.py
+ Quaternary Encoding تشفير رباعي DNA-inspired ACGT checksums with codon chunking and Hamming verification memory/quaternary.py
Self-Healing لوّامة Immune memory, health metrics, adaptive checkpoints core/self_healing.py
Parallel Agents — Concurrent task scheduling + skill transfer core/parallel_agents.py
Imagination تصوير Predictive coding — simulate before acting core/imagination.py
@@ -1157,19 +1160,21 @@ Extension Modules
- 5-Layer Memory Pyramid
+ Memory Architecture (6-Layer Pyramid)
All memory layers are queried through a unified MemoryPyramid:
Layer Module Purpose
+ Living Memory memory/living_memory.pyNovelty gate with hybrid text + vector similarity, importance scoring, Dhikr daemon
Dhikr memory/dhikr.pyThree-tier persistent memory (episodic, semantic, procedural)
Masalik memory/masalik.pyNeural pathways with spreading activation
- Lawh al-Mahfuz memory/lawh_mahfuz.pyImmutable memory with triple-checksum integrity (SHA-256 + CRC-32 + length)
- VectorStore memory/vector_store.pySemantic embedding search (ChromaDB)
- KnowledgeGraph memory/knowledge_graph.pyEntity-relationship graph (SQLite)
+ VectorStore memory/vector_store.pySemantic embedding search (ChromaDB) — also used by Living Memory
+ KnowledgeGraph memory/knowledge_graph.pyEntity-relationship graph with full-text entity search (SQLite)
+ Lawh al-Mahfuz memory/lawh_mahfuz.pyImmutable memory with 4-layer integrity: SHA-256 + CRC-32 + length + quaternary checksum
Unified query: memory/memory_pyramid.py merges, deduplicates, and ranks by relevance × certainty × recency.
+ Quaternary Encoding: DNA-inspired error detection using a 4-symbol alphabet (A, C, G, T). Data is encoded as quaternary strings, chunked into codons (triplets mapping to 64 semantic categories), and verified using XOR parity and Hamming distance checks.
Yaqin Certainty Engine
Every piece of knowledge is tagged with its certainty level:
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index b850703..f12b1af 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -21,6 +21,7 @@ import type {
Memory,
Integration,
SystemStatus,
+ PerceptionResult,
} from "./types";
import { config } from "./config";
import { useApi } from "./hooks/useApi";
@@ -133,6 +134,7 @@ class ErrorBoundary extends Component {
}
const ChannelsPage = lazy(() => import("./pages/ChannelsPage"));
+const PerceptionPage = lazy(() => import("./pages/PerceptionPage"));
const SkillsPage = lazy(() => import("./pages/SkillsPage"));
const SecurityPage = lazy(() => import("./pages/SecurityPage"));
const AutomationPage = lazy(() => import("./pages/AutomationPage"));
@@ -145,6 +147,19 @@ const DeveloperPage = lazy(() => import("./pages/DeveloperPage"));
const WelcomePage = lazy(() => import("./pages/WelcomePage"));
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
+function fileToBase64(file: File): Promise {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ const result = reader.result as string;
+ const base64 = result.split(",")[1] || result;
+ resolve(base64);
+ };
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+}
+
// ===== MAIN APP INNER =====
function AppInner() {
const { addToast } = useToast();
@@ -220,6 +235,7 @@ function AppInner() {
}[]
>([]);
const [showSessionHistory, setShowSessionHistory] = useState(false);
+ const [attachedFiles, setAttachedFiles] = useState([]);
const CHAT_COMMANDS = [
{ name: "/help", description: "Show available commands" },
@@ -414,6 +430,26 @@ function AppInner() {
addTerminalLine("Task completed", "gold");
loadAgents();
break;
+ case "perception_result":
+ setStreaming(false);
+ setStreamingText("");
+ setTypingIndicator(false);
+ setToolStatus("");
+ setMessages((prev) => [
+ ...prev,
+ {
+ id: Date.now(),
+ role: "assistant" as const,
+ content:
+ (data.result as PerceptionResult)?.batin ||
+ "Perception analysis complete",
+ agent: "Basirah",
+ ts: new Date().toLocaleTimeString(),
+ perception: data.result as PerceptionResult,
+ },
+ ]);
+ addTerminalLine("Perception analysis complete", "gold");
+ break;
case "agent_created":
addTerminalLine(
`Agent created: ${(data.agent as Record).name}`,
@@ -677,12 +713,21 @@ function AppInner() {
}, []);
const sendMessage = async () => {
- if (!input.trim() || streaming) return;
+ if ((!input.trim() && attachedFiles.length === 0) || streaming) return;
const content = input;
+ const files = [...attachedFiles];
+ const hasMedia = files.some(
+ (f) => f.type.startsWith("image/") || f.type.startsWith("audio/"),
+ );
+
const userMsg: ChatMessage = {
id: Date.now(),
role: "user",
- content,
+ content:
+ content +
+ (files.length > 0
+ ? ` [${files.map((f) => f.name).join(", ")}]`
+ : ""),
ts: new Date().toLocaleTimeString(),
};
setMessages((prev) => [...prev, userMsg]);
@@ -691,12 +736,40 @@ function AppInner() {
setTypingIndicator(true);
setToolStatus("");
setInput("");
+ setAttachedFiles([]);
// Reset textarea height after clearing
if (chatTextareaRef.current) {
chatTextareaRef.current.style.height = "auto";
}
addTerminalLine(`> ${content.substring(0, 60)}...`, "info");
+ // If media files attached, send as multimodal via WebSocket
+ if (hasMedia && ws) {
+ try {
+ const payload: Record = {
+ type: "multimodal",
+ text: content,
+ agent_id: selectedAgent?.id,
+ session_id: sessionId,
+ };
+ for (const file of files) {
+ const b64 = await fileToBase64(file);
+ if (file.type.startsWith("image/")) {
+ payload.image_base64 = b64;
+ payload.media_type = file.type;
+ } else if (file.type.startsWith("audio/")) {
+ payload.audio_base64 = b64;
+ }
+ }
+ ws.send(JSON.stringify(payload));
+ } catch {
+ setStreaming(false);
+ setTypingIndicator(false);
+ addTerminalLine("Failed to process media files", "error");
+ }
+ return;
+ }
+
// Prefer HTTP POST /api/chat (returns message_id, streams via WebSocket)
// Fall back to WebSocket direct send if HTTP fails
try {
@@ -865,6 +938,12 @@ function AppInner() {
{
label: "Tools",
items: [
+ {
+ id: "perception",
+ label: "Perception",
+ desc: "Vision & voice analysis",
+ icon: ,
+ },
{
id: "memory",
label: "Memory",
@@ -1335,22 +1414,62 @@ function AppInner() {
{/* Input box */}
+ {/* Attached files preview */}
+ {attachedFiles.length > 0 && (
+
+ {attachedFiles.map((file, idx) => (
+
+ {file.type.startsWith("image/") ? (
+
+
+
+ ) : (
+
+
+
+
+ )}
+
+ {file.name}
+
+
+ setAttachedFiles((prev) =>
+ prev.filter((_, i) => i !== idx),
+ )
+ }
+ >
+
+
+
+
+
+ ))}
+
+ )}
+
{/* Left action buttons */}
{/* Attach / Upload button */}
{
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.multiple = true;
- fileInput.accept = '*/*';
+ fileInput.accept = 'image/*,audio/*';
fileInput.onchange = (e) => {
const files = (e.target as HTMLInputElement).files;
if (files && files.length > 0) {
- const names = Array.from(files).map(f => f.name).join(', ');
- setInput((prev) => prev + (prev ? ' ' : '') + `[Attached: ${names}]`);
+ setAttachedFiles((prev) => [
+ ...prev,
+ ...Array.from(files),
+ ]);
}
};
fileInput.click();
@@ -1448,7 +1567,7 @@ function AppInner() {
@@ -1906,6 +2025,8 @@ function AppInner() {
);
+ case "perception":
+ return
;
case "skills":
return
;
case "security":
diff --git a/frontend/src/components/ChatMessage.tsx b/frontend/src/components/ChatMessage.tsx
index e1baf57..edddf8c 100644
--- a/frontend/src/components/ChatMessage.tsx
+++ b/frontend/src/components/ChatMessage.tsx
@@ -2,6 +2,7 @@ import { useState, useMemo } from "react";
import type {
ChatMessage as ChatMessageType,
CognitiveMetadata,
+ PerceptionResult,
} from "../types";
import { Markdown } from "./Markdown";
@@ -391,6 +392,133 @@ function CognitiveBar({ cognitive }: { cognitive: CognitiveMetadata }) {
);
}
+const PERCEPTION_CATEGORY_COLORS: Record
= {
+ text: "bg-blue-100 dark:bg-blue-500/15 text-blue-700 dark:text-blue-400",
+ diagram: "bg-purple-100 dark:bg-purple-500/15 text-purple-700 dark:text-purple-400",
+ screenshot: "bg-cyan-100 dark:bg-cyan-500/15 text-cyan-700 dark:text-cyan-400",
+ photo: "bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400",
+ document: "bg-amber-100 dark:bg-amber-500/15 text-amber-700 dark:text-amber-400",
+};
+
+function PerceptionCard({ perception }: { perception: PerceptionResult }) {
+ const [expanded, setExpanded] = useState(false);
+ const basirah = perception.perception?.basirah;
+ const nutq = perception.perception?.nutq;
+
+ return (
+
+
setExpanded((prev) => !prev)}
+ className="flex flex-wrap items-center gap-1.5 cursor-pointer group"
+ aria-expanded={expanded}
+ aria-label="Perception results"
+ >
+ {/* Vision pill */}
+ {basirah && (
+
+
+
+
+
+ {basirah.category}
+
+ )}
+
+ {/* Audio pill */}
+ {nutq && (
+
+
+
+
+
+ {nutq.intent}
+
+ )}
+
+ {/* Key terms pills */}
+ {perception.key_terms?.slice(0, 3).map((term, i) => (
+
+ {term}
+
+ ))}
+
+
+
+
+
+
+ {expanded && (
+
+ {basirah && (
+
+
Vision (Basirah)
+
{basirah.description}
+
+
Confidence
+
+
{(basirah.confidence * 100).toFixed(0)}%
+
+ {basirah.extracted_text && (
+
+ Text:
+ {basirah.extracted_text.substring(0, 200)}
+ {basirah.extracted_text.length > 200 && "..."}
+
+ )}
+ {basirah.key_elements?.length > 0 && (
+
+ {basirah.key_elements.map((el, i) => (
+
+ {el}
+
+ ))}
+
+ )}
+
+ )}
+ {nutq && (
+
+
Voice (Nutq)
+
{nutq.text}
+
+ Intent: {nutq.intent}
+ {" · "}
+ Lang: {nutq.language}
+
+
+ )}
+ {perception.zahir && (
+
+ Zahir: {perception.zahir}
+
+ )}
+ {perception.batin && (
+
+ Batin: {perception.batin}
+
+ )}
+
+ )}
+
+ );
+}
+
interface ChatMessageBubbleProps {
msg: ChatMessageType;
selectedAgent?: { name: string } | null;
@@ -476,6 +604,9 @@ export function ChatMessageBubble({
{/* Cognitive bar */}
{msg.cognitive && }
+ {/* Perception card */}
+ {msg.perception && }
+
{/* Action buttons — hover reveal */}
),
+ Eye: () => (
+
+
+
+
+ ),
Sun: () =>
,
Moon: () =>
,
Monitor: () =>
,
diff --git a/frontend/src/pages/PerceptionPage.tsx b/frontend/src/pages/PerceptionPage.tsx
new file mode 100644
index 0000000..9dc0948
--- /dev/null
+++ b/frontend/src/pages/PerceptionPage.tsx
@@ -0,0 +1,508 @@
+/**
+ * Perception Page — Multimodal Analysis (Basirah + Nutq)
+ * Upload images and audio for vision and voice analysis through the QCA pipeline.
+ */
+
+import { useState, useCallback } from "react";
+import type { PageProps, PerceptionResult } from "../types";
+
+const QALB_STATES = [
+ { value: "", label: "Auto-detect" },
+ { value: "neutral", label: "Neutral" },
+ { value: "positive", label: "Positive" },
+ { value: "frustrated", label: "Frustrated" },
+ { value: "anxious", label: "Anxious" },
+ { value: "confused", label: "Confused" },
+ { value: "determined", label: "Determined" },
+];
+
+const CATEGORY_COLORS: Record
= {
+ text: "bg-blue-100 dark:bg-blue-500/15 text-blue-700 dark:text-blue-400",
+ diagram:
+ "bg-purple-100 dark:bg-purple-500/15 text-purple-700 dark:text-purple-400",
+ screenshot:
+ "bg-cyan-100 dark:bg-cyan-500/15 text-cyan-700 dark:text-cyan-400",
+ photo:
+ "bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400",
+ document:
+ "bg-amber-100 dark:bg-amber-500/15 text-amber-700 dark:text-amber-400",
+};
+
+function fileToBase64(file: File): Promise {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ const result = reader.result as string;
+ // Strip data URL prefix to get raw base64
+ const base64 = result.split(",")[1] || result;
+ resolve(base64);
+ };
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+}
+
+export default function PerceptionPage({ api }: PageProps) {
+ const [text, setText] = useState("");
+ const [imageFile, setImageFile] = useState(null);
+ const [imagePreview, setImagePreview] = useState(null);
+ const [audioFile, setAudioFile] = useState(null);
+ const [qalbState, setQalbState] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [result, setResult] = useState(null);
+ const [error, setError] = useState(null);
+
+ const handleImageSelect = useCallback(
+ (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ setImageFile(file);
+ const url = URL.createObjectURL(file);
+ setImagePreview(url);
+ }
+ },
+ [],
+ );
+
+ const handleAudioSelect = useCallback(
+ (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ setAudioFile(file);
+ }
+ },
+ [],
+ );
+
+ const handleAnalyze = async () => {
+ if (!text && !imageFile && !audioFile) return;
+
+ setLoading(true);
+ setError(null);
+ setResult(null);
+
+ try {
+ const body: Record = { text };
+ if (qalbState) body.qalb_state = qalbState;
+
+ if (imageFile) {
+ body.image_base64 = await fileToBase64(imageFile);
+ body.media_type = imageFile.type || "image/png";
+ }
+ if (audioFile) {
+ body.audio_base64 = await fileToBase64(audioFile);
+ }
+
+ const res = await api.post("/perception/analyze", body);
+ setResult(res as unknown as PerceptionResult);
+ } catch (e) {
+ setError((e as Error).message || "Analysis failed");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const clearAll = () => {
+ setText("");
+ setImageFile(null);
+ setImagePreview(null);
+ setAudioFile(null);
+ setResult(null);
+ setError(null);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
Perception
+
+ Vision (Basirah) & Voice (Nutq) analysis through the QCA pipeline
+
+
+
+ Clear
+
+
+
+
+
+ {/* Input Panel */}
+
+ {/* Text input */}
+
+
+ Text Context
+
+
+
+ {/* Image upload */}
+
+
+ Image (Basirah)
+
+
+ document.getElementById("perception-image-input")?.click()
+ }
+ >
+ {imagePreview ? (
+
+
+
+ {imageFile?.name}
+
+
+ ) : (
+
+
+
+
+
+
+ Click to upload an image
+
+
+ PNG, JPG, WebP
+
+
+ )}
+
+
+
+
+ {/* Audio upload */}
+
+
+ Audio (Nutq)
+
+
+ document.getElementById("perception-audio-input")?.click()
+ }
+ >
+ {audioFile ? (
+
+
+
+
+
+
+
+ {audioFile.name}
+
+
+ ) : (
+
+
+
+
+
+
+ Click to upload audio
+
+
+ MP3, WAV, M4A
+
+
+ )}
+
+
+
+
+ {/* Qalb state + Analyze */}
+
+
setQalbState(e.target.value)}
+ >
+ {QALB_STATES.map((s) => (
+
+ {s.label}
+
+ ))}
+
+
+ {loading ? (
+ <>
+
+
+
+ Analyzing...
+ >
+ ) : (
+ "Analyze"
+ )}
+
+
+
+
+ {/* Results Panel */}
+
+ {error && (
+
+ )}
+
+ {result && (
+ <>
+ {/* Basirah result */}
+ {result.perception?.basirah && (
+
+
+
+ Vision (Basirah)
+
+
+ {result.perception.basirah.category}
+
+
+
+
+ {result.perception.basirah.description}
+
+
+ {/* Confidence bar */}
+
+
+ Confidence
+
+
+
+ {(result.perception.basirah.confidence * 100).toFixed(0)}
+ %
+
+
+
+ {/* Extracted text */}
+ {result.perception.basirah.extracted_text && (
+
+
+ Extracted Text
+
+
+ {result.perception.basirah.extracted_text}
+
+
+ )}
+
+ {/* Key elements */}
+ {result.perception.basirah.key_elements?.length > 0 && (
+
+ {result.perception.basirah.key_elements.map(
+ (el, i) => (
+
+ {el}
+
+ ),
+ )}
+
+ )}
+
+ )}
+
+ {/* Nutq result */}
+ {result.perception?.nutq && (
+
+
+
+ Voice (Nutq)
+
+
+ {result.perception.nutq.intent}
+
+
+ {result.perception.nutq.language}
+
+
+
+ {result.perception.nutq.text}
+
+
+
+ Confidence
+
+
+
+ {(result.perception.nutq.confidence * 100).toFixed(0)}%
+
+
+
+ )}
+
+ {/* QCA Integration results */}
+
+
+ Cognitive Integration
+
+
+ {/* Key terms */}
+ {result.key_terms?.length > 0 && (
+
+
+ Key Terms
+
+
+ {result.key_terms.map((term, i) => (
+
+ {term}
+
+ ))}
+
+
+ )}
+
+ {/* Zahir / Batin */}
+ {result.zahir && (
+
+
+
+ Zahir (Apparent)
+
+
+ {result.zahir}
+
+
+ {result.batin && (
+
+
+ Batin (Hidden)
+
+
+ {result.batin}
+
+
+ )}
+
+ )}
+
+ {/* Roots */}
+ {result.roots_identified &&
+ Object.keys(result.roots_identified).length > 0 && (
+
+
+ Arabic Roots (ISM)
+
+
+ {Object.entries(result.roots_identified).map(
+ ([root, info]) => (
+
+ {root}
+
+ ),
+ )}
+
+
+ )}
+
+ >
+ )}
+
+ {!result && !error && !loading && (
+
+
+
+
+
+
No analysis yet
+
+ Upload an image or audio file, then click Analyze
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index 00c1017..05d8c59 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -188,6 +188,7 @@ export interface ChatMessage {
model?: string;
ts: string;
cognitive?: CognitiveMetadata;
+ perception?: PerceptionResult;
}
// ===== Memory Types =====
@@ -429,6 +430,41 @@ export interface PageProps {
addTerminalLine?: (text: string, type: string) => void;
}
+// ===== Perception Types =====
+
+export interface BasirahInsight {
+ description: string;
+ category: "text" | "diagram" | "screenshot" | "photo" | "document";
+ confidence: number;
+ extracted_text: string;
+ key_elements: string[];
+ processing_time_ms: number;
+}
+
+export interface NutqTranscription {
+ text: string;
+ confidence: number;
+ language: string;
+ intent: string;
+ processing_time_ms: number;
+}
+
+export interface PerceptionResult {
+ perception: {
+ sam: Record;
+ basar: Record;
+ fuad: Record;
+ nutq: NutqTranscription | null;
+ basirah: BasirahInsight | null;
+ };
+ roots_identified: Record;
+ key_terms: string[];
+ zahir: string;
+ batin: string;
+ has_vision?: boolean;
+ has_audio?: boolean;
+}
+
// ===== WebSocket Types =====
export type WsConnectionStatus =