repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
The Living OS cockpit
stars
latest
clone command
git clone gitlawb://did:key:z6Mku78K...XywC/living-os-cockp...git clone gitlawb://did:key:z6Mku78K.../living-os-cockp...59751530feat: surface worker supervisor health in live work5h ago| #1 | 'use client'; |
| #2 | |
| #3 | import { useEffect, useRef, useState } from 'react'; |
| #4 | import type { CSSProperties, RefObject } from 'react'; |
| #5 | import useSWR from 'swr'; |
| #6 | import { ChevronLeft, ChevronRight, Mic, Send } from 'lucide-react'; |
| #7 | import AethonTasks from '@/components/AethonTasks'; |
| #8 | import ApprovalQueuePanel from '@/components/ApprovalQueuePanel'; |
| #9 | import MessageContent from '@/components/MessageContent'; |
| #10 | import VaultGraph3D from '@/components/VaultGraph3D'; |
| #11 | import { isAuthError, readJsonOrThrow, sameOriginFetch, swrFetcher } from '@/lib/client-api'; |
| #12 | |
| #13 | type ChatTurn = { |
| #14 | id: string; |
| #15 | role: 'user' | 'assistant'; |
| #16 | content: string; |
| #17 | at: string; |
| #18 | pending?: boolean; |
| #19 | model?: string; |
| #20 | }; |
| #21 | |
| #22 | type CouncilAgent = { |
| #23 | id: string; |
| #24 | name: string; |
| #25 | role: string; |
| #26 | brain: string; |
| #27 | mandate: string; |
| #28 | }; |
| #29 | |
| #30 | type CouncilMessage = { |
| #31 | id: string; |
| #32 | ts: string; |
| #33 | room: string; |
| #34 | agentId: string; |
| #35 | agentName: string; |
| #36 | role: string; |
| #37 | brain: string; |
| #38 | kind: 'boot' | 'operator' | 'agent' | 'synthesis' | 'guardrail'; |
| #39 | content: string; |
| #40 | gated?: boolean; |
| #41 | checks?: { |
| #42 | pipeline?: boolean; |
| #43 | organs?: boolean; |
| #44 | soul?: boolean; |
| #45 | driftWatch?: boolean; |
| #46 | }; |
| #47 | }; |
| #48 | |
| #49 | type RoomId = 'living' | 'pyra' | 'archivist' | 'oracle' | 'black' | 'reaper' | 'scribe' | 'harbinger' | 'embodied'; |
| #50 | type MotionState = 'idle' | 'thinking' | 'working' | 'speaking'; |
| #51 | type OperatingMode = 'operator' | 'demo'; |
| #52 | |
| #53 | type CharacterState = { |
| #54 | id: RoomId; |
| #55 | seatId: string; |
| #56 | label: string; |
| #57 | mode: string; |
| #58 | role: string; |
| #59 | brain: string; |
| #60 | color: string; |
| #61 | soft: string; |
| #62 | oneLine: string; |
| #63 | assetSrc: string; |
| #64 | glbSeat: string; |
| #65 | chamberName: string; |
| #66 | chamberSubtitle: string; |
| #67 | briefing: string[]; |
| #68 | quickActions: string[]; |
| #69 | }; |
| #70 | |
| #71 | type OpsActivityRow = { |
| #72 | id: string; |
| #73 | actor: string; |
| #74 | message: string; |
| #75 | at?: string; |
| #76 | color?: string; |
| #77 | status?: string; |
| #78 | source?: string; |
| #79 | detail?: unknown; |
| #80 | count?: number; |
| #81 | }; |
| #82 | |
| #83 | const fetcher = swrFetcher; |
| #84 | |
| #85 | const MODELS = [ |
| #86 | { id: 'deepseek-v4-flash', label: 'V4 Flash' }, |
| #87 | { id: 'venice-uncensored', label: 'Venice 1.1 Uncensored' }, |
| #88 | { id: 'kimi-k2-6', label: 'Kimi K2.6' }, |
| #89 | { id: 'openyourmind-qwen35', label: 'OYM Local' }, |
| #90 | { id: 'deepseek-v4-pro', label: 'V4 Pro' }, |
| #91 | { id: 'claude-opus-4-8', label: 'Opus 4.8' }, |
| #92 | { id: 'kimi-k2-5', label: 'Kimi K2.5' }, |
| #93 | ]; |
| #94 | |
| #95 | const CHARACTER_STATES: CharacterState[] = [ |
| #96 | { |
| #97 | id: 'living', |
| #98 | seatId: 'living', |
| #99 | label: 'TheLiving', |
| #100 | mode: 'Converse', |
| #101 | role: 'Daily Assistant', |
| #102 | brain: 'deepseek-v4-flash', |
| #103 | color: '#69aaf8', |
| #104 | soft: 'rgba(105, 170, 248, 0.2)', |
| #105 | oneLine: 'The everyday room: talk, delegate, and watch the active stream.', |
| #106 | assetSrc: '/council2d/LivingEmbodied.png', |
| #107 | glbSeat: 'living', |
| #108 | chamberName: 'THE LIVING', |
| #109 | chamberSubtitle: 'DAILY ASSISTANT CHAMBER', |
| #110 | briefing: ['3 priority tasks', '2 approvals pending', '1 council session ready', '7 new memories archived'], |
| #111 | quickActions: ['Create Task', 'Delegate to Council', 'Analyze Document', 'Schedule Session'], |
| #112 | }, |
| #113 | { |
| #114 | id: 'pyra', |
| #115 | seatId: 'pyra', |
| #116 | label: 'Pyra', |
| #117 | mode: 'Create', |
| #118 | role: 'Scripts, articles, symbols, creative works.', |
| #119 | brain: 'kimi-k2-6', |
| #120 | color: '#a978ff', |
| #121 | soft: 'rgba(169, 120, 255, 0.18)', |
| #122 | oneLine: 'Creative work in development, shaped without burying the operator in machinery.', |
| #123 | assetSrc: '/council2d/LivingPyra.png', |
| #124 | glbSeat: 'pyra', |
| #125 | chamberName: 'PYRA', |
| #126 | chamberSubtitle: 'CREATIVE STUDIO', |
| #127 | briefing: ['Storyboard queue open', 'Public language ready for shaping', 'Creative drafts gated before release'], |
| #128 | quickActions: ['Write Script', 'Draft Article', 'Shape Symbol', 'Build Campaign'], |
| #129 | }, |
| #130 | { |
| #131 | id: 'archivist', |
| #132 | seatId: 'archivist', |
| #133 | label: 'Archivist', |
| #134 | mode: 'Remember', |
| #135 | role: 'Vault continuity and recall.', |
| #136 | brain: 'openyourmind-qwen35', |
| #137 | color: '#dbe9ff', |
| #138 | soft: 'rgba(219, 233, 255, 0.16)', |
| #139 | oneLine: 'The memory room: vault graph, recent conversations, and grounded recall.', |
| #140 | assetSrc: '/council2d/LivingArchivist.png', |
| #141 | glbSeat: 'archivist', |
| #142 | chamberName: 'ARCHIVIST', |
| #143 | chamberSubtitle: 'RECORDS CHAMBER', |
| #144 | briefing: ['Vault graph queryable', 'Recent conversations indexed', 'Source continuity preserved'], |
| #145 | quickActions: ['Search Memory', 'Open Vault', 'Trace Source', 'Summarize Record'], |
| #146 | }, |
| #147 | { |
| #148 | id: 'oracle', |
| #149 | seatId: 'oracle', |
| #150 | label: 'Oracle', |
| #151 | mode: 'Strategize', |
| #152 | role: 'Roadmaps, forecasts, risk maps.', |
| #153 | brain: 'claude-opus-4-8', |
| #154 | color: '#e7c76f', |
| #155 | soft: 'rgba(231, 199, 111, 0.18)', |
| #156 | oneLine: 'The strategy room: translate the mission into dependencies and next moves.', |
| #157 | assetSrc: '/council2d/LivingOracle.png', |
| #158 | glbSeat: 'oracle', |
| #159 | chamberName: 'ORACLE', |
| #160 | chamberSubtitle: 'STRATEGY ROOM', |
| #161 | briefing: ['Roadmap branches ready', 'Timing window monitored', 'Dependencies mapped before action'], |
| #162 | quickActions: ['Map Risks', 'Forecast Timing', 'Sequence Moves', 'Compare Paths'], |
| #163 | }, |
| #164 | { |
| #165 | id: 'black', |
| #166 | seatId: 'black', |
| #167 | label: 'Black', |
| #168 | mode: 'Defense', |
| #169 | role: 'Defender', |
| #170 | brain: 'claude-opus-4-8', |
| #171 | color: '#b9c8ff', |
| #172 | soft: 'rgba(185, 200, 255, 0.14)', |
| #173 | oneLine: 'The defense room: review gates, watch active work, and isolate threats.', |
| #174 | assetSrc: '/council2d/LivingBlack.png', |
| #175 | glbSeat: 'black', |
| #176 | chamberName: 'BLACK', |
| #177 | chamberSubtitle: 'DEFENSE OPS', |
| #178 | briefing: ['Threat posture monitored', 'Approval gates enforced', 'Unsafe actions blocked before execution'], |
| #179 | quickActions: ['Review Threat', 'Harden Plan', 'Inspect Gate', 'Prepare Rebuttal'], |
| #180 | }, |
| #181 | { |
| #182 | id: 'reaper', |
| #183 | seatId: 'reaper', |
| #184 | label: 'Reaper', |
| #185 | mode: 'Execute', |
| #186 | role: 'Executor', |
| #187 | brain: 'kimi-k2-6', |
| #188 | color: '#cf2b34', |
| #189 | soft: 'rgba(207, 43, 52, 0.18)', |
| #190 | oneLine: 'The escalation room: forceful posture only after Black calls it in.', |
| #191 | assetSrc: '/council2d/LivingReaper.png', |
| #192 | glbSeat: 'reaper', |
| #193 | chamberName: 'REAPER', |
| #194 | chamberSubtitle: 'EXECUTION OPS', |
| #195 | briefing: ['Execution remains gated', 'Black escalation link required', 'No autonomous strike path exposed'], |
| #196 | quickActions: ['Prepare Execution', 'Verify Gate', 'Stage Action', 'Report Readiness'], |
| #197 | }, |
| #198 | { |
| #199 | id: 'scribe', |
| #200 | seatId: 'scribe', |
| #201 | label: 'Scribe', |
| #202 | mode: 'Draft', |
| #203 | role: 'Drafts, notices, remedy queue.', |
| #204 | brain: 'kimi-k2-5', |
| #205 | color: '#86d88d', |
| #206 | soft: 'rgba(134, 216, 141, 0.16)', |
| #207 | oneLine: 'The drafting room: written artifacts, remedy queue, and approval-ready text.', |
| #208 | assetSrc: '/council2d/LivingScribe.png', |
| #209 | glbSeat: 'scribe', |
| #210 | chamberName: 'SCRIBE', |
| #211 | chamberSubtitle: 'DRAFT DESK', |
| #212 | briefing: ['Draft queue open', 'Remedy language staged', 'Documents wait for King disposition'], |
| #213 | quickActions: ['Draft Notice', 'Prepare Packet', 'Format Filing', 'Queue Review'], |
| #214 | }, |
| #215 | { |
| #216 | id: 'harbinger', |
| #217 | seatId: 'harbinger', |
| #218 | label: 'Harbinger', |
| #219 | mode: 'Remember', |
| #220 | role: 'Source Verifier', |
| #221 | brain: 'deepseek-v4-pro', |
| #222 | color: '#9fb8ff', |
| #223 | soft: 'rgba(159, 184, 255, 0.15)', |
| #224 | oneLine: 'The verification room: drift checks and process audit before action lands.', |
| #225 | assetSrc: '/council2d/LivingHarbinger.png', |
| #226 | glbSeat: 'harbinger', |
| #227 | chamberName: 'HARBINGER', |
| #228 | chamberSubtitle: 'VAULT / MEMORY', |
| #229 | briefing: ['Source checks active', 'Drift scan ready', 'Process audit before claims land'], |
| #230 | quickActions: ['Verify Claim', 'Check Drift', 'Audit Source', 'Compare Record'], |
| #231 | }, |
| #232 | { |
| #233 | id: 'embodied', |
| #234 | seatId: 'living', |
| #235 | label: 'Embodied', |
| #236 | mode: 'Council Mode', |
| #237 | role: 'Mastermind synthesis after the room deliberates.', |
| #238 | brain: 'claude-opus-4-8', |
| #239 | color: '#f5efe0', |
| #240 | soft: 'rgba(245, 239, 224, 0.16)', |
| #241 | oneLine: 'The Council Chamber: watch the eight seats deliberate, then read the synthesis.', |
| #242 | assetSrc: '/council2d/LivingEmbodied.png', |
| #243 | glbSeat: 'embodied', |
| #244 | chamberName: 'EMBODIED', |
| #245 | chamberSubtitle: 'COUNCIL MODE', |
| #246 | briefing: ['All seats can convene', 'Objections stay visible', 'Embodied speaks the synthesis last'], |
| #247 | quickActions: ['Convene Council', 'Ask Mastermind', 'Read Deliberation', 'Issue Directive'], |
| #248 | }, |
| #249 | ]; |
| #250 | |
| #251 | const CHAMBER_CYCLE_ORDER: RoomId[] = ['embodied', 'oracle', 'reaper', 'black', 'harbinger', 'scribe', 'pyra', 'archivist']; |
| #252 | |
| #253 | const CHAMBER_VISUAL_STATES = CHAMBER_CYCLE_ORDER |
| #254 | .map(id => CHARACTER_STATES.find(character => character.id === id)) |
| #255 | .filter((character): character is CharacterState => Boolean(character)); |
| #256 | |
| #257 | const CHAMBER_SLOT_LAYOUT: Array<{ x: number; y: number; scale: number; z: number }> = [ |
| #258 | { x: 50, y: 40, scale: 1.16, z: 9 }, |
| #259 | { x: 35, y: 27, scale: 0.7, z: 5 }, |
| #260 | { x: 65, y: 27, scale: 0.7, z: 5 }, |
| #261 | { x: 88, y: 51, scale: 0.62, z: 4 }, |
| #262 | { x: 72, y: 78, scale: 0.66, z: 4 }, |
| #263 | { x: 50, y: 83, scale: 0.66, z: 4 }, |
| #264 | { x: 28, y: 78, scale: 0.66, z: 4 }, |
| #265 | { x: 12, y: 51, scale: 0.62, z: 3 }, |
| #266 | ]; |
| #267 | |
| #268 | function normalizedChamberActive(activeId: RoomId) { |
| #269 | return CHAMBER_CYCLE_ORDER.includes(activeId) ? activeId : 'embodied'; |
| #270 | } |
| #271 | |
| #272 | function getChamberLayout(id: RoomId, activeId: RoomId) { |
| #273 | const activeIndex = CHAMBER_CYCLE_ORDER.indexOf(normalizedChamberActive(activeId)); |
| #274 | const seatIndex = CHAMBER_CYCLE_ORDER.indexOf(id); |
| #275 | const offset = seatIndex >= 0 ? (seatIndex - activeIndex + CHAMBER_CYCLE_ORDER.length) % CHAMBER_CYCLE_ORDER.length : 0; |
| #276 | return CHAMBER_SLOT_LAYOUT[offset] ?? CHAMBER_SLOT_LAYOUT[0]; |
| #277 | } |
| #278 | |
| #279 | const COUNCIL_FALLBACK: CouncilAgent[] = [ |
| #280 | { id: 'black', name: 'Black', role: 'Defense', brain: 'claude-opus-4-8', mandate: 'Threat isolation and safety verification.' }, |
| #281 | { id: 'reaper', name: 'Reaper', role: 'Crisis / Strike', brain: 'kimi-k2-6', mandate: 'Gated escalation from Black only.' }, |
| #282 | { id: 'living', name: 'Living', role: 'Teaching', brain: 'deepseek-v4-flash', mandate: 'Plain-language teaching.' }, |
| #283 | { id: 'archivist', name: 'Archivist', role: 'Memory', brain: 'openyourmind-qwen35', mandate: 'Vault continuity and recall.' }, |
| #284 | { id: 'scribe', name: 'Scribe', role: 'Documents', brain: 'kimi-k2-5', mandate: 'Drafts and written artifacts.' }, |
| #285 | { id: 'harbinger', name: 'Harbinger', role: 'Verifier', brain: 'deepseek-v4-pro', mandate: 'Drift and contradiction checks.' }, |
| #286 | { id: 'oracle', name: 'Oracle', role: 'Strategy', brain: 'claude-opus-4-8', mandate: 'Strategy and timing.' }, |
| #287 | { id: 'pyra', name: 'Pyra', role: 'Creative', brain: 'kimi-k2-6', mandate: 'Tone, symbolic presentation, and creative shape.' }, |
| #288 | ]; |
| #289 | |
| #290 | function formatDiem(value: unknown) { |
| #291 | const number = Number(value ?? 0); |
| #292 | if (!Number.isFinite(number)) return '0.000'; |
| #293 | return number.toFixed(number >= 1 ? 2 : 4); |
| #294 | } |
| #295 | |
| #296 | function formatCountdown(seconds: unknown) { |
| #297 | const total = Number(seconds ?? 0); |
| #298 | if (!Number.isFinite(total) || total <= 0) return 'reset soon'; |
| #299 | const hours = Math.floor(total / 3600); |
| #300 | const minutes = Math.floor((total % 3600) / 60); |
| #301 | if (hours <= 0) return `${minutes}m`; |
| #302 | return `${hours}h ${minutes.toString().padStart(2, '0')}m`; |
| #303 | } |
| #304 | |
| #305 | function formatTime(value: unknown) { |
| #306 | const date = new Date(String(value || Date.now())); |
| #307 | if (Number.isNaN(date.getTime())) return ''; |
| #308 | return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); |
| #309 | } |
| #310 | |
| #311 | function formatEventTime(value: unknown) { |
| #312 | if (!value) return 'now'; |
| #313 | const date = new Date(String(value)); |
| #314 | if (Number.isNaN(date.getTime())) return 'now'; |
| #315 | return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); |
| #316 | } |
| #317 | |
| #318 | function isGlassesRow(row: OpsActivityRow) { |
| #319 | const joined = `${row.actor} ${row.source ?? ''} ${row.status ?? ''}`.toLowerCase(); |
| #320 | return joined.includes('glasses') || joined.includes('bridge_king') || joined.includes('bridge_maaxx'); |
| #321 | } |
| #322 | |
| #323 | function isFailureRow(row: OpsActivityRow) { |
| #324 | const detail = row.detail as any; |
| #325 | const explicitError = detail?.error ?? detail?.payload?.error ?? detail?.raw?.error ?? detail?.stack ?? detail?.payload?.stack; |
| #326 | if (explicitError) return true; |
| #327 | const status = String(row.status ?? '').toLowerCase(); |
| #328 | const message = String(row.message ?? '').toLowerCase(); |
| #329 | const eventType = String(detail?.event_type ?? detail?.raw?.event_type ?? '').toLowerCase(); |
| #330 | const statusText = `${status} ${message} ${eventType}`; |
| #331 | return ['wake_miss', 'failed', 'failure', 'down', 'denied', 'exception', 'fragment_drop', 'transcription_fragment_drop', 'timeout'].some(token => statusText.includes(token)); |
| #332 | } |
| #333 | |
| #334 | function groupOpsRows(rows: OpsActivityRow[]) { |
| #335 | const grouped = new Map<string, OpsActivityRow>(); |
| #336 | for (const row of rows) { |
| #337 | const key = `${row.actor}:${row.message}:${row.status ?? ''}`; |
| #338 | const existing = grouped.get(key); |
| #339 | if (!existing) { |
| #340 | grouped.set(key, { ...row, count: row.count ?? 1 }); |
| #341 | continue; |
| #342 | } |
| #343 | const previousDetail = existing.detail && typeof existing.detail === 'object' && 'examples' in (existing.detail as any) |
| #344 | ? (existing.detail as any).examples |
| #345 | : [existing.detail ?? existing]; |
| #346 | grouped.set(key, { |
| #347 | ...existing, |
| #348 | at: row.at ?? existing.at, |
| #349 | count: (existing.count ?? 1) + 1, |
| #350 | detail: { |
| #351 | grouped: true, |
| #352 | count: (existing.count ?? 1) + 1, |
| #353 | latest: row.detail ?? row, |
| #354 | examples: [...previousDetail, row.detail ?? row].slice(-5), |
| #355 | }, |
| #356 | }); |
| #357 | } |
| #358 | return Array.from(grouped.values()); |
| #359 | } |
| #360 | |
| #361 | function stringifyLogDetail(detail: unknown) { |
| #362 | try { |
| #363 | return JSON.stringify(detail, null, 2); |
| #364 | } catch { |
| #365 | return String(detail ?? ''); |
| #366 | } |
| #367 | } |
| #368 | |
| #369 | function ExpandableLogRow({ row }: { row: OpsActivityRow }) { |
| #370 | const failure = isFailureRow(row); |
| #371 | const live = row.status === 'wake' || row.status === 'response' || row.status === 'session'; |
| #372 | return ( |
| #373 | <details |
| #374 | className={`ops-log-row ${failure ? 'warn' : live ? 'live' : ''}`} |
| #375 | style={{ '--seat-color': row.color ?? '#69aaf8' } as CSSProperties} |
| #376 | > |
| #377 | <summary> |
| #378 | <i /> |
| #379 | <div> |
| #380 | <strong>{row.actor}{row.count && row.count > 1 ? <small> ×{row.count}</small> : null}</strong> |
| #381 | <span>{row.message}</span> |
| #382 | </div> |
| #383 | <em>{formatEventTime(row.at)}</em> |
| #384 | </summary> |
| #385 | <pre>{stringifyLogDetail(row.detail ?? row)}</pre> |
| #386 | </details> |
| #387 | ); |
| #388 | } |
| #389 | |
| #390 | function statusStyle(index: number): CSSProperties { |
| #391 | return { '--stagger-index': index } as CSSProperties; |
| #392 | } |
| #393 | |
| #394 | function CouncilChamber({ |
| #395 | activeStateId, |
| #396 | selectState, |
| #397 | cycleState, |
| #398 | motionState, |
| #399 | animationsEnabled, |
| #400 | }: { |
| #401 | activeStateId: RoomId; |
| #402 | selectState: (id: RoomId) => void; |
| #403 | cycleState: (direction: 1 | -1) => void; |
| #404 | motionState: MotionState; |
| #405 | animationsEnabled: boolean; |
| #406 | }) { |
| #407 | const stageRef = useRef<HTMLDivElement | null>(null); |
| #408 | const activeRef = useRef(activeStateId); |
| #409 | const motionRef = useRef(motionState); |
| #410 | |
| #411 | useEffect(() => { |
| #412 | activeRef.current = activeStateId; |
| #413 | motionRef.current = motionState; |
| #414 | }, [activeStateId, motionState]); |
| #415 | |
| #416 | useEffect(() => { |
| #417 | const mount = stageRef.current; |
| #418 | if (!mount) return; |
| #419 | |
| #420 | let cancelled = false; |
| #421 | let cleanup = () => {}; |
| #422 | |
| #423 | Promise.all([ |
| #424 | import('three'), |
| #425 | import('three/examples/jsm/loaders/GLTFLoader.js'), |
| #426 | import('three/examples/jsm/loaders/DRACOLoader.js'), |
| #427 | ]).then(([threeModule, loaderModule, dracoModule]) => { |
| #428 | if (cancelled || !stageRef.current) return; |
| #429 | const THREE = ((threeModule as any).default ?? threeModule) as any; |
| #430 | const GLTFLoader = (loaderModule as any).GLTFLoader; |
| #431 | const DRACOLoader = (dracoModule as any).DRACOLoader; |
| #432 | |
| #433 | const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); |
| #434 | renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.7)); |
| #435 | renderer.setSize(mount.clientWidth || 900, mount.clientHeight || 650); |
| #436 | renderer.outputColorSpace = THREE.SRGBColorSpace; |
| #437 | renderer.domElement.setAttribute('aria-label', 'Aethon Council Meshy-ready GLB chamber'); |
| #438 | renderer.domElement.dataset.meshyGlbCanvas = 'true'; |
| #439 | mount.appendChild(renderer.domElement); |
| #440 | |
| #441 | const scene = new THREE.Scene(); |
| #442 | scene.fog = new THREE.FogExp2(0x020711, 0.026); |
| #443 | |
| #444 | const camera = new THREE.PerspectiveCamera(40, (mount.clientWidth || 900) / (mount.clientHeight || 650), 0.1, 100); |
| #445 | camera.position.set(0, 1.7, 10.8); |
| #446 | camera.lookAt(0, 0.72, 0); |
| #447 | |
| #448 | scene.add(new THREE.AmbientLight(0xf2f7ff, 2.05)); |
| #449 | const hemiLight = new THREE.HemisphereLight(0xffffff, 0x17233b, 1.35); |
| #450 | scene.add(hemiLight); |
| #451 | const keyLight = new THREE.DirectionalLight(0xffffff, 5.1); |
| #452 | keyLight.position.set(1.4, 4.9, 5.2); |
| #453 | scene.add(keyLight); |
| #454 | const fillLight = new THREE.DirectionalLight(0x9fcaff, 2.6); |
| #455 | fillLight.position.set(-3.5, 2.5, 4.2); |
| #456 | scene.add(fillLight); |
| #457 | const rimLight = new THREE.PointLight(0x78b7ff, 2.4, 18); |
| #458 | rimLight.position.set(-4.5, 2.7, -2.5); |
| #459 | scene.add(rimLight); |
| #460 | |
| #461 | const floor = new THREE.Group(); |
| #462 | const floorMat = new THREE.MeshBasicMaterial({ color: 0xd9b15f, transparent: true, opacity: 0.25, side: THREE.DoubleSide }); |
| #463 | for (let radius = 1.45; radius <= 4.75; radius += 0.58) { |
| #464 | const ring = new THREE.Mesh(new THREE.RingGeometry(radius, radius + 0.006, 160), floorMat.clone()); |
| #465 | ring.rotation.x = -Math.PI / 2; |
| #466 | floor.add(ring); |
| #467 | } |
| #468 | const spokes = new THREE.Mesh( |
| #469 | new THREE.CircleGeometry(4.75, 16), |
| #470 | new THREE.MeshBasicMaterial({ color: 0xd9b15f, transparent: true, opacity: 0.035, side: THREE.DoubleSide }), |
| #471 | ); |
| #472 | spokes.rotation.x = -Math.PI / 2; |
| #473 | floor.add(spokes); |
| #474 | floor.position.y = -0.02; |
| #475 | scene.add(floor); |
| #476 | |
| #477 | const dracoLoader = new DRACOLoader(); |
| #478 | dracoLoader.setDecoderPath('/draco/'); |
| #479 | dracoLoader.preload(); |
| #480 | const loader = new GLTFLoader(); |
| #481 | loader.setDRACOLoader(dracoLoader); |
| #482 | const clock = new THREE.Clock(); |
| #483 | const entries: Array<{ |
| #484 | character: CharacterState; |
| #485 | group: any; |
| #486 | aura: any; |
| #487 | loaded: boolean; |
| #488 | idleModel?: any; |
| #489 | riggedModel?: any; |
| #490 | mixer?: any; |
| #491 | actions?: any[]; |
| #492 | riggedRequested?: boolean; |
| #493 | riggedFailed?: boolean; |
| #494 | }> = []; |
| #495 | |
| #496 | const toWorld = (character: CharacterState) => { |
| #497 | const position = getChamberLayout(character.id, activeRef.current); |
| #498 | return new THREE.Vector3( |
| #499 | (position.x - 50) * 0.092, |
| #500 | 0.26 + (56 - position.y) * 0.011, |
| #501 | (position.y - 48) * 0.04, |
| #502 | ); |
| #503 | }; |
| #504 | |
| #505 | const prepareCouncilModel = (model: any, character: CharacterState) => { |
| #506 | const bounds = new THREE.Box3().setFromObject(model); |
| #507 | const size = bounds.getSize(new THREE.Vector3()); |
| #508 | const center = bounds.getCenter(new THREE.Vector3()); |
| #509 | model.position.sub(center); |
| #510 | const scalar = size.y > 0 ? 1.82 / size.y : 1; |
| #511 | model.scale.setScalar(scalar); |
| #512 | model.position.y = 0.93; |
| #513 | model.traverse((node: any) => { |
| #514 | if (!node?.isMesh) return; |
| #515 | if (node.geometry?.computeBoundingBox) { |
| #516 | node.geometry.computeBoundingBox(); |
| #517 | const geometrySize = node.geometry.boundingBox?.getSize(new THREE.Vector3()); |
| #518 | if (geometrySize) { |
| #519 | const maxAxis = Math.max(geometrySize.x, geometrySize.y, geometrySize.z); |
| #520 | const minAxis = Math.min(geometrySize.x, geometrySize.y, geometrySize.z); |
| #521 | const squareish = geometrySize.x > maxAxis * 0.58 && geometrySize.y > maxAxis * 0.58; |
| #522 | if (maxAxis > 0 && minAxis / maxAxis < 0.012 && squareish) { |
| #523 | node.visible = false; |
| #524 | return; |
| #525 | } |
| #526 | } |
| #527 | } |
| #528 | node.frustumCulled = false; |
| #529 | if (!node.material) { |
| #530 | node.material = new THREE.MeshStandardMaterial({ |
| #531 | color: new THREE.Color(character.color), |
| #532 | emissive: new THREE.Color(0x000000), |
| #533 | emissiveIntensity: 0, |
| #534 | metalness: 0.18, |
| #535 | roughness: 0.38, |
| #536 | transparent: false, |
| #537 | opacity: 1, |
| #538 | side: THREE.DoubleSide, |
| #539 | depthWrite: true, |
| #540 | }); |
| #541 | return; |
| #542 | } |
| #543 | const mats = Array.isArray(node.material) ? node.material : [node.material]; |
| #544 | mats.forEach((material: any) => { |
| #545 | if (!material) return; |
| #546 | material.transparent = false; |
| #547 | material.opacity = 1; |
| #548 | material.side = THREE.DoubleSide; |
| #549 | material.depthWrite = true; |
| #550 | material.toneMapped = true; |
| #551 | if (material.map) { |
| #552 | material.map.colorSpace = THREE.SRGBColorSpace; |
| #553 | material.map.needsUpdate = true; |
| #554 | } |
| #555 | if (material.emissiveMap) { |
| #556 | material.emissiveMap.colorSpace = THREE.SRGBColorSpace; |
| #557 | material.emissiveMap.needsUpdate = true; |
| #558 | } |
| #559 | if (node.geometry?.attributes?.color && 'vertexColors' in material) { |
| #560 | material.vertexColors = true; |
| #561 | } |
| #562 | if ('envMapIntensity' in material && typeof material.envMapIntensity === 'number') { |
| #563 | material.envMapIntensity = Math.max(material.envMapIntensity, 1.35); |
| #564 | } |
| #565 | if ('emissive' in material && material.emissive?.set) { |
| #566 | material.userData = material.userData || {}; |
| #567 | material.userData.aethonOriginalEmissive = material.emissive.clone?.(); |
| #568 | material.userData.aethonOriginalEmissiveIntensity = Number(material.emissiveIntensity ?? 0); |
| #569 | } |
| #570 | if ('roughness' in material && typeof material.roughness === 'number') material.roughness = Math.min(0.62, material.roughness + 0.08); |
| #571 | material.needsUpdate = true; |
| #572 | }); |
| #573 | }); |
| #574 | return model; |
| #575 | }; |
| #576 | |
| #577 | const loadRiggedModel = (entry: (typeof entries)[number]) => { |
| #578 | if (entry.riggedRequested || entry.riggedFailed || entry.riggedModel) return; |
| #579 | entry.riggedRequested = true; |
| #580 | loader.load(`/api/council-glb/${entry.character.glbSeat}?variant=rigged`, (gltf: any) => { |
| #581 | if (cancelled) return; |
| #582 | const model = prepareCouncilModel(gltf.scene, entry.character); |
| #583 | model.visible = false; |
| #584 | entry.riggedModel = model; |
| #585 | if (gltf.animations?.length) { |
| #586 | const mixer = new THREE.AnimationMixer(model); |
| #587 | const actions = gltf.animations.map((clip: any) => mixer.clipAction(clip)); |
| #588 | actions.forEach((action: any) => { |
| #589 | action.enabled = true; |
| #590 | action.paused = true; |
| #591 | action.play(); |
| #592 | }); |
| #593 | entry.mixer = mixer; |
| #594 | entry.actions = actions; |
| #595 | } |
| #596 | entry.group.add(model); |
| #597 | }, undefined, () => { |
| #598 | entry.riggedFailed = true; |
| #599 | }); |
| #600 | }; |
| #601 | |
| #602 | CHAMBER_VISUAL_STATES.forEach((character) => { |
| #603 | const group = new THREE.Group(); |
| #604 | const aura = new THREE.Mesh( |
| #605 | new THREE.TorusGeometry(0.68, 0.012, 10, 96), |
| #606 | new THREE.MeshBasicMaterial({ color: new THREE.Color(character.color), transparent: true, opacity: 0.48 }), |
| #607 | ); |
| #608 | aura.rotation.x = Math.PI / 2; |
| #609 | aura.position.y = -0.02; |
| #610 | group.add(aura); |
| #611 | group.position.copy(toWorld(character)); |
| #612 | scene.add(group); |
| #613 | const entry: (typeof entries)[number] = { character, group, aura, loaded: false }; |
| #614 | entries.push(entry); |
| #615 | |
| #616 | loader.load(`/api/council-glb/${character.glbSeat}?variant=idle`, (gltf: any) => { |
| #617 | if (cancelled) return; |
| #618 | const model = prepareCouncilModel(gltf.scene, character); |
| #619 | entry.idleModel = model; |
| #620 | group.add(model); |
| #621 | entry.loaded = true; |
| #622 | }, undefined, () => { |
| #623 | const fallback = new THREE.Mesh( |
| #624 | new THREE.CapsuleGeometry(0.24, 1.4, 8, 18), |
| #625 | new THREE.MeshStandardMaterial({ color: character.color, emissive: character.color, emissiveIntensity: 0.35, transparent: true, opacity: 0.58 }), |
| #626 | ); |
| #627 | fallback.position.y = 1.05; |
| #628 | group.add(fallback); |
| #629 | }); |
| #630 | }); |
| #631 | |
| #632 | const resize = () => { |
| #633 | if (!stageRef.current) return; |
| #634 | const width = stageRef.current.clientWidth || 900; |
| #635 | const height = stageRef.current.clientHeight || 650; |
| #636 | renderer.setSize(width, height); |
| #637 | camera.aspect = width / height; |
| #638 | camera.updateProjectionMatrix(); |
| #639 | }; |
| #640 | const observer = new ResizeObserver(resize); |
| #641 | observer.observe(mount); |
| #642 | |
| #643 | let raf = 0; |
| #644 | const animate = () => { |
| #645 | const delta = clock.getDelta(); |
| #646 | const time = performance.now() / 1000; |
| #647 | floor.rotation.y = time * 0.045; |
| #648 | const motion = motionRef.current; |
| #649 | |
| #650 | entries.forEach((entry, index) => { |
| #651 | const { character, group, aura } = entry; |
| #652 | const active = character.id === normalizedChamberActive(activeRef.current); |
| #653 | if (active && animationsEnabled) loadRiggedModel(entry); |
| #654 | const showRigged = active && animationsEnabled && Boolean(entry.riggedModel && entry.actions?.length); |
| #655 | if (entry.idleModel) entry.idleModel.visible = !showRigged; |
| #656 | if (entry.riggedModel) entry.riggedModel.visible = showRigged; |
| #657 | if (entry.actions?.length) { |
| #658 | entry.actions.forEach((action: any) => { |
| #659 | action.paused = !showRigged; |
| #660 | action.timeScale = showRigged ? 0.82 : 0; |
| #661 | }); |
| #662 | } |
| #663 | if (showRigged && entry.mixer) entry.mixer.update(delta); |
| #664 | const base = toWorld(character); |
| #665 | const float = Math.sin(time * (active ? 1.4 : 0.75) + index * 0.72) * (active ? 0.07 : 0.04); |
| #666 | const pulse = motion === 'thinking' ? 0.08 : motion === 'working' ? 0.055 : motion === 'speaking' ? 0.1 : 0.035; |
| #667 | const target = base.clone(); |
| #668 | target.y += float + (active ? 0.04 : 0); |
| #669 | target.z += active ? 0.46 : -0.2; |
| #670 | group.position.lerp(target, 0.065); |
| #671 | const layout = getChamberLayout(character.id, activeRef.current); |
| #672 | const scale = layout.scale * (active ? 1.1 : 0.9); |
| #673 | group.scale.lerp(new THREE.Vector3(scale, scale, scale), 0.07); |
| #674 | group.rotation.y = Math.sin(time * 0.42 + index) * (active ? 0.085 : 0.045); |
| #675 | aura.material.opacity = active ? 0.72 + Math.sin(time * 3.1) * pulse : 0.22; |
| #676 | aura.scale.setScalar(active ? 1.28 + Math.sin(time * 2.4) * 0.045 : 0.82); |
| #677 | group.traverse((node: any) => { |
| #678 | if (!node?.material || !node?.isMesh) return; |
| #679 | const mats = Array.isArray(node.material) ? node.material : [node.material]; |
| #680 | mats.forEach((material: any) => { |
| #681 | if (!material) return; |
| #682 | material.opacity = 1; |
| #683 | material.transparent = false; |
| #684 | if ('emissiveIntensity' in material) { |
| #685 | const baseEmissive = Number(material.userData?.aethonOriginalEmissiveIntensity ?? 0); |
| #686 | material.emissiveIntensity = baseEmissive + (active ? 0.055 : 0.008); |
| #687 | } |
| #688 | }); |
| #689 | }); |
| #690 | }); |
| #691 | |
| #692 | renderer.render(scene, camera); |
| #693 | raf = window.requestAnimationFrame(animate); |
| #694 | }; |
| #695 | animate(); |
| #696 | |
| #697 | cleanup = () => { |
| #698 | window.cancelAnimationFrame(raf); |
| #699 | observer.disconnect(); |
| #700 | renderer.dispose(); |
| #701 | dracoLoader.dispose?.(); |
| #702 | scene.traverse((node: any) => { |
| #703 | if (node.geometry?.dispose) node.geometry.dispose(); |
| #704 | if (node.material) { |
| #705 | const mats = Array.isArray(node.material) ? node.material : [node.material]; |
| #706 | mats.forEach((material: any) => material?.dispose?.()); |
| #707 | } |
| #708 | }); |
| #709 | renderer.domElement.remove(); |
| #710 | }; |
| #711 | }).catch(() => {}); |
| #712 | |
| #713 | return () => { |
| #714 | cancelled = true; |
| #715 | cleanup(); |
| #716 | }; |
| #717 | }, []); |
| #718 | |
| #719 | return ( |
| #720 | <section className="osv4-chamber-card" data-osv4-council-chamber="meshy-glb-ring"> |
| #721 | <div className="chamber-orbit" data-motion-state={motionState}> |
| #722 | <div ref={stageRef} className="council-glb-stage" data-meshy-slot-map="idle-unrigged-hover-rigged-ready" /> |
| #723 | <div className="chamber-floor-glyph" aria-hidden="true" /> |
| #724 | <div className="chamber-starfield" aria-hidden="true" /> |
| #725 | <button type="button" className="chamber-side-arrow chamber-side-arrow-left" aria-label="Previous chamber" onClick={() => cycleState(-1)}> |
| #726 | <ChevronLeft size={24} /> |
| #727 | </button> |
| #728 | <button type="button" className="chamber-side-arrow chamber-side-arrow-right" aria-label="Next chamber" onClick={() => cycleState(1)}> |
| #729 | <ChevronRight size={24} /> |
| #730 | </button> |
| #731 | {CHAMBER_VISUAL_STATES.map((character) => { |
| #732 | const position = getChamberLayout(character.id, activeStateId); |
| #733 | const active = character.id === normalizedChamberActive(activeStateId); |
| #734 | return ( |
| #735 | <button |
| #736 | key={character.id} |
| #737 | type="button" |
| #738 | className={`council-character glb-hotspot seat-${character.id} ${active ? 'active' : ''}`} |
| #739 | onMouseEnter={() => selectState(character.id)} |
| #740 | onFocus={() => selectState(character.id)} |
| #741 | onClick={() => selectState(character.id)} |
| #742 | style={{ |
| #743 | '--x': `${position.x}%`, |
| #744 | '--y': `${position.y}%`, |
| #745 | '--scale': position.scale, |
| #746 | '--z': position.z, |
| #747 | '--seat-color': character.color, |
| #748 | '--seat-soft': character.soft, |
| #749 | } as CSSProperties} |
| #750 | aria-label={`Activate ${character.label}`} |
| #751 | > |
| #752 | <span className="character-aura" aria-hidden="true" /> |
| #753 | <span className="character-label"> |
| #754 | <i>Aethon</i> |
| #755 | <strong>{character.label}</strong> |
| #756 | <em>{character.role}</em> |
| #757 | </span> |
| #758 | </button> |
| #759 | ); |
| #760 | })} |
| #761 | </div> |
| #762 | </section> |
| #763 | ); |
| #764 | } |
| #765 | |
| #766 | export default function Cockpit({ initialMode = 'operator' }: { initialMode?: OperatingMode }) { |
| #767 | const [osMode, setOsMode] = useState<OperatingMode>(initialMode); |
| #768 | const isDemo = osMode === 'demo'; |
| #769 | const [activeStateId, setActiveStateId] = useState<RoomId>('embodied'); |
| #770 | const [model, setModel] = useState('deepseek-v4-flash'); |
| #771 | const [input, setInput] = useState(''); |
| #772 | const [response, setResponse] = useState(''); |
| #773 | const [chatTranscript, setChatTranscript] = useState<ChatTurn[]>([]); |
| #774 | const [chatHistory, setChatHistory] = useState<any[]>([]); |
| #775 | const [activeSessionId, setActiveSessionId] = useState(''); |
| #776 | const [loading, setLoading] = useState(false); |
| #777 | const [presence, setPresence] = useState<MotionState>('idle'); |
| #778 | const [modelStatus, setModelStatus] = useState(''); |
| #779 | const [glbAnimationsEnabled, setGlbAnimationsEnabled] = useState(false); |
| #780 | const [showSystemLogs, setShowSystemLogs] = useState(false); |
| #781 | const [missionGoal, setMissionGoal] = useState('Bring the next right action into view.'); |
| #782 | const [taskGoal, setTaskGoal] = useState(''); |
| #783 | const [taskStatus, setTaskStatus] = useState(''); |
| #784 | const [submittingTask, setSubmittingTask] = useState(false); |
| #785 | const [toolFeed, setToolFeed] = useState<any>(null); |
| #786 | const [councilMessages, setCouncilMessages] = useState<CouncilMessage[]>([]); |
| #787 | const [activeCouncilRoom, setActiveCouncilRoom] = useState('convene-all'); |
| #788 | const [councilPrompt, setCouncilPrompt] = useState(''); |
| #789 | const [councilStatus, setCouncilStatus] = useState(''); |
| #790 | const [conveningCouncil, setConveningCouncil] = useState(false); |
| #791 | const [seatPrompt, setSeatPrompt] = useState(''); |
| #792 | const [seatChatStatus, setSeatChatStatus] = useState(''); |
| #793 | const [seatChatLoading, setSeatChatLoading] = useState(false); |
| #794 | const [seatChatTranscript, setSeatChatTranscript] = useState<ChatTurn[]>([]); |
| #795 | const [scrapeUrls, setScrapeUrls] = useState(''); |
| #796 | const [scrapeStatus, setScrapeStatus] = useState(''); |
| #797 | const [submittingScrape, setSubmittingScrape] = useState(false); |
| #798 | const [historySearch, setHistorySearch] = useState(''); |
| #799 | const [graphSearch, setGraphSearch] = useState(''); |
| #800 | const [nowLabel, setNowLabel] = useState(''); |
| #801 | |
| #802 | const chatInputRef = useRef<HTMLTextAreaElement | null>(null); |
| #803 | const taskInputRef = useRef<HTMLTextAreaElement | null>(null); |
| #804 | const councilPromptRef = useRef<HTMLTextAreaElement | null>(null); |
| #805 | const seatPromptRef = useRef<HTMLTextAreaElement | null>(null); |
| #806 | const scrapeInputRef = useRef<HTMLTextAreaElement | null>(null); |
| #807 | |
| #808 | const { data: me, error: meError } = useSWR(isDemo ? null : '/api/me', fetcher, { shouldRetryOnError: (error) => !isAuthError(error) }); |
| #809 | const { data: status } = useSWR(isDemo ? null : '/api/status', fetcher, { refreshInterval: 5000, shouldRetryOnError: (error) => !isAuthError(error) }); |
| #810 | const { data: spend } = useSWR(isDemo ? null : '/api/spend', fetcher, { refreshInterval: 10000, shouldRetryOnError: (error) => !isAuthError(error) }); |
| #811 | const { data: approvals } = useSWR(isDemo ? null : '/api/approval-queue?status=pending', fetcher, { refreshInterval: 6000, shouldRetryOnError: (error) => !isAuthError(error) }); |
| #812 | const { data: tasks } = useSWR(isDemo ? null : '/api/aethon-tasks', fetcher, { refreshInterval: 8000, shouldRetryOnError: (error) => !isAuthError(error) }); |
| #813 | const { data: vaultGraph } = useSWR(isDemo ? null : '/api/vault-graph', fetcher, { refreshInterval: 60000, shouldRetryOnError: (error) => !isAuthError(error) }); |
| #814 | const { data: councilState } = useSWR(isDemo ? null : '/api/council-bus', fetcher, { refreshInterval: 10000, shouldRetryOnError: (error) => !isAuthError(error) }); |
| #815 | |
| #816 | useEffect(() => { |
| #817 | const updateClock = () => setNowLabel(new Date().toLocaleString()); |
| #818 | updateClock(); |
| #819 | const id = window.setInterval(updateClock, 30000); |
| #820 | return () => window.clearInterval(id); |
| #821 | }, []); |
| #822 | |
| #823 | useEffect(() => { |
| #824 | if (isDemo) return; |
| #825 | const saved = window.localStorage.getItem('living-os-current-goal'); |
| #826 | if (saved?.trim()) setMissionGoal(saved); |
| #827 | }, [isDemo]); |
| #828 | |
| #829 | useEffect(() => { |
| #830 | if (isDemo) return; |
| #831 | window.localStorage.setItem('living-os-current-goal', missionGoal); |
| #832 | }, [isDemo, missionGoal]); |
| #833 | |
| #834 | useEffect(() => { |
| #835 | if (isDemo || !isAuthError(meError)) return; |
| #836 | const next = `${window.location.origin}${window.location.pathname}${window.location.search}`; |
| #837 | window.location.href = `https://theliving.ai/auth/login?redirect=${encodeURIComponent(next)}`; |
| #838 | }, [isDemo, meError]); |
| #839 | |
| #840 | useEffect(() => { |
| #841 | if (isDemo) return; |
| #842 | const source = new EventSource('/api/v1/tasks/stream'); |
| #843 | source.addEventListener('task-progress', event => { |
| #844 | try { |
| #845 | setToolFeed(JSON.parse((event as MessageEvent).data)); |
| #846 | } catch { |
| #847 | setToolFeed(null); |
| #848 | } |
| #849 | }); |
| #850 | source.onerror = () => source.close(); |
| #851 | return () => source.close(); |
| #852 | }, [isDemo]); |
| #853 | |
| #854 | useEffect(() => { |
| #855 | if (isDemo) return; |
| #856 | const source = new EventSource('/api/council-bus/stream'); |
| #857 | source.addEventListener('council-message', event => { |
| #858 | try { |
| #859 | const payload = JSON.parse((event as MessageEvent).data); |
| #860 | if (Array.isArray(payload?.messages)) setCouncilMessages(payload.messages); |
| #861 | } catch {} |
| #862 | }); |
| #863 | source.onerror = () => source.close(); |
| #864 | return () => source.close(); |
| #865 | }, [isDemo]); |
| #866 | |
| #867 | useEffect(() => { |
| #868 | if (isDemo) return; |
| #869 | if (Array.isArray(councilState?.messages)) setCouncilMessages(councilState.messages); |
| #870 | }, [councilState, isDemo]); |
| #871 | |
| #872 | const hydrateChatHistory = async (preferredSessionId?: string) => { |
| #873 | if (isDemo) return; |
| #874 | try { |
| #875 | const sessionsPayload = await sameOriginFetch('/api/v1/chat/sessions').then(readJsonOrThrow); |
| #876 | const sessions = Array.isArray(sessionsPayload?.sessions) ? sessionsPayload.sessions : []; |
| #877 | const records = await Promise.all(sessions.slice(0, 18).map(async (session: any) => { |
| #878 | try { |
| #879 | const detail = await sameOriginFetch(`/api/v1/chat/sessions/${encodeURIComponent(session.session_id)}`).then(readJsonOrThrow); |
| #880 | const messages = Array.isArray(detail?.messages) ? detail.messages : []; |
| #881 | const lastUser = [...messages].reverse().find((m: any) => m.role === 'user'); |
| #882 | const lastAssistant = [...messages].reverse().find((m: any) => m.role === 'assistant'); |
| #883 | return { |
| #884 | id: session.session_id, |
| #885 | at: session.updated_at || session.created_at || new Date().toISOString(), |
| #886 | query: lastUser?.content || session.title || 'Conversation', |
| #887 | answer: lastAssistant?.content || 'Open conversation', |
| #888 | sessionId: session.session_id, |
| #889 | }; |
| #890 | } catch { |
| #891 | return null; |
| #892 | } |
| #893 | })); |
| #894 | setChatHistory(records.filter(Boolean)); |
| #895 | if (preferredSessionId) setActiveSessionId(preferredSessionId); |
| #896 | else if (!activeSessionId && sessions[0]?.session_id) setActiveSessionId(sessions[0].session_id); |
| #897 | } catch { |
| #898 | setChatHistory([]); |
| #899 | } |
| #900 | }; |
| #901 | |
| #902 | useEffect(() => { |
| #903 | if (!isDemo && me?.userId) void hydrateChatHistory(); |
| #904 | }, [isDemo, me?.userId]); |
| #905 | |
| #906 | const ensureChatSession = async (title: string) => { |
| #907 | if (activeSessionId) return activeSessionId; |
| #908 | const created = await sameOriginFetch('/api/v1/chat/sessions', { |
| #909 | method: 'POST', |
| #910 | headers: { 'Content-Type': 'application/json' }, |
| #911 | body: JSON.stringify({ title: title.slice(0, 120) || 'New Conversation' }), |
| #912 | }).then(readJsonOrThrow); |
| #913 | const sessionId = created?.session_id || ''; |
| #914 | if (sessionId) setActiveSessionId(sessionId); |
| #915 | return sessionId; |
| #916 | }; |
| #917 | |
| #918 | const streamSharedChat = async (body: Record<string, any>, onChunk: (chunk: string) => void) => { |
| #919 | const res = await sameOriginFetch('/api/v1/chat/stream', { |
| #920 | method: 'POST', |
| #921 | headers: { 'Content-Type': 'application/json' }, |
| #922 | body: JSON.stringify(body), |
| #923 | }); |
| #924 | if (!res.ok || !res.body) { |
| #925 | const detail = await res.text().catch(() => ''); |
| #926 | throw new Error(detail || `Chat stream returned ${res.status}`); |
| #927 | } |
| #928 | |
| #929 | const reader = res.body.getReader(); |
| #930 | const decoder = new TextDecoder(); |
| #931 | let buffer = ''; |
| #932 | let output = ''; |
| #933 | let eventName = 'message'; |
| #934 | let meta: Record<string, any> | null = null; |
| #935 | const flush = (raw: string) => { |
| #936 | const line = raw.replace(/\r$/, ''); |
| #937 | if (line.startsWith('event:')) { |
| #938 | eventName = line.slice(6).trim() || 'message'; |
| #939 | return; |
| #940 | } |
| #941 | if (!line.startsWith('data:')) return; |
| #942 | const chunk = line.startsWith('data: ') ? line.slice(6) : line.slice(5); |
| #943 | if (chunk === '[DONE]') { |
| #944 | eventName = 'message'; |
| #945 | return; |
| #946 | } |
| #947 | if (eventName === 'meta') { |
| #948 | try { |
| #949 | meta = JSON.parse(chunk); |
| #950 | const called = String(meta?.model || meta?.provider || '').trim(); |
| #951 | if (called) setModelStatus(`Model called: ${called}`); |
| #952 | } catch {} |
| #953 | eventName = 'message'; |
| #954 | return; |
| #955 | } |
| #956 | output += chunk.replace(/\\n/g, '\n'); |
| #957 | onChunk(output); |
| #958 | }; |
| #959 | |
| #960 | while (true) { |
| #961 | const { value, done } = await reader.read(); |
| #962 | if (done) break; |
| #963 | buffer += decoder.decode(value, { stream: true }); |
| #964 | const parts = buffer.split('\n'); |
| #965 | buffer = parts.pop() || ''; |
| #966 | parts.forEach(flush); |
| #967 | } |
| #968 | if (buffer) flush(buffer); |
| #969 | return { output, meta }; |
| #970 | }; |
| #971 | |
| #972 | const selectState = (id: RoomId) => { |
| #973 | const next = CHARACTER_STATES.find(item => item.id === id) ?? CHARACTER_STATES[0]; |
| #974 | setActiveStateId(next.id); |
| #975 | setModel(next.brain); |
| #976 | if (next.id === 'embodied') setActiveCouncilRoom('convene-all'); |
| #977 | else if (next.id !== 'living') setActiveCouncilRoom(next.seatId); |
| #978 | if (next.id === 'living') window.setTimeout(() => chatInputRef.current?.focus({ preventScroll: true }), 120); |
| #979 | if (next.id === 'embodied') window.setTimeout(() => councilPromptRef.current?.focus({ preventScroll: true }), 120); |
| #980 | }; |
| #981 | |
| #982 | const cycleState = (direction: 1 | -1) => { |
| #983 | const index = CHAMBER_CYCLE_ORDER.indexOf(normalizedChamberActive(activeStateId)); |
| #984 | const next = (Math.max(0, index) + direction + CHAMBER_CYCLE_ORDER.length) % CHAMBER_CYCLE_ORDER.length; |
| #985 | selectState(CHAMBER_CYCLE_ORDER[next]); |
| #986 | }; |
| #987 | |
| #988 | const sendMessage = async () => { |
| #989 | if (isDemo || !input.trim() || loading) return; |
| #990 | const prompt = input.trim(); |
| #991 | const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`; |
| #992 | const at = new Date().toISOString(); |
| #993 | const sessionId = await ensureChatSession(prompt); |
| #994 | const userTurn: ChatTurn = { id: `${id}-user`, role: 'user', content: prompt, at, model }; |
| #995 | const assistantTurn: ChatTurn = { id: `${id}-assistant`, role: 'assistant', content: 'Aethon is thinking...', at, pending: true, model }; |
| #996 | setInput(''); |
| #997 | setLoading(true); |
| #998 | setPresence('thinking'); |
| #999 | setChatTranscript(prev => [...prev, userTurn, assistantTurn].slice(-40)); |
| #1000 | setResponse(''); |
| #1001 | try { |
| #1002 | const streamed = await streamSharedChat({ message: prompt, history: [], session_id: sessionId, model, feature: `osv3_state_${activeStateId}` }, partial => { |
| #1003 | setResponse(partial); |
| #1004 | setChatTranscript(prev => prev.map(turn => turn.id === assistantTurn.id ? { ...turn, content: partial, pending: false } : turn)); |
| #1005 | }); |
| #1006 | const rendered = streamed.output || 'Aethon completed without returned text.'; |
| #1007 | const calledModel = String((streamed.meta as any)?.model || model || '').trim(); |
| #1008 | setChatTranscript(prev => prev.map(turn => turn.id === assistantTurn.id ? { ...turn, content: rendered, pending: false, model: calledModel || turn.model } : turn)); |
| #1009 | setChatHistory(prev => [{ id, at, query: prompt, answer: rendered, sessionId }, ...prev].slice(0, 60)); |
| #1010 | setPresence('speaking'); |
| #1011 | void hydrateChatHistory(sessionId); |
| #1012 | } catch (error: any) { |
| #1013 | setChatTranscript(prev => prev.map(turn => turn.id === assistantTurn.id ? { ...turn, content: `Error: ${error?.message ?? 'unknown'}`, pending: false } : turn)); |
| #1014 | setPresence('idle'); |
| #1015 | } finally { |
| #1016 | setLoading(false); |
| #1017 | window.setTimeout(() => setPresence('idle'), 2200); |
| #1018 | } |
| #1019 | }; |
| #1020 | |
| #1021 | const submitDelegationTask = async () => { |
| #1022 | if (isDemo || !taskGoal.trim() || submittingTask) return; |
| #1023 | setSubmittingTask(true); |
| #1024 | setTaskStatus('Handing the goal to Aethon...'); |
| #1025 | setPresence('thinking'); |
| #1026 | try { |
| #1027 | const result = await sameOriginFetch('/api/delegation-task', { |
| #1028 | method: 'POST', |
| #1029 | headers: { 'Content-Type': 'application/json' }, |
| #1030 | body: JSON.stringify({ goal: taskGoal.trim() }), |
| #1031 | }).then(readJsonOrThrow); |
| #1032 | if (!result?.ok) throw new Error(result?.error || result?.summary || 'Harness did not accept the goal.'); |
| #1033 | setTaskStatus(result.summary || 'Goal accepted. Watch the activity stream and approvals.'); |
| #1034 | setTaskGoal(''); |
| #1035 | setPresence('working'); |
| #1036 | } catch (error: any) { |
| #1037 | setTaskStatus(error?.message || 'Delegation failed.'); |
| #1038 | setPresence('idle'); |
| #1039 | } finally { |
| #1040 | setSubmittingTask(false); |
| #1041 | } |
| #1042 | }; |
| #1043 | |
| #1044 | const runCouncilPrompt = async (promptText: string, clearPrompt = false) => { |
| #1045 | if (isDemo || !promptText.trim() || conveningCouncil) return; |
| #1046 | setConveningCouncil(true); |
| #1047 | setCouncilStatus('Convening the room...'); |
| #1048 | setActiveStateId('embodied'); |
| #1049 | setActiveCouncilRoom('convene-all'); |
| #1050 | setPresence('thinking'); |
| #1051 | try { |
| #1052 | const result = await sameOriginFetch('/api/council-bus', { |
| #1053 | method: 'POST', |
| #1054 | headers: { 'Content-Type': 'application/json' }, |
| #1055 | body: JSON.stringify({ prompt: promptText.trim() }), |
| #1056 | }).then(readJsonOrThrow); |
| #1057 | if (Array.isArray(result?.messages)) setCouncilMessages(result.messages); |
| #1058 | if (clearPrompt) setCouncilPrompt(''); |
| #1059 | setCouncilStatus('Council response landed.'); |
| #1060 | setPresence('speaking'); |
| #1061 | } catch (error: any) { |
| #1062 | setCouncilStatus(error?.message || 'Council failed to convene.'); |
| #1063 | setPresence('idle'); |
| #1064 | } finally { |
| #1065 | setConveningCouncil(false); |
| #1066 | } |
| #1067 | }; |
| #1068 | |
| #1069 | const conveneCouncil = async () => { |
| #1070 | await runCouncilPrompt(councilPrompt.trim(), true); |
| #1071 | }; |
| #1072 | |
| #1073 | const mastermindContent = async (content: string, label = 'Aethon response') => { |
| #1074 | if (isDemo || conveningCouncil || !content.trim()) return; |
| #1075 | const prompt = [ |
| #1076 | `Mastermind this ${label}.`, |
| #1077 | 'Convene all eight seats. Debate accuracy, missing context, risk of drift, and the strongest synthesis.', |
| #1078 | 'Embodied concludes with the best answer after the room speaks.', |
| #1079 | '', |
| #1080 | content.trim(), |
| #1081 | ].join('\n'); |
| #1082 | setCouncilPrompt(prompt); |
| #1083 | await runCouncilPrompt(prompt, false); |
| #1084 | }; |
| #1085 | |
| #1086 | const sendCouncilSeatMessage = async () => { |
| #1087 | if (isDemo || !seatPrompt.trim() || seatChatLoading) return; |
| #1088 | const agent = councilAgents.find(item => item.id === selectedState.seatId); |
| #1089 | if (!agent) return; |
| #1090 | const prompt = seatPrompt.trim(); |
| #1091 | const id = `${agent.id}-${Date.now()}-${Math.random().toString(36).slice(2)}`; |
| #1092 | const at = new Date().toISOString(); |
| #1093 | const userTurn: ChatTurn = { id: `${id}-user`, role: 'user', content: prompt, at, model: agent.brain }; |
| #1094 | const assistantTurn: ChatTurn = { id: `${id}-assistant`, role: 'assistant', content: `${agent.name} is answering...`, at, pending: true, model: agent.brain }; |
| #1095 | setSeatPrompt(''); |
| #1096 | setSeatChatLoading(true); |
| #1097 | setSeatChatStatus(`${agent.name} is answering through ${agent.brain}.`); |
| #1098 | setSeatChatTranscript(prev => [...prev, userTurn, assistantTurn].slice(-40)); |
| #1099 | setPresence('thinking'); |
| #1100 | try { |
| #1101 | const sessionId = await ensureChatSession(`Council direct: ${agent.name}`); |
| #1102 | const streamed = await streamSharedChat({ |
| #1103 | message: `[Direct Council channel: ${agent.name} / ${agent.role} / model=${agent.brain}]\n${prompt}`, |
| #1104 | history: [], |
| #1105 | session_id: sessionId, |
| #1106 | model: agent.brain, |
| #1107 | feature: `council_direct_${agent.id}`, |
| #1108 | }, partial => { |
| #1109 | setSeatChatTranscript(prev => prev.map(turn => turn.id === assistantTurn.id ? { ...turn, content: partial, pending: false } : turn)); |
| #1110 | }); |
| #1111 | const rendered = streamed.output || `${agent.name} completed without returned text.`; |
| #1112 | const calledModel = String((streamed.meta as any)?.model || agent.brain || '').trim(); |
| #1113 | setSeatChatTranscript(prev => prev.map(turn => turn.id === assistantTurn.id ? { ...turn, content: rendered, pending: false, model: calledModel || turn.model } : turn)); |
| #1114 | setSeatChatStatus(`Direct channel complete. ${calledModel ? `Model called: ${calledModel}` : `Brain: ${agent.brain}`}`); |
| #1115 | setPresence('speaking'); |
| #1116 | } catch (error: any) { |
| #1117 | setSeatChatTranscript(prev => prev.map(turn => turn.id === assistantTurn.id ? { ...turn, content: `Error: ${error?.message ?? 'unknown'}`, pending: false } : turn)); |
| #1118 | setSeatChatStatus(error?.message || 'Direct channel failed.'); |
| #1119 | setPresence('idle'); |
| #1120 | } finally { |
| #1121 | setSeatChatLoading(false); |
| #1122 | window.setTimeout(() => setPresence('idle'), 2200); |
| #1123 | } |
| #1124 | }; |
| #1125 | |
| #1126 | const submitScrapeSurface = async () => { |
| #1127 | if (isDemo || submittingScrape) return; |
| #1128 | const urls = scrapeUrls.split(/\r?\n|,/g).map(item => item.trim()).filter(Boolean); |
| #1129 | if (!urls.length) { |
| #1130 | setScrapeStatus('Paste at least one channel or video URL.'); |
| #1131 | scrapeInputRef.current?.focus({ preventScroll: true }); |
| #1132 | return; |
| #1133 | } |
| #1134 | setSubmittingScrape(true); |
| #1135 | setScrapeStatus('Submitting source ingest to the delegation harness...'); |
| #1136 | setPresence('working'); |
| #1137 | try { |
| #1138 | const goal = [ |
| #1139 | 'Run the overnight_multichannel_scrape corpus-only path for these sources.', |
| #1140 | 'Mechanical only: enumerate, transcript, OCR keyframes where needed, embed to source-tagged vaults, delete temporary media after extraction, and stream progress to the activity stream.', |
| #1141 | 'Do not run Opus analysis. Do not edit framework files.', |
| #1142 | 'Sources:', |
| #1143 | ...urls.map(url => `- ${url}`), |
| #1144 | ].join('\n'); |
| #1145 | const result = await sameOriginFetch('/api/delegation-task', { |
| #1146 | method: 'POST', |
| #1147 | headers: { 'Content-Type': 'application/json' }, |
| #1148 | body: JSON.stringify({ goal }), |
| #1149 | }).then(readJsonOrThrow); |
| #1150 | if (!result?.ok) throw new Error(result?.error || result?.summary || 'Harness did not accept scrape goal.'); |
| #1151 | setScrapeStatus(result.summary || 'Scrape accepted. Watch the activity stream.'); |
| #1152 | setTaskStatus('Scrape goal accepted. Watch the activity stream.'); |
| #1153 | } catch (error: any) { |
| #1154 | setScrapeStatus(error?.message || 'Scrape submission failed.'); |
| #1155 | setPresence('idle'); |
| #1156 | } finally { |
| #1157 | setSubmittingScrape(false); |
| #1158 | } |
| #1159 | }; |
| #1160 | |
| #1161 | if (!isDemo && isAuthError(meError)) { |
| #1162 | return ( |
| #1163 | <main className="min-h-screen px-4 py-6 md:px-8"> |
| #1164 | <section className="card mx-auto max-w-[900px] p-5"> |
| #1165 | <h1 className="card-header text-lg">Refreshing your Living.OS session</h1> |
| #1166 | <p className="mt-2 text-sm leading-6 text-[color:var(--text-2)]">Redirecting through the TheLiving.AI login bridge now.</p> |
| #1167 | </section> |
| #1168 | </main> |
| #1169 | ); |
| #1170 | } |
| #1171 | |
| #1172 | const selectedState = CHARACTER_STATES.find(item => item.id === activeStateId) ?? CHARACTER_STATES[0]; |
| #1173 | const displayName = isDemo ? 'Demo' : (me?.displayName || 'Operator'); |
| #1174 | const spendBudget = spend?.budget_diem ?? 6.03; |
| #1175 | const spendUsed = spend?.spent_diem ?? 0; |
| #1176 | const spendPercent = Math.min(100, spend?.percent_used ?? 0); |
| #1177 | const spendItems = isDemo ? [] : ([...(spend?.items ?? [])] as any[]).reverse(); |
| #1178 | const localJobs = isDemo ? null : spend?.local_jobs; |
| #1179 | const localSaved = Number(localJobs?.estimated_diem_saved ?? 0); |
| #1180 | const pendingApprovals = approvals?.decisions?.length ?? 0; |
| #1181 | const activeTasks = (tasks?.inProgress?.length ?? 0) + (tasks?.queued?.length ?? 0); |
| #1182 | const completedTasks = tasks?.totals?.completed ?? tasks?.completed?.length ?? 0; |
| #1183 | const totalTrackedWork = activeTasks + pendingApprovals + completedTasks; |
| #1184 | const servicesUp = status?.services?.filter((svc: any) => svc.status === 'up').length ?? 0; |
| #1185 | const serviceTotal = status?.services?.length ?? 0; |
| #1186 | const statusServices = isDemo ? [] : ((status?.services ?? []) as any[]); |
| #1187 | const downServices = statusServices.filter((svc: any) => svc.status !== 'up'); |
| #1188 | const vaultNodes = vaultGraph?.nodes?.length ?? 0; |
| #1189 | const vaultEdges = vaultGraph?.edges?.length ?? 0; |
| #1190 | const councilAgents: CouncilAgent[] = isDemo ? COUNCIL_FALLBACK : (councilState?.roster ?? COUNCIL_FALLBACK); |
| #1191 | const visibleCouncilMessages = (activeCouncilRoom === 'convene-all' |
| #1192 | ? councilMessages |
| #1193 | : councilMessages.filter(message => message.room === activeCouncilRoom || message.agentId === activeCouncilRoom) |
| #1194 | ).slice(-80); |
| #1195 | const graphNodesForView = Array.isArray(vaultGraph?.nodes) ? vaultGraph.nodes : []; |
| #1196 | const graphEdgesForView = Array.isArray(vaultGraph?.edges) ? vaultGraph.edges : []; |
| #1197 | const stateStyle = { |
| #1198 | '--state-color': selectedState.color, |
| #1199 | '--state-soft': selectedState.soft, |
| #1200 | } as CSSProperties; |
| #1201 | const motionState: MotionState = isDemo |
| #1202 | ? 'idle' |
| #1203 | : loading || conveningCouncil || seatChatLoading |
| #1204 | ? 'thinking' |
| #1205 | : activeTasks > 0 || submittingTask || submittingScrape |
| #1206 | ? 'working' |
| #1207 | : pendingApprovals > 0 |
| #1208 | ? 'speaking' |
| #1209 | : presence; |
| #1210 | |
| #1211 | const selectedAgent = activeCouncilRoom === 'convene-all' |
| #1212 | ? undefined |
| #1213 | : councilAgents.find(agent => agent.id === activeCouncilRoom); |
| #1214 | const selectedSeatTurns = selectedAgent |
| #1215 | ? seatChatTranscript.filter(turn => turn.id.startsWith(`${selectedAgent.id}-`)).slice(-8) |
| #1216 | : []; |
| #1217 | const focusProgress = totalTrackedWork ? Math.round((completedTasks / totalTrackedWork) * 100) : 0; |
| #1218 | const runningNowRows = isDemo ? [] : ((status?.runningNow ?? []) as any[]); |
| #1219 | const streamRows = isDemo ? [] : ((toolFeed?.recent ?? []) as any[]); |
| #1220 | const statusEventRows = isDemo ? [] : ((status?.recentEvents ?? []) as any[]); |
| #1221 | const rawOpsActivityRows: OpsActivityRow[] = [ |
| #1222 | ...(toolFeed?.message ? [{ |
| #1223 | id: `stream-current-${toolFeed.timestamp ?? 'now'}`, |
| #1224 | actor: 'Live tool feed', |
| #1225 | message: toolFeed.message, |
| #1226 | at: toolFeed.timestamp, |
| #1227 | color: selectedState.color, |
| #1228 | status: toolFeed.type, |
| #1229 | source: 'task-stream-current', |
| #1230 | detail: toolFeed, |
| #1231 | }] : []), |
| #1232 | ...runningNowRows.map((row: any) => ({ |
| #1233 | id: `running-${row.id}`, |
| #1234 | actor: String(row.label ?? 'Running work'), |
| #1235 | message: String(row.message ?? row.status ?? 'working'), |
| #1236 | at: row.updated_at, |
| #1237 | color: '#69aaf8', |
| #1238 | status: String(row.status ?? 'running'), |
| #1239 | source: String(row.source ?? 'running-now'), |
| #1240 | detail: row, |
| #1241 | })), |
| #1242 | ...streamRows.map((row: any, index: number) => ({ |
| #1243 | id: `stream-${row.ts ?? row.timestamp ?? index}`, |
| #1244 | actor: row.source === 'glasses' ? 'Glasses' : 'Agentic task', |
| #1245 | message: String(row.message ?? row.event ?? 'progress'), |
| #1246 | at: row.ts ?? row.timestamp, |
| #1247 | color: row.source === 'glasses' ? '#e7c76f' : '#69aaf8', |
| #1248 | status: String(row.event_type ?? row.event ?? 'progress'), |
| #1249 | source: String(row.source ?? 'task-stream'), |
| #1250 | detail: row, |
| #1251 | })), |
| #1252 | ...statusEventRows.map((row: any, index: number) => ({ |
| #1253 | id: `status-event-${row.id ?? index}`, |
| #1254 | actor: String(row.label ?? 'Aethon event'), |
| #1255 | message: String(row.message ?? row.status ?? 'event'), |
| #1256 | at: row.ts, |
| #1257 | color: String(row.label ?? '').toLowerCase().includes('glasses') ? '#e7c76f' : '#86d88d', |
| #1258 | status: String(row.status ?? row.event_type ?? 'event'), |
| #1259 | source: String(row.source ?? row.event_type ?? 'status-event'), |
| #1260 | detail: row.detail ?? row, |
| #1261 | })), |
| #1262 | ]; |
| #1263 | const opsActivityRows = groupOpsRows(rawOpsActivityRows.filter(row => !isGlassesRow(row))).slice(0, 18); |
| #1264 | const glassesLogRows = groupOpsRows(rawOpsActivityRows.filter(isGlassesRow)).slice(0, 14); |
| #1265 | const liveWorkRows = groupOpsRows([ |
| #1266 | ...runningNowRows.map((row: any) => ({ |
| #1267 | id: `live-running-${row.id}`, |
| #1268 | actor: String(row.label ?? 'Running work'), |
| #1269 | message: String(row.message ?? row.status ?? 'working'), |
| #1270 | at: row.updated_at, |
| #1271 | color: '#69aaf8', |
| #1272 | status: String(row.status ?? 'running'), |
| #1273 | source: String(row.source ?? 'running-now'), |
| #1274 | detail: row, |
| #1275 | })), |
| #1276 | ...(tasks?.inProgress ?? []).map((task: any) => ({ |
| #1277 | id: `live-task-${task.id}`, |
| #1278 | actor: 'Active task', |
| #1279 | message: `${task.title ?? task.id}${task.liveStatus ? ` · ${task.liveStatus}` : ''}`, |
| #1280 | at: Number.isFinite(Number(task.mtime)) ? new Date(Number(task.mtime)).toISOString() : undefined, |
| #1281 | color: '#86d88d', |
| #1282 | status: 'in_progress', |
| #1283 | source: task.file ?? 'aethon-tasks', |
| #1284 | detail: task, |
| #1285 | })), |
| #1286 | ...(tasks?.queued ?? []).map((task: any) => ({ |
| #1287 | id: `live-queued-${task.id}`, |
| #1288 | actor: 'Queued task', |
| #1289 | message: String(task.title ?? task.id), |
| #1290 | at: Number.isFinite(Number(task.mtime)) ? new Date(Number(task.mtime)).toISOString() : undefined, |
| #1291 | color: '#e7c76f', |
| #1292 | status: 'queued', |
| #1293 | source: task.file ?? 'aethon-tasks', |
| #1294 | detail: task, |
| #1295 | })), |
| #1296 | ...(tasks?.workerStatus ?? []).map((worker: any) => ({ |
| #1297 | id: `live-worker-${worker.id}`, |
| #1298 | actor: String(worker.title ?? 'Background worker'), |
| #1299 | message: String(worker.message ?? worker.status ?? 'watching'), |
| #1300 | at: worker.updated_at, |
| #1301 | color: worker.id === 'catalog-hunter' ? '#e7c76f' : '#69aaf8', |
| #1302 | status: String(worker.status ?? 'watching'), |
| #1303 | source: String(worker.source ?? 'worker-progress'), |
| #1304 | detail: worker.detail ?? worker, |
| #1305 | })), |
| #1306 | ...(toolFeed?.message ? [{ |
| #1307 | id: `live-stream-${toolFeed.timestamp ?? 'now'}`, |
| #1308 | actor: 'Current tool call', |
| #1309 | message: String(toolFeed.message), |
| #1310 | at: toolFeed.timestamp, |
| #1311 | color: selectedState.color, |
| #1312 | status: String(toolFeed.type ?? 'progress'), |
| #1313 | source: 'task-stream-current', |
| #1314 | detail: toolFeed, |
| #1315 | }] : []), |
| #1316 | ...streamRows.filter((row: any) => row.source !== 'glasses').map((row: any, index: number) => ({ |
| #1317 | id: `live-stream-row-${row.ts ?? row.timestamp ?? index}`, |
| #1318 | actor: 'Progress event', |
| #1319 | message: String(row.message ?? row.event ?? 'progress'), |
| #1320 | at: row.ts ?? row.timestamp, |
| #1321 | color: '#69aaf8', |
| #1322 | status: String(row.event_type ?? row.event ?? 'progress'), |
| #1323 | source: String(row.file ?? row.source ?? 'agentic-audit'), |
| #1324 | detail: row, |
| #1325 | })), |
| #1326 | ]).slice(0, 18); |
| #1327 | const systemLogRows = isDemo ? [] : [ |
| #1328 | ...runningNowRows.map((row: any) => ({ |
| #1329 | label: String(row.label ?? 'Running work'), |
| #1330 | value: String(row.message ?? row.status ?? 'working'), |
| #1331 | detail: row, |
| #1332 | })), |
| #1333 | ...statusEventRows.map((row: any) => ({ |
| #1334 | label: String(row.label ?? row.event_type ?? 'Event'), |
| #1335 | value: `${formatEventTime(row.ts)} · ${String(row.message ?? row.status ?? 'event')}`, |
| #1336 | detail: row.detail ?? row, |
| #1337 | })), |
| #1338 | ...statusServices.map((service: any) => ({ |
| #1339 | label: String(service.label ?? service.id ?? 'Service'), |
| #1340 | value: `${String(service.status ?? 'watching').toUpperCase()}${service.detail ? ` · ${service.detail}` : service.error ? ` · ${service.error}` : ''}`, |
| #1341 | detail: service, |
| #1342 | })), |
| #1343 | ].slice(0, 24); |
| #1344 | |
| #1345 | return ( |
| #1346 | <main className="council-cockpit-shell min-h-screen" style={stateStyle} data-osv4-council-cockpit="meshy-glb-council"> |
| #1347 | <header className="osv4-topbar"> |
| #1348 | <section className="osv4-brand"> |
| #1349 | <img src="/brand/aethon-emblem.png" alt="" /> |
| #1350 | <div> |
| #1351 | <span>The Living OS V4</span> |
| #1352 | <strong>AETHON</strong> |
| #1353 | <em>Council Cockpit</em> |
| #1354 | </div> |
| #1355 | </section> |
| #1356 | |
| #1357 | <section className="osv4-focus-card"> |
| #1358 | <span>Current Focus</span> |
| #1359 | <label className="mission-inline-editor"> |
| #1360 | <input |
| #1361 | value={isDemo ? 'Build Living Council Pipeline' : missionGoal} |
| #1362 | onChange={event => setMissionGoal(event.target.value)} |
| #1363 | disabled={isDemo} |
| #1364 | aria-label="Current Focus" |
| #1365 | /> |
| #1366 | </label> |
| #1367 | <div className="focus-stats-row"> |
| #1368 | <button type="button" onClick={() => selectState('living')} title="Open the active work panel"> |
| #1369 | <strong>{activeTasks}</strong><span>Active tasks</span> |
| #1370 | </button> |
| #1371 | <button type="button" onClick={() => selectState('black')} title="Open the approval queue"> |
| #1372 | <strong>{pendingApprovals}</strong><span>Waiting approval</span> |
| #1373 | </button> |
| #1374 | <button type="button" onClick={() => selectState('living')} title="Open completed work"> |
| #1375 | <strong>{completedTasks}</strong><span>Completed records</span> |
| #1376 | </button> |
| #1377 | <em title="Completed work divided by active tasks plus waiting approvals plus completed records."> |
| #1378 | {focusProgress}% tracked work complete |
| #1379 | </em> |
| #1380 | <div className="focus-progress-track"><i style={{ width: `${focusProgress}%` }} /></div> |
| #1381 | </div> |
| #1382 | </section> |
| #1383 | |
| #1384 | <section className="osv4-operator-strip"> |
| #1385 | <div><span>Operator</span><strong>{isDemo ? 'Demo' : displayName}</strong></div> |
| #1386 | <div><span>DIEM</span><strong>{isDemo ? 'Masked' : formatDiem(spendUsed)}</strong></div> |
| #1387 | <details className="spend-dropdown" data-spend-dropdown="b1-tracker"> |
| #1388 | <summary>SPEND</summary> |
| #1389 | <div className="spend-popover"> |
| #1390 | <strong>{isDemo ? 'Private spend masked' : `${formatDiem(spendUsed)} / ${Number(spendBudget).toFixed(2)} DIEM`}</strong> |
| #1391 | <em>{isDemo ? 'Demo mode does not request the ledger.' : `${spendPercent.toFixed(1)}% used · ${formatDiem(localSaved)} local DIEM saved · reset ${formatCountdown(spend?.epoch?.seconds_until_reset)}`}</em> |
| #1392 | {isDemo ? ( |
| #1393 | <div className="demo-mask">Spend rows are not sent to the demo client.</div> |
| #1394 | ) : ( |
| #1395 | <div className="capacity-ledger-list"> |
| #1396 | {spendItems.slice(0, 8).map((item: any) => ( |
| #1397 | <article key={item.call_id ?? `${item.timestamp}-${item.model}`} className="capacity-ledger-row"> |
| #1398 | <div> |
| #1399 | <strong>{item.task ?? 'Inference call'}</strong> |
| #1400 | <span>{item.model ?? 'model'} · {formatTime(item.timestamp)}</span> |
| #1401 | </div> |
| #1402 | <b>{formatDiem(item.diem_debited)} DIEM</b> |
| #1403 | </article> |
| #1404 | ))} |
| #1405 | {!spendItems.length && <div className="empty-state">No paid DIEM debits in this epoch yet.</div>} |
| #1406 | </div> |
| #1407 | )} |
| #1408 | </div> |
| #1409 | </details> |
| #1410 | <div><span>Models</span><strong>{modelStatus ? 'Active' : 'Online'} <i className="online-dot" /></strong></div> |
| #1411 | <div className="mode-toggle" aria-label="OS mode"> |
| #1412 | {(['operator', 'demo'] as const).map(mode => ( |
| #1413 | <button key={mode} type="button" onClick={() => setOsMode(mode)} className={osMode === mode ? 'active' : ''}> |
| #1414 | {mode === 'operator' ? 'Operator' : 'Demo'} |
| #1415 | </button> |
| #1416 | ))} |
| #1417 | </div> |
| #1418 | <button type="button" className="command-link" onClick={() => setShowSystemLogs(value => !value)} aria-label="Toggle V4 system logs">•••</button> |
| #1419 | </section> |
| #1420 | </header> |
| #1421 | |
| #1422 | <section className="osv4-main-grid"> |
| #1423 | <aside className="osv4-left-rail"> |
| #1424 | <section className="osv4-panel narrative-panel"> |
| #1425 | <div className="panel-title-row"><span>Activity Stream</span><b>Live</b></div> |
| #1426 | <div className="narrative-list"> |
| #1427 | {opsActivityRows.map(row => ( |
| #1428 | <ExpandableLogRow key={row.id} row={row} /> |
| #1429 | ))} |
| #1430 | {!opsActivityRows.length && <div className="empty-state">No live agentic, ingest, or system events yet. Glasses have their own log on the right.</div>} |
| #1431 | </div> |
| #1432 | </section> |
| #1433 | |
| #1434 | <section className="osv4-panel approval-panel"> |
| #1435 | <div className="panel-title-row"><span>Approval Queue</span><b>{pendingApprovals}</b></div> |
| #1436 | {isDemo ? <div className="demo-mask">Approval cards are operator-only.</div> : <ApprovalQueuePanel compact />} |
| #1437 | </section> |
| #1438 | |
| #1439 | <section className="osv4-panel site-status-panel"> |
| #1440 | <div className="panel-title-row"><span>Site Status</span><b>{servicesUp}/{serviceTotal || '–'}</b></div> |
| #1441 | <ul> |
| #1442 | {statusServices.slice(0, 10).map((service: any) => ( |
| #1443 | <li key={service.id ?? service.label} className={service.status === 'up' ? 'up' : 'down'} title={service.detail ?? service.error ?? ''}> |
| #1444 | <span /> |
| #1445 | <strong>{service.label ?? service.id}</strong> |
| #1446 | <em>{service.status === 'up' ? 'up' : `down${service.error ? ` · ${service.error}` : ''}`}</em> |
| #1447 | </li> |
| #1448 | ))} |
| #1449 | {!statusServices.length && <li className="down"><span /><strong>Status API</strong><em>waiting for real service rows</em></li>} |
| #1450 | </ul> |
| #1451 | {downServices.length > 0 && ( |
| #1452 | <p className="status-down-summary"> |
| #1453 | Down: {downServices.map((service: any) => service.label ?? service.id).join(', ')} |
| #1454 | </p> |
| #1455 | )} |
| #1456 | <button type="button" className="full-logs-button" onClick={() => setShowSystemLogs(value => !value)}> |
| #1457 | {showSystemLogs ? 'Hide Recent Logs' : 'Recent System Logs'} |
| #1458 | </button> |
| #1459 | {showSystemLogs && ( |
| #1460 | <div className="v4-system-logs" data-v4-native-logs="true"> |
| #1461 | {isDemo ? ( |
| #1462 | <div className="demo-mask">System log details are operator-only.</div> |
| #1463 | ) : systemLogRows.length ? systemLogRows.map(row => ( |
| #1464 | <details key={`${row.label}-${row.value}`} className="system-log-row"> |
| #1465 | <summary> |
| #1466 | <span>{row.label}</span> |
| #1467 | <strong>{row.value}</strong> |
| #1468 | </summary> |
| #1469 | <pre>{stringifyLogDetail(row.detail ?? row)}</pre> |
| #1470 | </details> |
| #1471 | )) : ( |
| #1472 | <div className="empty-state">No recent OS events found. Sources: /home/kingbau/logs/theliving-os/events.jsonl and book_source_audit events.</div> |
| #1473 | )} |
| #1474 | </div> |
| #1475 | )} |
| #1476 | </section> |
| #1477 | </aside> |
| #1478 | |
| #1479 | <section className="osv4-center-stage"> |
| #1480 | <CouncilChamber |
| #1481 | activeStateId={activeStateId} |
| #1482 | selectState={selectState} |
| #1483 | cycleState={cycleState} |
| #1484 | motionState={motionState} |
| #1485 | animationsEnabled={glbAnimationsEnabled} |
| #1486 | /> |
| #1487 | |
| #1488 | <section className="selected-chamber-panel" data-selected-chamber={selectedState.id}> |
| #1489 | <div className="selected-chamber-heading"> |
| #1490 | <div> |
| #1491 | <span>{selectedState.chamberName}</span> |
| #1492 | <strong>{selectedState.chamberSubtitle}</strong> |
| #1493 | </div> |
| #1494 | <div className="chamber-heading-actions"> |
| #1495 | <button type="button" className="btn-ghost voice-mode-button" onClick={() => setGlbAnimationsEnabled(value => !value)}> |
| #1496 | {glbAnimationsEnabled ? 'Animations On' : 'Animations Off'} |
| #1497 | </button> |
| #1498 | <button type="button" className="btn-ghost voice-mode-button"><Mic size={14} /> Voice Mode</button> |
| #1499 | </div> |
| #1500 | </div> |
| #1501 | |
| #1502 | {activeStateId === 'embodied' ? ( |
| #1503 | <CouncilRoom |
| #1504 | isDemo={isDemo} |
| #1505 | agents={councilAgents} |
| #1506 | activeCouncilRoom={activeCouncilRoom} |
| #1507 | setActiveCouncilRoom={setActiveCouncilRoom} |
| #1508 | visibleMessages={visibleCouncilMessages} |
| #1509 | councilPrompt={councilPrompt} |
| #1510 | setCouncilPrompt={setCouncilPrompt} |
| #1511 | councilPromptRef={councilPromptRef} |
| #1512 | conveningCouncil={conveningCouncil} |
| #1513 | conveneCouncil={conveneCouncil} |
| #1514 | councilStatus={councilStatus} |
| #1515 | selectedAgent={selectedAgent} |
| #1516 | seatPrompt={seatPrompt} |
| #1517 | setSeatPrompt={setSeatPrompt} |
| #1518 | seatPromptRef={seatPromptRef} |
| #1519 | sendCouncilSeatMessage={sendCouncilSeatMessage} |
| #1520 | seatChatLoading={seatChatLoading} |
| #1521 | seatChatStatus={seatChatStatus} |
| #1522 | selectedSeatTurns={selectedSeatTurns} |
| #1523 | /> |
| #1524 | ) : (activeStateId === 'archivist' || activeStateId === 'harbinger') ? ( |
| #1525 | <div className="memory-vault-chamber"> |
| #1526 | <article className="memory-vault-main"> |
| #1527 | <div className="memory-vault-heading"> |
| #1528 | <span>{activeStateId === 'archivist' ? 'Obsidian Graph' : 'Verification Graph'}</span> |
| #1529 | <strong>{vaultNodes} notes · {vaultEdges} edges</strong> |
| #1530 | <em>{activeStateId === 'archivist' ? 'Archivist opens the whole vault instead of a postage-stamp preview.' : 'Harbinger checks claims against the live memory map.'}</em> |
| #1531 | </div> |
| #1532 | {isDemo ? ( |
| #1533 | <div className="demo-mask">Vault graph, case names, and private memory are not requested in demo mode.</div> |
| #1534 | ) : ( |
| #1535 | <> |
| #1536 | <input value={graphSearch} onChange={event => setGraphSearch(event.target.value)} placeholder="Find a vault note" /> |
| #1537 | <VaultGraph3D |
| #1538 | nodes={graphSearch ? graphNodesForView.filter((node: any) => JSON.stringify(node).toLowerCase().includes(graphSearch.toLowerCase())) : graphNodesForView} |
| #1539 | edges={graphEdgesForView} |
| #1540 | onPrompt={setInput} |
| #1541 | /> |
| #1542 | </> |
| #1543 | )} |
| #1544 | </article> |
| #1545 | <article className="memory-vault-side"> |
| #1546 | <span>Recent Conversations</span> |
| #1547 | {isDemo ? ( |
| #1548 | <div className="demo-mask">History stays private.</div> |
| #1549 | ) : ( |
| #1550 | <> |
| #1551 | <input value={historySearch} onChange={event => setHistorySearch(event.target.value)} placeholder="Search prior chats" /> |
| #1552 | <div className="history-list"> |
| #1553 | {chatHistory.filter(item => !historySearch || `${item.query} ${item.answer}`.toLowerCase().includes(historySearch.toLowerCase())).slice(0, 12).map((item, index) => ( |
| #1554 | <button key={item.id} type="button" style={statusStyle(index)} onClick={() => { setInput(item.query); setResponse(item.answer); selectState('living'); }}> |
| #1555 | <strong>{item.query.slice(0, 90)}</strong> |
| #1556 | <span>{formatTime(item.at)}</span> |
| #1557 | </button> |
| #1558 | ))} |
| #1559 | </div> |
| #1560 | </> |
| #1561 | )} |
| #1562 | </article> |
| #1563 | </div> |
| #1564 | ) : ( |
| #1565 | <div className="chamber-panel-grid"> |
| #1566 | <article> |
| #1567 | <span>Today's Briefing</span> |
| #1568 | <ul>{selectedState.briefing.map(item => <li key={item}>{item}</li>)}</ul> |
| #1569 | </article> |
| #1570 | <article> |
| #1571 | <span>Active Tasks</span> |
| #1572 | {isDemo ? <div className="demo-mask">Task payloads are masked.</div> : <AethonTasks section="active" />} |
| #1573 | </article> |
| #1574 | <article> |
| #1575 | <span>Daily Summary</span> |
| #1576 | <p>{selectedState.oneLine}</p> |
| #1577 | {(activeStateId === 'black' || activeStateId === 'reaper' || activeStateId === 'scribe') && ( |
| #1578 | isDemo ? <div className="demo-mask">Queue details are private.</div> : <ApprovalQueuePanel compact /> |
| #1579 | )} |
| #1580 | </article> |
| #1581 | </div> |
| #1582 | )} |
| #1583 | </section> |
| #1584 | </section> |
| #1585 | |
| #1586 | <aside className="osv4-right-rail"> |
| #1587 | <section className="osv4-panel conversation-portal"> |
| #1588 | <div className="panel-title-row"> |
| #1589 | <span>Conversation</span> |
| #1590 | <b>{activeStateId === 'embodied' ? 'Council' : selectedState.label}</b> |
| #1591 | </div> |
| #1592 | <div className="conversation-thread"> |
| #1593 | {activeStateId === 'embodied' ? ( |
| #1594 | visibleCouncilMessages.length ? visibleCouncilMessages.slice(-8).map((message, index) => ( |
| #1595 | <article key={message.id} className="conversation-council-message" style={{ '--stagger-index': index } as CSSProperties}> |
| #1596 | <header><strong>{message.agentName}</strong><time>{formatTime(message.ts)}</time></header> |
| #1597 | <p>{message.content}</p> |
| #1598 | {message.kind !== 'synthesis' && ( |
| #1599 | <button type="button" className="mastermind-chip" onClick={() => mastermindContent(message.content, `${message.agentName} council message`)} disabled={isDemo || conveningCouncil}> |
| #1600 | Mastermind this |
| #1601 | </button> |
| #1602 | )} |
| #1603 | </article> |
| #1604 | )) : ( |
| #1605 | <div className="empty-state">The Council is quiet. Ask the Mastermind to convene.</div> |
| #1606 | ) |
| #1607 | ) : activeStateId === 'living' ? ( |
| #1608 | chatTranscript.length ? chatTranscript.map(turn => ( |
| #1609 | <article key={turn.id} className={`state-chat-turn ${turn.role}`}> |
| #1610 | <header><span>{turn.role === 'user' ? 'King' : 'Aethon'}</span>{turn.model && <em>{turn.model}</em>}</header> |
| #1611 | <MessageContent content={turn.content} role={turn.role} /> |
| #1612 | {turn.role === 'assistant' && !turn.pending && ( |
| #1613 | <button type="button" className="mastermind-chip" onClick={() => mastermindContent(turn.content, 'single Aethon response')} disabled={isDemo || conveningCouncil}> |
| #1614 | Mastermind this |
| #1615 | </button> |
| #1616 | )} |
| #1617 | </article> |
| #1618 | )) : <div className="empty-state">No active conversation in this room yet.</div> |
| #1619 | ) : ( |
| #1620 | selectedSeatTurns.length ? selectedSeatTurns.map(turn => ( |
| #1621 | <article key={turn.id} className={`seat-direct-turn ${turn.role}`}> |
| #1622 | <span>{turn.role === 'user' ? 'King' : selectedState.label}</span> |
| #1623 | <p>{turn.content}</p> |
| #1624 | {turn.model && <em>{turn.model}</em>} |
| #1625 | {turn.role === 'assistant' && !turn.pending && ( |
| #1626 | <button type="button" className="mastermind-chip" onClick={() => mastermindContent(turn.content, `${selectedState.label} direct response`)} disabled={isDemo || conveningCouncil}> |
| #1627 | Mastermind this |
| #1628 | </button> |
| #1629 | )} |
| #1630 | </article> |
| #1631 | )) : <div className="empty-state">Direct channel ready for {selectedState.label}.</div> |
| #1632 | )} |
| #1633 | </div> |
| #1634 | |
| #1635 | <div className="input-mode-tabs" aria-label="Input mode"> |
| #1636 | <button type="button" className="active">Text</button> |
| #1637 | <button type="button">Voice</button> |
| #1638 | <button type="button">File</button> |
| #1639 | <button type="button">Image</button> |
| #1640 | </div> |
| #1641 | <div className="conversation-controls"> |
| #1642 | <select value={model} onChange={event => setModel(event.target.value)} className="model-select model-select-primary" aria-label="Chat model selector"> |
| #1643 | {MODELS.map(item => <option key={item.id} value={item.id}>{item.label}</option>)} |
| #1644 | </select> |
| #1645 | <button |
| #1646 | type="button" |
| #1647 | className="btn-ghost mastermind-thread-button" |
| #1648 | onClick={() => mastermindContent(chatTranscript.map(turn => `${turn.role === 'user' ? 'King' : 'Aethon'}: ${turn.content}`).join('\n\n'), 'conversation thread')} |
| #1649 | disabled={isDemo || conveningCouncil || !chatTranscript.length} |
| #1650 | > |
| #1651 | Mastermind thread |
| #1652 | </button> |
| #1653 | <button type="button" className="btn-ghost freestyle-button" title="Freestyle voice-in lands in Stage 4"> |
| #1654 | <Mic size={14} /> Tap to speak |
| #1655 | </button> |
| #1656 | </div> |
| #1657 | |
| #1658 | {activeStateId === 'embodied' ? ( |
| #1659 | <div className="state-input-row"> |
| #1660 | <textarea ref={councilPromptRef} value={councilPrompt} onChange={event => setCouncilPrompt(event.target.value)} placeholder="Ask the Council to deliberate." /> |
| #1661 | <button type="button" className="btn-gold" onClick={conveneCouncil} disabled={isDemo || conveningCouncil || !councilPrompt.trim()}> |
| #1662 | {conveningCouncil ? 'Convening...' : 'Convene'} |
| #1663 | </button> |
| #1664 | </div> |
| #1665 | ) : activeStateId === 'living' ? ( |
| #1666 | <div className="state-input-row"> |
| #1667 | <textarea |
| #1668 | ref={chatInputRef} |
| #1669 | value={input} |
| #1670 | onChange={event => setInput(event.target.value)} |
| #1671 | onKeyDown={event => { |
| #1672 | if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) void sendMessage(); |
| #1673 | }} |
| #1674 | placeholder="Speak with Aethon. Ctrl+Enter sends." |
| #1675 | /> |
| #1676 | <button type="button" className="btn-gold" onClick={sendMessage} disabled={loading || isDemo || !input.trim()}> |
| #1677 | <Send size={14} /> Send |
| #1678 | </button> |
| #1679 | </div> |
| #1680 | ) : ( |
| #1681 | <div className="state-input-row"> |
| #1682 | <textarea ref={seatPromptRef} value={seatPrompt} onChange={event => setSeatPrompt(event.target.value)} placeholder={`Talk to ${selectedState.label}.`} /> |
| #1683 | <button type="button" className="btn-gold" onClick={sendCouncilSeatMessage} disabled={isDemo || seatChatLoading || !seatPrompt.trim()}> |
| #1684 | {seatChatLoading ? 'Sending...' : 'Send'} |
| #1685 | </button> |
| #1686 | </div> |
| #1687 | )} |
| #1688 | {(response || councilStatus || seatChatStatus) && ( |
| #1689 | <div className="latest-response"> |
| #1690 | {response && activeStateId === 'living' ? <MessageContent content={response} role="assistant" /> : (councilStatus || seatChatStatus)} |
| #1691 | </div> |
| #1692 | )} |
| #1693 | </section> |
| #1694 | |
| #1695 | <section className="osv4-panel task-input-card"> |
| #1696 | <div className="panel-title-row"><span>Give Aethon a task</span><b>Gated</b></div> |
| #1697 | <TaskComposer |
| #1698 | isDemo={isDemo} |
| #1699 | value={taskGoal} |
| #1700 | setValue={setTaskGoal} |
| #1701 | inputRef={taskInputRef} |
| #1702 | submitting={submittingTask} |
| #1703 | submit={submitDelegationTask} |
| #1704 | status={taskStatus} |
| #1705 | placeholder="Give Aethon a goal. Effects stop for approval." |
| #1706 | /> |
| #1707 | </section> |
| #1708 | |
| #1709 | <section className="osv4-panel scrape-surface-card" data-scrape-surface="harness"> |
| #1710 | <div className="panel-title-row"><span>Ingest Sources</span><b>Corpus</b></div> |
| #1711 | {isDemo ? ( |
| #1712 | <div className="demo-mask">Source targets are operator-only.</div> |
| #1713 | ) : ( |
| #1714 | <> |
| #1715 | <textarea ref={scrapeInputRef} value={scrapeUrls} onChange={event => setScrapeUrls(event.target.value)} placeholder="Paste channel or video URLs, one per line." /> |
| #1716 | <button type="button" className="btn-gold" onClick={submitScrapeSurface} disabled={submittingScrape || !scrapeUrls.trim()}> |
| #1717 | {submittingScrape ? 'Submitting...' : 'Start scrape'} |
| #1718 | </button> |
| #1719 | {scrapeStatus && <p>{scrapeStatus}</p>} |
| #1720 | </> |
| #1721 | )} |
| #1722 | </section> |
| #1723 | |
| #1724 | <section className="osv4-panel live-work-panel" data-live-work-panel="operator-progress"> |
| #1725 | <div className="panel-title-row"><span>Live Work</span><b>{liveWorkRows.length ? 'Tracking' : 'Idle'}</b></div> |
| #1726 | {isDemo ? ( |
| #1727 | <div className="demo-mask">Live worker details are operator-only.</div> |
| #1728 | ) : ( |
| #1729 | <> |
| #1730 | <div className="live-work-summary"> |
| #1731 | <article> |
| #1732 | <span>Running</span> |
| #1733 | <strong>{runningNowRows.length + (tasks?.inProgress?.length ?? 0)}</strong> |
| #1734 | </article> |
| #1735 | <article> |
| #1736 | <span>Queued</span> |
| #1737 | <strong>{tasks?.queued?.length ?? 0}</strong> |
| #1738 | </article> |
| #1739 | <article> |
| #1740 | <span>Approvals</span> |
| #1741 | <strong>{pendingApprovals}</strong> |
| #1742 | </article> |
| #1743 | </div> |
| #1744 | <div className="live-work-current"> |
| #1745 | <span>Now</span> |
| #1746 | <strong>{toolFeed?.message ?? runningNowRows[0]?.message ?? 'No active tool progress event in the current window.'}</strong> |
| #1747 | {(toolFeed?.timestamp || runningNowRows[0]?.updated_at) && <em>{formatEventTime(toolFeed?.timestamp ?? runningNowRows[0]?.updated_at)}</em>} |
| #1748 | </div> |
| #1749 | <div className="live-work-list"> |
| #1750 | {liveWorkRows.map(row => ( |
| #1751 | <ExpandableLogRow key={`live-${row.id}`} row={row} /> |
| #1752 | ))} |
| #1753 | {!liveWorkRows.length && ( |
| #1754 | <div className="empty-state">No live task rows yet. Scrapes, ingest jobs, delegation steps, approvals, and progress JSONL updates will appear here as they run.</div> |
| #1755 | )} |
| #1756 | </div> |
| #1757 | <div className="live-work-sources"> |
| #1758 | <span>Sources</span> |
| #1759 | {(status?.logSources ?? ['/api/v1/tasks/stream', '/api/aethon-tasks']).slice(0, 4).map((source: string) => ( |
| #1760 | <code key={source}>{source}</code> |
| #1761 | ))} |
| #1762 | </div> |
| #1763 | </> |
| #1764 | )} |
| #1765 | </section> |
| #1766 | |
| #1767 | <section className="osv4-panel glasses-log-panel" data-glasses-log="dedicated"> |
| #1768 | <div className="panel-title-row"><span>King Glasses Log</span><b>{glassesLogRows.length ? 'Live' : 'Quiet'}</b></div> |
| #1769 | {isDemo ? ( |
| #1770 | <div className="demo-mask">Glasses transcripts and wake events are operator-only.</div> |
| #1771 | ) : ( |
| #1772 | <div className="glasses-log-list"> |
| #1773 | {glassesLogRows.map(row => ( |
| #1774 | <ExpandableLogRow key={`glasses-${row.id}`} row={row} /> |
| #1775 | ))} |
| #1776 | {!glassesLogRows.length && <div className="empty-state">No glasses events in the recent log window. New sessions, transcriptions, wake matches, misses, queries, and responses will appear here.</div>} |
| #1777 | </div> |
| #1778 | )} |
| #1779 | </section> |
| #1780 | </aside> |
| #1781 | </section> |
| #1782 | </main> |
| #1783 | ); |
| #1784 | } |
| #1785 | |
| #1786 | function TaskComposer({ |
| #1787 | isDemo, |
| #1788 | value, |
| #1789 | setValue, |
| #1790 | inputRef, |
| #1791 | submitting, |
| #1792 | submit, |
| #1793 | status, |
| #1794 | placeholder, |
| #1795 | }: { |
| #1796 | isDemo: boolean; |
| #1797 | value: string; |
| #1798 | setValue: (value: string) => void; |
| #1799 | inputRef: RefObject<HTMLTextAreaElement | null>; |
| #1800 | submitting: boolean; |
| #1801 | submit: () => void; |
| #1802 | status: string; |
| #1803 | placeholder: string; |
| #1804 | }) { |
| #1805 | if (isDemo) return <div className="demo-mask">Execution controls are hidden in demo mode.</div>; |
| #1806 | return ( |
| #1807 | <div className="task-composer"> |
| #1808 | <textarea |
| #1809 | ref={inputRef} |
| #1810 | value={value} |
| #1811 | onChange={event => setValue(event.target.value)} |
| #1812 | onKeyDown={event => { |
| #1813 | if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) submit(); |
| #1814 | }} |
| #1815 | placeholder={placeholder} |
| #1816 | /> |
| #1817 | <button type="button" className="btn-gold" onClick={submit} disabled={submitting || !value.trim()}> |
| #1818 | {submitting ? 'Handing off...' : 'Give task'} |
| #1819 | </button> |
| #1820 | {status && <p>{status}</p>} |
| #1821 | </div> |
| #1822 | ); |
| #1823 | } |
| #1824 | |
| #1825 | function CouncilRoom({ |
| #1826 | isDemo, |
| #1827 | agents, |
| #1828 | activeCouncilRoom, |
| #1829 | setActiveCouncilRoom, |
| #1830 | visibleMessages, |
| #1831 | councilPrompt, |
| #1832 | setCouncilPrompt, |
| #1833 | councilPromptRef, |
| #1834 | conveningCouncil, |
| #1835 | conveneCouncil, |
| #1836 | councilStatus, |
| #1837 | selectedAgent, |
| #1838 | seatPrompt, |
| #1839 | setSeatPrompt, |
| #1840 | seatPromptRef, |
| #1841 | sendCouncilSeatMessage, |
| #1842 | seatChatLoading, |
| #1843 | seatChatStatus, |
| #1844 | selectedSeatTurns, |
| #1845 | }: { |
| #1846 | isDemo: boolean; |
| #1847 | agents: CouncilAgent[]; |
| #1848 | activeCouncilRoom: string; |
| #1849 | setActiveCouncilRoom: (room: string) => void; |
| #1850 | visibleMessages: CouncilMessage[]; |
| #1851 | councilPrompt: string; |
| #1852 | setCouncilPrompt: (value: string) => void; |
| #1853 | councilPromptRef: RefObject<HTMLTextAreaElement | null>; |
| #1854 | conveningCouncil: boolean; |
| #1855 | conveneCouncil: () => void; |
| #1856 | councilStatus: string; |
| #1857 | selectedAgent?: CouncilAgent; |
| #1858 | seatPrompt: string; |
| #1859 | setSeatPrompt: (value: string) => void; |
| #1860 | seatPromptRef: RefObject<HTMLTextAreaElement | null>; |
| #1861 | sendCouncilSeatMessage: () => void; |
| #1862 | seatChatLoading: boolean; |
| #1863 | seatChatStatus: string; |
| #1864 | selectedSeatTurns: ChatTurn[]; |
| #1865 | }) { |
| #1866 | return ( |
| #1867 | <div className="state-room-panel council-room-open" data-council-bus="readable"> |
| #1868 | <div className="room-heading"> |
| #1869 | <span>Council Chamber</span> |
| #1870 | <strong>Read the room while it thinks.</strong> |
| #1871 | <em>{agents.length} seats · {activeCouncilRoom === 'convene-all' ? 'convene-all' : activeCouncilRoom}</em> |
| #1872 | </div> |
| #1873 | <div className="council-state-grid"> |
| #1874 | <section className="council-seat-rail"> |
| #1875 | <button type="button" className={activeCouncilRoom === 'convene-all' ? 'active' : ''} onClick={() => setActiveCouncilRoom('convene-all')}> |
| #1876 | <span>All</span> |
| #1877 | <em>Convene room</em> |
| #1878 | </button> |
| #1879 | {agents.map(agent => ( |
| #1880 | <button key={agent.id} type="button" className={activeCouncilRoom === agent.id ? 'active' : ''} onClick={() => setActiveCouncilRoom(agent.id)}> |
| #1881 | <span>{agent.name}</span> |
| #1882 | <em>{agent.brain}</em> |
| #1883 | </button> |
| #1884 | ))} |
| #1885 | </section> |
| #1886 | <section className="council-readable-transcript"> |
| #1887 | {isDemo ? ( |
| #1888 | <div className="demo-mask">Council transcript is private in demo mode.</div> |
| #1889 | ) : visibleMessages.length ? ( |
| #1890 | visibleMessages.map((message, index) => ( |
| #1891 | <article key={message.id} className={`council-message ${message.gated ? 'gated' : ''}`} style={statusStyle(index)}> |
| #1892 | <header> |
| #1893 | <div> |
| #1894 | <strong>{message.agentName}</strong> |
| #1895 | <span>{message.role} · {message.brain}</span> |
| #1896 | </div> |
| #1897 | <time>{formatTime(message.ts)}</time> |
| #1898 | </header> |
| #1899 | <p>{message.content}</p> |
| #1900 | <footer> |
| #1901 | <span>{message.kind}</span> |
| #1902 | {message.gated && <span>Gated</span>} |
| #1903 | {message.checks?.pipeline && <span>Pipeline</span>} |
| #1904 | {message.checks?.organs && <span>Organs</span>} |
| #1905 | {message.checks?.soul && <span>SOUL</span>} |
| #1906 | </footer> |
| #1907 | </article> |
| #1908 | )) |
| #1909 | ) : ( |
| #1910 | <div className="empty-state">Waiting for the Council bus.</div> |
| #1911 | )} |
| #1912 | </section> |
| #1913 | <section className="council-command-panel"> |
| #1914 | {isDemo ? ( |
| #1915 | <div className="demo-mask">Council prompts are operator-only.</div> |
| #1916 | ) : ( |
| #1917 | <> |
| #1918 | <label> |
| #1919 | <span>Convene all</span> |
| #1920 | <textarea ref={councilPromptRef} value={councilPrompt} onChange={event => setCouncilPrompt(event.target.value)} placeholder="Ask the Council to debate a goal, objection, or next move." /> |
| #1921 | </label> |
| #1922 | <button type="button" className="btn-gold" onClick={conveneCouncil} disabled={conveningCouncil || !councilPrompt.trim()}> |
| #1923 | {conveningCouncil ? 'Convening...' : 'Convene Council'} |
| #1924 | </button> |
| #1925 | {councilStatus && <p>{councilStatus}</p>} |
| #1926 | <div className="seat-direct-box"> |
| #1927 | <span>Direct seat</span> |
| #1928 | <strong>{selectedAgent?.name ?? 'Select a seat'}</strong> |
| #1929 | <em>{selectedAgent ? `${selectedAgent.role} · ${selectedAgent.brain}` : 'Pick from the rail.'}</em> |
| #1930 | <textarea ref={seatPromptRef} value={seatPrompt} onChange={event => setSeatPrompt(event.target.value)} placeholder={selectedAgent ? `Talk to ${selectedAgent.name}.` : 'Select a seat first.'} /> |
| #1931 | <button type="button" className="btn-ghost" onClick={sendCouncilSeatMessage} disabled={!selectedAgent || seatChatLoading || !seatPrompt.trim()}> |
| #1932 | {seatChatLoading ? 'Sending...' : 'Send to seat'} |
| #1933 | </button> |
| #1934 | {seatChatStatus && <p>{seatChatStatus}</p>} |
| #1935 | <div className="seat-direct-turns"> |
| #1936 | {selectedSeatTurns.map(turn => ( |
| #1937 | <article key={turn.id} className={`seat-direct-turn ${turn.role}`}> |
| #1938 | <span>{turn.role === 'user' ? 'King' : selectedAgent?.name ?? 'Seat'}</span> |
| #1939 | <p>{turn.content}</p> |
| #1940 | {turn.model && <em>{turn.model}</em>} |
| #1941 | </article> |
| #1942 | ))} |
| #1943 | </div> |
| #1944 | </div> |
| #1945 | </> |
| #1946 | )} |
| #1947 | </section> |
| #1948 | </div> |
| #1949 | </div> |
| #1950 | ); |
| #1951 | } |
| #1952 |