1094 lines
81 KiB
TypeScript
1094 lines
81 KiB
TypeScript
/* eslint-disable */
|
|
// @ts-nocheck
|
|
// [TSM.ID].[11031972] — All Rights Reserved. Proprietary & Confidential.
|
|
"use client";
|
|
|
|
import { useEffect, useState, useRef, useCallback } from "react";
|
|
import { XCUQuantumMatrix } from "../lib/xcu-quantum-decoder";
|
|
import { JumlahChat } from "./JumlahChat";
|
|
import { NeuralAttentionEngine } from "./NeuralAttentionEngine";
|
|
import { QuantumAdapter } from "../lib/quantum-adapter";
|
|
|
|
export function XCURoom({ roomName, token, serverUrl, isVoiceCall = false, initialCameraOn = false, initialMicOn = false, isAudience = false, username: propUsername = '', adapter = QuantumAdapter.getInstance() }: any) {
|
|
// BUG FIX #1: FPS selector state
|
|
const [currentFps, setCurrentFps] = useState<15 | 30 | 60>(30);
|
|
const [logs, setLogs] = useState<string[]>([]);
|
|
const [isMatrixActive, setIsMatrixActive] = useState(false);
|
|
|
|
const [isChatOpen, setIsChatOpen] = useState(false);
|
|
const [isParticipantsOpen, setIsParticipantsOpen] = useState(false);
|
|
const [isCameraOn, setIsCameraOn] = useState(initialCameraOn);
|
|
const [cameraFacingMode, setCameraFacingMode] = useState<"user" | "environment">("user");
|
|
const [useVirtualBg, setUseVirtualBg] = useState<number>(0);
|
|
const [useBeautyFilter, setUseBeautyFilter] = useState(false);
|
|
const [isMicOn, setIsMicOn] = useState(initialMicOn);
|
|
const [isScreenSharing, setIsScreenSharing] = useState(false);
|
|
const [isMoreMenuOpen, setIsMoreMenuOpen] = useState(false);
|
|
const [layoutMode, setLayoutMode] = useState<'speaker' | 'gallery'>('gallery');
|
|
const [isPodcast, setIsPodcast] = useState(false);
|
|
const [participants, setParticipants] = useState<number[]>([]);
|
|
const [participantNames, setParticipantNames] = useState<Record<number, string>>({});
|
|
const [localStream, setLocalStream] = useState<MediaStream | null>(null);
|
|
const [activeSpeakerId, setActiveSpeakerId] = useState<number | null>(null);
|
|
const [audioLevel, setAudioLevel] = useState<number>(0);
|
|
const [participantId, setParticipantId] = useState<number>(0);
|
|
const [isTransportReady, setIsTransportReady] = useState(false);
|
|
const [webTransport, setWebTransport] = useState<{ datagrams: { readable: ReadableStream, writable: WritableStream } } | null>(null);
|
|
const [isShadowNode, setIsShadowNode] = useState(false);
|
|
const [username, setUsername] = useState(propUsername || "");
|
|
const [isRenamingLocal, setIsRenamingLocal] = useState(false);
|
|
const [renameInput, setRenameInput] = useState("");
|
|
const omniChannelRef = useRef<BroadcastChannel | null>(null);
|
|
|
|
// FASE 84 & 86: Zero UI Cinematic Mode & Attention Engine
|
|
const [isIdle, setIsIdle] = useState(false);
|
|
const [isAttentive, setIsAttentive] = useState(false);
|
|
const [meetingTime, setMeetingTime] = useState(0);
|
|
const idleTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
// FASE 86: Parallax and Audio Refs
|
|
const audioRef = useRef<number>(0);
|
|
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
|
const [status, setStatus] = useState<string>("Inisialisasi XCom WebTransport...");
|
|
const [unlockedModules, setUnlockedModules] = useState<number[]>([]);
|
|
const [bgUrl, setBgUrl] = useState<string | null>(null);
|
|
const [e2eeKeyStr, setE2eeKeyStr] = useState<string | null>(null);
|
|
const [videoEngineMode, setVideoEngineMode] = useState<'auto'|'canvas'|'webcodecs'>('auto');
|
|
const [isMatrixCommandOpen, setIsMatrixCommandOpen] = useState(false);
|
|
const [matrixCommandTab, setMatrixCommandTab] = useState<'network'|'quantum'>('network');
|
|
const [wasmCapabilities, setWasmCapabilities] = useState({ postQuantum: false, aegis: false, doppler: false, neuralWhisper: false });
|
|
const [globalTimer, setGlobalTimer] = useState<number | null>(null);
|
|
const [activeWasmModules, setActiveWasmModules] = useState<{kyber: boolean, aegis: boolean, whisper: boolean, doppler: boolean}>({kyber: false, aegis: false, whisper: false, doppler: false});
|
|
|
|
// PKEPX Zoom-Killer States
|
|
const [isBreakoutOpen, setIsBreakoutOpen] = useState(false);
|
|
const [activeReactions, setActiveReactions] = useState<{id: number, type: string, ts: number}[]>([]);
|
|
const [isRecording, setIsRecording] = useState(false);
|
|
const [isHost, setIsHost] = useState(false); // Host is the first one or designated
|
|
const [isLeaving, setIsLeaving] = useState(false); // Confirmation for leaving
|
|
// Helper: format bytes/sec to human readable
|
|
const formatRate = (bps: number) => {
|
|
if (bps === 0) return '0 B/s';
|
|
if (bps < 1024) return bps + ' B/s';
|
|
if (bps < 1048576) return (bps / 1024).toFixed(1) + ' KB/s';
|
|
return (bps / 1048576).toFixed(2) + ' MB/s';
|
|
};
|
|
const formatBytes = (b: number) => {
|
|
if (b < 1024) return b + ' B';
|
|
if (b < 1048576) return (b / 1024).toFixed(1) + ' KB';
|
|
return (b / 1048576).toFixed(1) + ' MB';
|
|
};
|
|
const [trafficStats, setTrafficStats] = useState({
|
|
tx: { video: 0, audio: 0, control: 0, total: 0 },
|
|
rx: { video: 0, audio: 0, control: 0, total: 0 },
|
|
rates: { txVideo: 0, txAudio: 0, txTotal: 0, rxVideo: 0, rxAudio: 0, rxTotal: 0 },
|
|
wsState: 'CLOSED',
|
|
});
|
|
const [audioEngineMode, setAudioEngineMode] = useState<'auto'|'pcm'|'xcu-neural'>('auto');
|
|
const [autoPilotMetrics, setAutoPilotMetrics] = useState({ vCodec: 'STANDBY', aCodec: 'STANDBY', bw: 0 });
|
|
const [uiMatrix, setUiMatrix] = useState<any>(null);
|
|
|
|
const matrixRef = useRef<XCUQuantumMatrix | null>(null);
|
|
const tokenRef = useRef<string>("");
|
|
const [quantumUpgradeAlert, setQuantumUpgradeAlert] = useState<string | null>(null);
|
|
const localVideoRef = useRef<HTMLVideoElement>(null);
|
|
|
|
useEffect(() => {
|
|
const timer = setInterval(() => setMeetingTime(t => t + 1), 1000);
|
|
return () => clearInterval(timer);
|
|
}, []);
|
|
|
|
const fetchQuantumTokenLive = useCallback(async () => {
|
|
try {
|
|
const res = await fetch(`/api/auth/quantum_token?_cb=${Date.now()}&_live=1`, { credentials: 'include' });
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setUiMatrix(data.capabilities?.ui || null);
|
|
// DYNAMIC DB CONFIG FOR WASM SDK (Mocking if missing)
|
|
setWasmCapabilities(data.capabilities?.wasm || {
|
|
postQuantum: true, // Supreme Admin gets true by default
|
|
aegis: true,
|
|
doppler: true,
|
|
neuralWhisper: true
|
|
});
|
|
console.log("[Quantum Sovereignty] UI Matrix live updated:", data.capabilities?.ui);
|
|
}
|
|
} catch(e) {}
|
|
}, []);
|
|
|
|
const formatTime = (s: number) => {
|
|
const min = Math.floor(s / 60);
|
|
const sec = s % 60;
|
|
return `${min}:${sec < 10 ? '0' : ''}${sec}`;
|
|
};
|
|
|
|
const resetIdleTimer = useCallback((e?: MouseEvent) => {
|
|
setIsIdle(false);
|
|
if (e) {
|
|
setMousePos({
|
|
x: (e.clientX / window.innerWidth) * 2 - 1,
|
|
y: (e.clientY / window.innerHeight) * 2 - 1
|
|
});
|
|
}
|
|
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
|
idleTimerRef.current = setTimeout(() => setIsIdle(isAttentive), 8000);
|
|
}, [isAttentive]);
|
|
|
|
useEffect(() => {
|
|
window.addEventListener('mousemove', resetIdleTimer);
|
|
const handleUserInteraction = async () => {
|
|
await adapter.warmUpAudio();
|
|
if (matrixRef.current) matrixRef.current.resumeAudioContext();
|
|
};
|
|
window.addEventListener('click', handleUserInteraction);
|
|
window.addEventListener('touchstart', handleUserInteraction);
|
|
|
|
// Set codec strategy based on browser
|
|
const strategy = adapter.getCodecStrategy();
|
|
console.log(`[QuantumAdapter] Selected Codec Strategy: ${strategy} for ${JSON.stringify(adapter.capabilities)}`);
|
|
|
|
const handleMessage = (e: MessageEvent) => {
|
|
if (e.data?.type === 'SYNC_SETTINGS_TO_VC') {
|
|
const payload = e.data.payload || {};
|
|
if (payload.videoBg === 'custom' && payload.customBgUrl) setBgUrl(payload.customBgUrl);
|
|
else setBgUrl(null);
|
|
} else if (e.data?.type === 'SET_E2EE_KEY') {
|
|
const key = e.data.payload;
|
|
setE2eeKeyStr(key);
|
|
if (matrixRef.current) matrixRef.current.setE2EEKey(key);
|
|
window.parent.postMessage({ type: 'SUPREME_EYE_TELEMETRY', payload: { e2eeActive: key !== 'NO_KEY' && key !== 'none' } }, '*');
|
|
} else if (e.data?.type === 'SET_CUSTOM_BG') setBgUrl(e.data.payload);
|
|
};
|
|
window.addEventListener('message', handleMessage);
|
|
|
|
const bc = new BroadcastChannel('omni_channel');
|
|
bc.addEventListener('message', (e) => {
|
|
if (e.data?.type === 'REFRESH_QUANTUM_TOKEN') {
|
|
console.log("Omni-Channel LIVE: Resyncing Quantum Token...");
|
|
fetchQuantumTokenLive();
|
|
}
|
|
});
|
|
omniChannelRef.current = bc;
|
|
|
|
setTimeout(() => resetIdleTimer(), 0);
|
|
return () => {
|
|
window.removeEventListener('mousemove', resetIdleTimer);
|
|
window.removeEventListener('click', handleUserInteraction);
|
|
window.removeEventListener('touchstart', handleUserInteraction);
|
|
window.removeEventListener('message', handleMessage);
|
|
bc.close();
|
|
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
|
};
|
|
}, [resetIdleTimer, adapter]);
|
|
|
|
useEffect(() => {
|
|
audioRef.current = audioLevel / 100;
|
|
}, [audioLevel]);
|
|
|
|
const addLog = useCallback((msg: string) => {
|
|
setLogs(prev => [...prev, msg]);
|
|
}, []);
|
|
|
|
const handleToggleCamera = useCallback(async () => {
|
|
if (!matrixRef.current) return;
|
|
if (!isCameraOn) {
|
|
setIsCameraOn(true);
|
|
setIsScreenSharing(false);
|
|
await matrixRef.current.activateUplink('camera', cameraFacingMode);
|
|
// BUG FIX #2: Ensure mic state stays independent of camera
|
|
// Camera activateUplink requests audio track too, so enforce current mic state
|
|
matrixRef.current.toggleMic(isMicOn);
|
|
} else {
|
|
await matrixRef.current.deactivateUplink();
|
|
setIsCameraOn(false);
|
|
setLocalStream(null);
|
|
}
|
|
}, [isCameraOn, isMicOn, cameraFacingMode]);
|
|
|
|
const handleFlipCamera = async () => {
|
|
const newMode = cameraFacingMode === 'user' ? 'environment' : 'user';
|
|
setCameraFacingMode(newMode);
|
|
if (isCameraOn && matrixRef.current) {
|
|
await matrixRef.current.deactivateUplink();
|
|
await matrixRef.current.activateUplink('camera', newMode);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (globalTimer === null || globalTimer <= 0) return;
|
|
const t = setInterval(() => setGlobalTimer(prev => prev !== null && prev > 0 ? prev - 1 : prev), 1000);
|
|
return () => clearInterval(t);
|
|
}, [globalTimer]);
|
|
|
|
useEffect(() => {
|
|
let matrix: XCUQuantumMatrix | null = null;
|
|
const initMatrix = async () => {
|
|
try {
|
|
const channel = new BroadcastChannel(`omni_matrix_${roomName}`);
|
|
omniChannelRef.current = channel;
|
|
let isMaster = false;
|
|
const electionPromise = new Promise<boolean>((resolve) => {
|
|
const timeout = setTimeout(() => resolve(true), 150);
|
|
channel.onmessage = (e) => {
|
|
if (e.data.type === "I_AM_MASTER") { clearTimeout(timeout); resolve(false); }
|
|
};
|
|
channel.postMessage({ type: "WHO_IS_MASTER" });
|
|
});
|
|
isMaster = await electionPromise;
|
|
if (!isMaster) {
|
|
setIsShadowNode(true);
|
|
setStatus("Shadow Node Active.");
|
|
channel.onmessage = (e) => {
|
|
if (e.data.type === "MASTER_UPGRADE_ALERT") { setQuantumUpgradeAlert(e.data.msg); setTimeout(() => setQuantumUpgradeAlert(null), 5000); }
|
|
if (e.data.type === "MASTER_LOG") addLog(e.data.msg);
|
|
};
|
|
return;
|
|
}
|
|
channel.onmessage = (e) => { if (e.data.type === "WHO_IS_MASTER") channel.postMessage({ type: "I_AM_MASTER" }); };
|
|
const currentHost = typeof window !== 'undefined' ? window.location.hostname : '127.0.0.1';
|
|
const nodes = process.env.NEXT_PUBLIC_XCU_NODES ? process.env.NEXT_PUBLIC_XCU_NODES.split(',') : [`${window.location.protocol}//${currentHost}:8443`];
|
|
let winningNode = serverUrl;
|
|
let quantumHash: string | undefined;
|
|
try {
|
|
const pingPromises = nodes.map(url => new Promise<{node: string, hash: string}>((resolve, reject) => {
|
|
const urlObj = new URL(url);
|
|
const proto = window.location.protocol;
|
|
fetch(`${proto}//${urlObj.hostname}/api/v1/system/cert`, { signal: AbortSignal.timeout(1500) })
|
|
.then(res => res.json()).then(data => data?.hash ? resolve({ node: url, hash: data.hash }) : reject()).catch(reject);
|
|
}));
|
|
const winner = await Promise.any(pingPromises);
|
|
winningNode = winner.node; quantumHash = winner.hash;
|
|
} catch (e) {}
|
|
|
|
// DETERMINISTIC Participant ID — same user = same ID, prevents duplicates
|
|
// Hash email to 16-bit integer (1-65534) so same user in different tabs/browsers = same ID
|
|
let pId = 1;
|
|
try {
|
|
const authForId = await fetch(`/vc/api/auth/me?_cb=${Date.now()}`);
|
|
const authIdData = await authForId.json();
|
|
if (authIdData.email) {
|
|
let hash = 0;
|
|
for (let i = 0; i < authIdData.email.length; i++) {
|
|
hash = ((hash << 5) - hash + authIdData.email.charCodeAt(i)) | 0;
|
|
}
|
|
pId = (Math.abs(hash) % 65534) + 1; // 1-65534 range
|
|
} else {
|
|
pId = Math.floor(Math.random() * 65534) + 1; // Fallback for guest
|
|
}
|
|
} catch(_) {
|
|
pId = Math.floor(Math.random() * 65534) + 1;
|
|
}
|
|
let entitlementToken = "";
|
|
let byokKeyFromToken = "none";
|
|
let matrixCapabilities: any = null;
|
|
|
|
try {
|
|
const res = await fetch(`/api/auth/quantum_token?_cb=${Date.now()}`, { credentials: 'include' });
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
entitlementToken = data.token;
|
|
tokenRef.current = entitlementToken;
|
|
matrixCapabilities = data.capabilities;
|
|
if (data.byokActive && data.byok) {
|
|
byokKeyFromToken = data.byok;
|
|
}
|
|
setUiMatrix(matrixCapabilities?.ui || null);
|
|
// Assign host capability based on UI Matrix
|
|
if (matrixCapabilities?.ui?.['jvc.ui.host_controls'] !== false) {
|
|
setIsHost(true);
|
|
}
|
|
} else {
|
|
throw new Error("Quantum Gateway menolak akses (Anti-Jumping Triggered).");
|
|
}
|
|
} catch (e) {
|
|
console.error("Quantum Auth Failed", e);
|
|
setStatus("Akses Ditolak. Harap masuk melalui JUMPA.ID IAM.");
|
|
return;
|
|
}
|
|
|
|
matrix = new XCUQuantumMatrix(roomName, pId, entitlementToken);
|
|
setParticipantId(matrix.participantId);
|
|
matrixRef.current = matrix;
|
|
// Poll REAL traffic stats from matrix every 1s
|
|
const trafficPoll = setInterval(() => {
|
|
if (matrixRef.current?.trafficStats) {
|
|
const s = matrixRef.current.trafficStats;
|
|
setTrafficStats({
|
|
tx: { ...s.tx }, rx: { ...s.rx },
|
|
rates: { ...s.rates }, wsState: s.wsState,
|
|
});
|
|
}
|
|
}, 1000);
|
|
|
|
// Terapkan Hukum 101-Modul Matrix (Kedaulatan Otak XCU)
|
|
if (matrixCapabilities && matrixCapabilities.video) {
|
|
// Jika Autopilot diizinkan oleh lisensi, gunakan 'auto'. Jika tidak, paksa 'webcodecs' atau 'canvas' berdasarkan resolusi
|
|
const isAutopilot = matrixCapabilities.video.features?.autopilot;
|
|
const hardCodec = matrixCapabilities.video.maxResolution === '4K' ? 'webcodecs' : 'canvas';
|
|
matrix.videoEngineMode = 'canvas'; // FORCED BY SUPREME COMMANDER TO AVOID WEBCODECS CRASH
|
|
|
|
// Log ke console untuk bukti Forensik Auto-Pilot / Hard-Codec
|
|
console.log(`[101-MATRIX] Video Codec disetel ke: ${matrix.videoEngineMode} (Lisensi: ${matrixCapabilities.video.codec})`);
|
|
|
|
// Audio juga dikontrol oleh Matrix
|
|
matrix.audioEngineMode = matrixCapabilities.chat?.features?.omniBrainAI ? 'xcu-neural' : 'auto';
|
|
} else {
|
|
matrix.videoEngineMode = videoEngineMode;
|
|
matrix.audioEngineMode = audioEngineMode;
|
|
}
|
|
|
|
// Prioritize BYOK Key from Token over Local State (Central Sovereignty)
|
|
const effectiveKey = byokKeyFromToken !== 'none' ? byokKeyFromToken : (e2eeKeyStr || 'NO_KEY');
|
|
if (effectiveKey !== 'NO_KEY') {
|
|
matrix.setE2EEKey(effectiveKey);
|
|
if (byokKeyFromToken !== 'none') setE2eeKeyStr(byokKeyFromToken);
|
|
}
|
|
matrix.onModuleUnlocked = (id) => setUnlockedModules(prev => prev.includes(id) ? prev : [...prev, id]);
|
|
matrix.onLocalStream = (s) => setLocalStream(s);
|
|
matrix.onParticipantJoined = (id) => {
|
|
setParticipants(prev => prev.includes(id) ? prev : [...prev, id]);
|
|
// BUG FIX #3: Immediately re-announce our name when a new participant joins
|
|
// so they see our name instead of a number
|
|
setTimeout(() => { if (matrixRef.current) matrixRef.current.announceDisplayName(); }, 500);
|
|
};
|
|
matrix.onParticipantLeft = (id) => {
|
|
setParticipants(prev => prev.filter(p => p !== id));
|
|
setParticipantNames(prev => { const n = {...prev}; delete n[id]; return n; });
|
|
};
|
|
matrix.onParticipantNameReceived = (id, name) => {
|
|
setParticipantNames(prev => ({...prev, [id]: name}));
|
|
};
|
|
matrix.onActiveSpeakerChanged = (id) => setActiveSpeakerId(id);
|
|
matrix.onAudioLevel = (l) => setAudioLevel(l);
|
|
matrix.onQuantumResonance = (id, type) => {
|
|
if (type.startsWith('TIMER:')) {
|
|
const secs = parseInt(type.split(':')[1]);
|
|
setGlobalTimer(secs);
|
|
return;
|
|
}
|
|
setActiveReactions(prev => [...prev, { id, type, ts: Date.now() }]);
|
|
// Auto cleanup after 4 seconds
|
|
setTimeout(() => {
|
|
setActiveReactions(prev => prev.filter(r => Date.now() - r.ts < 4000));
|
|
}, 4000);
|
|
};
|
|
matrix.onSovereignSignal = (type, payload) => {
|
|
if (type === 'MUTED_BY_HOST') setIsMicOn(false);
|
|
if (type === 'WARP_COMPLETE') window.location.href = `/room/${payload}`;
|
|
};
|
|
|
|
// BUG FIX #3: Use display name (not email) — like Zoom shows participant names
|
|
const authResp = await fetch(`/vc/api/auth/me?_cb=${Date.now()}`);
|
|
const authData = await authResp.json();
|
|
if (authData.name || authData.displayName) {
|
|
// Prefer human-readable name over email
|
|
const displayName = authData.name || authData.displayName;
|
|
matrix.displayName = displayName;
|
|
setUsername(displayName);
|
|
} else if (authData.email) {
|
|
// Fallback: Use email prefix (before @) as display name
|
|
const emailName = authData.email.split('@')[0];
|
|
matrix.displayName = emailName;
|
|
setUsername(emailName);
|
|
} else if (propUsername) {
|
|
matrix.displayName = propUsername;
|
|
}
|
|
await matrix.ignite(roomName, winningNode, entitlementToken);
|
|
setIsMatrixActive(true); setIsTransportReady(true);
|
|
setWebTransport(matrix.transport);
|
|
|
|
// Announce display name to room after 2s (let connection stabilize)
|
|
setTimeout(() => { if (matrixRef.current) matrixRef.current.announceDisplayName(); }, 2000);
|
|
// Re-announce every 10s so new joiners learn existing names
|
|
const nameAnnounceInterval = setInterval(() => {
|
|
if (matrixRef.current) matrixRef.current.announceDisplayName();
|
|
}, 10000);
|
|
|
|
// Auto-pilot based on Lobby selection
|
|
if (initialCameraOn) {
|
|
setTimeout(() => { if (matrixRef.current) matrixRef.current.activateUplink('camera', cameraFacingMode).then(() => setIsCameraOn(true)); }, 1000);
|
|
}
|
|
if (initialMicOn) {
|
|
setTimeout(() => { if (matrixRef.current) matrixRef.current.toggleMic(true); }, 1500);
|
|
}
|
|
} catch (e) { setStatus("Connection Failed."); }
|
|
};
|
|
initMatrix();
|
|
|
|
// Immediate cleanup on tab close to prevent ghost participants
|
|
const handleBeforeUnload = () => {
|
|
if (matrixRef.current) matrixRef.current.shutdown();
|
|
};
|
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
|
|
return () => {
|
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
if (matrix) matrix.shutdown();
|
|
if (omniChannelRef.current) omniChannelRef.current.close();
|
|
};
|
|
}, []);
|
|
|
|
const onDisconnect = () => {
|
|
setStatus("Reconnecting...");
|
|
setIsMatrixActive(false);
|
|
setTimeout(() => {
|
|
if (matrixRef.current) {
|
|
matrixRef.current.ignite(roomName as string, "wss://mesh.ultramodul.xyz/api/system/engine", tokenRef.current)
|
|
.then(() => { setStatus("Connected"); setIsMatrixActive(true); })
|
|
.catch(() => window.location.href = '/dashboard');
|
|
}
|
|
}, 3000);
|
|
};
|
|
const handleToggleMic = () => {
|
|
const newState = !isMicOn; setIsMicOn(newState);
|
|
if (matrixRef.current) { matrixRef.current.toggleMic(newState); matrixRef.current.resumeAudioContext(); }
|
|
};
|
|
// Update effects when toggled
|
|
useEffect(() => {
|
|
if (matrixRef.current) {
|
|
matrixRef.current.setEffects(useVirtualBg, useBeautyFilter);
|
|
}
|
|
}, [useVirtualBg, useBeautyFilter]);
|
|
|
|
const handleToggleScreen = async () => {
|
|
if (!matrixRef.current) return;
|
|
if (isScreenSharing) {
|
|
await matrixRef.current.deactivateUplink(); setIsScreenSharing(false); setLocalStream(null);
|
|
if (isCameraOn) await matrixRef.current.activateUplink('camera', cameraFacingMode);
|
|
} else {
|
|
await matrixRef.current.deactivateUplink(); setIsCameraOn(false); setIsScreenSharing(true);
|
|
await matrixRef.current.activateUplink('screen');
|
|
}
|
|
};
|
|
|
|
const renderVideoTile = (id: number, isSmall: boolean) => {
|
|
const isLocal = id === participantId;
|
|
if (isAudience && isLocal) return null;
|
|
return (
|
|
<div className="w-full h-full relative group bg-black flex items-center justify-center">
|
|
{isLocal ? (
|
|
<>
|
|
<NeuralAttentionEngine videoRef={localVideoRef} onAttentionChange={setIsAttentive} />
|
|
{(isCameraOn || isScreenSharing) ? (
|
|
<video ref={(el) => { if (el && localStream && el.srcObject !== localStream) el.srcObject = localStream; localVideoRef.current = el; }} autoPlay playsInline muted className={`w-full h-full object-cover ${isCameraOn && !isScreenSharing ? 'scale-x-[-1]' : ''}`} />
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center bg-slate-900">
|
|
<div className={`${isSmall ? 'w-10 h-10 text-sm' : 'w-24 h-24 text-4xl'} rounded-full bg-gradient-to-br from-blue-600 to-indigo-700 flex items-center justify-center font-bold text-white`}>{(username || 'U').substring(0,2).toUpperCase()}</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
<canvas id={`quantum-matrix-${id}`} className="w-full h-full object-cover" ref={el => { if (el && matrixRef.current && !isSmall) matrixRef.current.registerCanvas(id, el.id); }} />
|
|
{!participants.includes(id) && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-slate-950">
|
|
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-emerald-600 to-teal-700 animate-pulse flex items-center justify-center text-white font-bold">{(participantNames[id] || 'P').substring(0,2).toUpperCase()}</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity z-50">
|
|
<button
|
|
onClick={async (e) => {
|
|
e.stopPropagation();
|
|
try {
|
|
if (isLocal && localVideoRef.current) {
|
|
await localVideoRef.current.requestPictureInPicture();
|
|
} else {
|
|
const canvas = document.getElementById(`quantum-matrix-${id}`) as HTMLCanvasElement;
|
|
if (canvas) {
|
|
const stream = canvas.captureStream(30);
|
|
const video = document.createElement('video');
|
|
video.srcObject = stream;
|
|
video.muted = true;
|
|
await video.play();
|
|
await video.requestPictureInPicture();
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn("[PiP] Failed to start Picture-in-Picture", err);
|
|
}
|
|
}}
|
|
className="p-1.5 bg-black/60 backdrop-blur-md rounded-lg text-white/80 hover:text-white border border-white/10 hover:border-white/30 transition-all"
|
|
title="Picture in Picture"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 3H3v18h18V3zM21 12H12v9h9v-9z"></path></svg>
|
|
</button>
|
|
</div>
|
|
<div className="absolute bottom-3 left-3 px-3 py-1 bg-black/60 backdrop-blur-md rounded-lg text-xs font-bold text-white flex items-center gap-2 border border-white/10 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<div className={`w-2 h-2 rounded-full ${isMicOn || !isLocal ? 'bg-emerald-500' : 'bg-red-500'}`}></div>
|
|
{isLocal ? (username || 'Me') : (participantNames[id] || `User ${id}`)}
|
|
{isLocal && <span className="text-[8px] text-white/50">(Me)</span>}
|
|
</div>
|
|
{/* PKEPX Quantum Resonance */}
|
|
{activeReactions.filter(r => r.id === id).map(r => (
|
|
<div key={r.ts} className="absolute inset-0 flex items-center justify-center z-50 pointer-events-none animate-[ping_1s_ease-out]">
|
|
<span className="text-6xl md:text-8xl drop-shadow-[0_0_30px_rgba(255,255,255,0.8)] animate-bounce">{r.type}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const allParticipants = [participantId, ...participants].filter((id, i, arr) => arr.indexOf(id) === i && id !== 0);
|
|
const effectiveActiveSpeaker = activeSpeakerId || participantId;
|
|
const theme = 'dark';
|
|
|
|
return (
|
|
<div className={`h-dvh w-full bg-black text-slate-200 font-sans flex flex-col overflow-hidden relative selection:bg-emerald-500/30 ${theme === 'light' ? 'bg-white' : ''}`}>
|
|
|
|
{/* Rename Modal — like Zoom rename */}
|
|
{isRenamingLocal && (
|
|
<div className="absolute inset-0 bg-black/80 backdrop-blur-md z-[100] flex items-center justify-center">
|
|
<div className="bg-[#050b14] border border-blue-500/30 rounded-2xl p-6 w-[340px] shadow-[0_0_50px_rgba(59,130,246,0.2)]">
|
|
<h3 className="text-sm font-black text-blue-400 uppercase tracking-widest mb-4">Rename</h3>
|
|
<input
|
|
type="text"
|
|
value={renameInput}
|
|
onChange={(e) => setRenameInput(e.target.value)}
|
|
className="w-full bg-[#0a101d] text-white border border-white/10 rounded-xl px-4 py-3 mb-4 focus:outline-none focus:border-blue-500 font-mono text-sm"
|
|
autoFocus
|
|
maxLength={64}
|
|
placeholder="Display name..."
|
|
/>
|
|
<div className="flex gap-2">
|
|
<button onClick={() => setIsRenamingLocal(false)} className="flex-1 py-2.5 text-gray-400 font-bold bg-white/5 hover:bg-white/10 rounded-xl text-sm">Batal</button>
|
|
<button onClick={() => {
|
|
if (renameInput.trim()) {
|
|
setUsername(renameInput.trim());
|
|
if (matrixRef.current) matrixRef.current.setDisplayName(renameInput.trim());
|
|
}
|
|
setIsRenamingLocal(false);
|
|
}} className="flex-1 py-2.5 text-white font-bold bg-blue-600 hover:bg-blue-500 rounded-xl text-sm">Simpan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* PKEPX Fractal Matrix Breakout Modal */}
|
|
{isBreakoutOpen && (
|
|
<div className="absolute inset-0 bg-black/80 backdrop-blur-md z-[100] flex items-center justify-center">
|
|
<div className="bg-[#050b14] border border-purple-500/30 rounded-2xl p-6 w-[340px] md:w-[400px] shadow-[0_0_50px_rgba(168,85,247,0.2)]">
|
|
<h3 className="text-sm font-black text-purple-400 uppercase tracking-widest mb-4 flex items-center gap-2">
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
|
|
Fractal Matrix Warp
|
|
</h3>
|
|
<div className="space-y-3">
|
|
<button onClick={() => { if(matrixRef.current) matrixRef.current.warpToMatrix(roomName + "-alpha"); setIsBreakoutOpen(false); }} className="w-full py-4 bg-white/5 hover:bg-purple-500/20 border border-white/10 hover:border-purple-500/50 rounded-xl font-bold transition-all text-sm flex justify-between items-center px-4">
|
|
<span>Alpha Matrix</span> <span className="text-[10px] bg-purple-500/30 text-purple-400 px-2 py-1 rounded">WARP ⚡</span>
|
|
</button>
|
|
<button onClick={() => { if(matrixRef.current) matrixRef.current.warpToMatrix(roomName + "-beta"); setIsBreakoutOpen(false); }} className="w-full py-4 bg-white/5 hover:bg-purple-500/20 border border-white/10 hover:border-purple-500/50 rounded-xl font-bold transition-all text-sm flex justify-between items-center px-4">
|
|
<span>Beta Matrix</span> <span className="text-[10px] bg-purple-500/30 text-purple-400 px-2 py-1 rounded">WARP ⚡</span>
|
|
</button>
|
|
<button onClick={() => setIsBreakoutOpen(false)} className="w-full py-3 mt-4 text-gray-500 hover:text-white transition-colors text-xs font-bold uppercase tracking-widest border border-transparent hover:border-white/10 rounded-xl">Batal Inisiasi</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* CSS-Only Quantum Background (GPU Safe) */}
|
|
<div className="absolute inset-0 z-0 opacity-20 pointer-events-none overflow-hidden">
|
|
<div className="absolute top-[-20%] left-[-10%] w-[60%] h-[60%] bg-emerald-900/30 blur-[100px] rounded-full animate-[pulse_8s_ease-in-out_infinite]"></div>
|
|
<div className="absolute bottom-[-20%] right-[-10%] w-[60%] h-[60%] bg-blue-900/30 blur-[100px] rounded-full animate-[pulse_10s_ease-in-out_infinite]"></div>
|
|
<div className="absolute top-[40%] left-[40%] w-[40%] h-[40%] bg-purple-900/20 blur-[120px] rounded-full animate-[pulse_12s_ease-in-out_infinite]"></div>
|
|
</div>
|
|
|
|
{globalTimer !== null && (
|
|
<div className="absolute top-20 left-1/2 -translate-x-1/2 z-[60] pointer-events-none">
|
|
<div className={`text-6xl md:text-8xl font-black tabular-nums tracking-tighter transition-colors duration-1000 ${globalTimer === 0 ? 'text-red-500 drop-shadow-[0_0_50px_rgba(239,68,68,0.8)] animate-bounce' : globalTimer < 60 ? 'text-amber-400 drop-shadow-[0_0_30px_rgba(251,191,36,0.8)]' : 'text-emerald-400 drop-shadow-[0_0_30px_rgba(16,185,129,0.8)]'}`}>
|
|
{Math.floor(globalTimer / 60).toString().padStart(2, '0')}:{(globalTimer % 60).toString().padStart(2, '0')}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Top Bar - Zoom Class */}
|
|
<div className={`absolute top-0 left-0 right-0 h-12 flex items-center justify-between px-4 z-50 transition-all duration-500`}>
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2 bg-black/40 backdrop-blur-xl px-3 py-1.5 rounded-full border border-white/10">
|
|
<svg className={`w-3.5 h-3.5 ${e2eeKeyStr && e2eeKeyStr !== 'none' ? 'text-emerald-500' : 'text-amber-500'}`} fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" /></svg>
|
|
<span className="text-[10px] font-black uppercase tracking-widest text-white/80">{roomName}</span>
|
|
<div className="w-[1px] h-3 bg-white/10 mx-1"></div>
|
|
<span className="text-[10px] font-mono text-white/60">{formatTime(meetingTime)}</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{uiMatrix?.['jvc.ui.layout_toggle'] !== false && (
|
|
<button onClick={() => setLayoutMode(prev => prev === 'speaker' ? 'gallery' : 'speaker')} className="bg-black/40 backdrop-blur-xl hover:bg-white/10 px-4 py-1.5 rounded-full border border-white/10 text-[10px] font-black uppercase tracking-widest transition-all">
|
|
{layoutMode === 'speaker' ? 'Gallery View' : 'Speaker View'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Grid Area */}
|
|
<div className={`flex-1 flex overflow-hidden relative transition-all duration-500 ${isChatOpen || isParticipantsOpen ? 'pr-[320px]' : ''}`}>
|
|
<div className="flex-1 p-2 md:p-6 flex items-center justify-center relative">
|
|
{!isMatrixActive ? (
|
|
<div className="flex flex-col items-center gap-6">
|
|
<div className="w-12 h-12 border-2 border-white/5 border-t-emerald-500 rounded-full animate-spin"></div>
|
|
<div className="text-[10px] font-black text-emerald-500 uppercase tracking-widest animate-pulse">{status}</div>
|
|
</div>
|
|
) : (
|
|
<div className="w-full h-full max-w-7xl mx-auto relative">
|
|
{layoutMode === 'gallery' ? (
|
|
<div className={`grid gap-2 md:gap-2 w-full h-full auto-rows-fr ${
|
|
allParticipants.length === 1 ? 'grid-cols-1' :
|
|
allParticipants.length === 2 ? 'grid-cols-1 md:grid-cols-2' :
|
|
allParticipants.length <= 4 ? 'grid-cols-2' :
|
|
'grid-cols-2 md:grid-cols-3'
|
|
}`}>
|
|
{allParticipants.map(id => (
|
|
<div key={id} className={`relative rounded-xl md:rounded-2xl overflow-hidden border-2 transition-all duration-500 ${activeSpeakerId === id ? 'border-emerald-500 shadow-[0_0_30px_rgba(16,185,129,0.3)]' : 'border-white/5'}`}>
|
|
{renderVideoTile(id, false)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col md:flex-row gap-4 w-full h-full">
|
|
<div className={`flex-1 rounded-3xl overflow-hidden border-2 border-emerald-500 shadow-[0_0_40px_rgba(16,185,129,0.2)]`}>
|
|
{renderVideoTile(effectiveActiveSpeaker!, false)}
|
|
</div>
|
|
<div className="w-full md:w-56 flex md:flex-col gap-3 overflow-auto custom-scroll">
|
|
{allParticipants.filter(p => p !== effectiveActiveSpeaker).map(id => (
|
|
<div key={id} onClick={() => setActiveSpeakerId(id)} className="w-48 md:w-full aspect-video rounded-xl overflow-hidden border border-white/10 hover:border-white/30 cursor-pointer transition-all">
|
|
{renderVideoTile(id, true)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right Sidebars (Chat & Participants) */}
|
|
<div className={`absolute top-0 right-0 bottom-0 w-[320px] bg-[#050b14] border-l border-white/5 z-40 transition-transform duration-500 shadow-2xl ${isChatOpen || isParticipantsOpen ? 'translate-x-0' : 'translate-x-full'}`}>
|
|
{isChatOpen && (
|
|
<JumlahChat
|
|
roomName={roomName} participantName={username || `User ${participantId}`}
|
|
participantId={participantId} webTransport={webTransport}
|
|
onClose={() => setIsChatOpen(false)}
|
|
/>
|
|
)}
|
|
{isParticipantsOpen && (
|
|
<div className="flex flex-col h-full p-6">
|
|
<div className="flex items-center justify-between mb-8">
|
|
<h2 className="text-sm font-black uppercase tracking-widest text-white/90">Participants ({allParticipants.length})</h2>
|
|
<div className="flex items-center gap-2">
|
|
{isHost && (
|
|
<button onClick={() => { if(matrixRef.current) matrixRef.current.broadcastSovereignSignal('MUTE_ALL'); }} className="text-[9px] bg-red-500/20 hover:bg-red-500/40 text-red-400 font-bold uppercase tracking-wider px-2 py-1 rounded">Mute All</button>
|
|
)}
|
|
<button onClick={() => setIsParticipantsOpen(false)} className="text-gray-500 hover:text-white"><svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg></button>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-4 overflow-y-auto custom-scroll">
|
|
{allParticipants.map(id => (
|
|
<div key={id} className="flex items-center gap-3 p-3 rounded-xl bg-white/5 border border-white/5">
|
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-white font-bold text-xs ${id === participantId ? 'bg-gradient-to-br from-blue-600 to-indigo-700' : 'bg-gradient-to-br from-emerald-600 to-teal-700'}`}>{(id === participantId ? (username || 'U') : (participantNames[id] || 'U')).substring(0,2).toUpperCase()}</div>
|
|
<div className="flex-1">
|
|
<div className="text-xs font-bold text-white/80">{id === participantId ? `${username || 'Me'} (Me)` : (participantNames[id] || `User ${id}`)}</div>
|
|
<div className="text-[9px] text-emerald-500 uppercase font-black">Connected</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<svg className={`w-4 h-4 ${id === participantId && !isMicOn ? 'text-red-500' : 'text-gray-500'}`} fill="currentColor" viewBox="0 0 24 24"><path d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" /></svg>
|
|
<svg className={`w-4 h-4 ${id === participantId && !isCameraOn ? 'text-red-500' : 'text-gray-500'}`} fill="currentColor" viewBox="0 0 24 24"><path d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
|
|
</div>
|
|
{id === participantId && (
|
|
<button onClick={() => { setIsRenamingLocal(true); setRenameInput(username); }} className="p-1.5 bg-blue-500/20 hover:bg-blue-500/40 text-blue-400 rounded-lg ml-1 transition-colors" title="Rename">
|
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>
|
|
</button>
|
|
)}
|
|
{isHost && id !== participantId && (
|
|
<button onClick={() => { if(matrixRef.current) matrixRef.current.broadcastSovereignSignal('KICK_PEER', id); }} className="p-1.5 bg-red-500/20 hover:bg-red-500/40 text-red-500 rounded-lg ml-2 transition-colors" title="Kick">
|
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 7a4 4 0 11-8 0 4 4 0 018 0zM9 14a6 6 0 00-6 6v1h12v-1a6 6 0 00-6-6zm11-2l-4 4m0-4l4 4"></path></svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bottom Bar - Zoom Class UI */}
|
|
<div className={`absolute bottom-0 left-0 right-0 h-20 md:h-24 bg-black/80 backdrop-blur-3xl border-t border-white/10 flex items-center justify-between px-2 md:px-6 z-50 transition-all duration-500`}>
|
|
|
|
<div className="hidden lg:flex items-center gap-6 w-1/4">
|
|
<div className="flex flex-col">
|
|
<span className="text-[9px] font-black text-emerald-500 uppercase tracking-widest">Quantum Engine</span>
|
|
<span className="text-[11px] text-white/60 font-medium">Auto-Pilot Optimized</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1 md:gap-2 flex-1 overflow-x-auto no-scrollbar pb-1 md:pb-0 px-2">
|
|
<div className="mx-auto flex items-center gap-1 md:gap-2">
|
|
<div className="flex items-center gap-1 group shrink-0">
|
|
{uiMatrix?.['jvc.ui.microphone'] !== false && (
|
|
<button onClick={handleToggleMic} className={`relative flex flex-col items-center justify-center w-12 min-w-[48px] md:w-14 h-12 md:h-14 rounded-2xl transition-all overflow-hidden ${!isMicOn ? 'bg-red-500/20 text-red-500 border border-red-500/30' : 'bg-white/5 hover:bg-white/10 text-gray-300 hover:text-white border border-white/5'}`}>
|
|
{isMicOn && (
|
|
<div className="absolute bottom-0 left-0 right-0 bg-emerald-500/30 transition-all duration-75" style={{ height: `${Math.min(audioLevel * 100, 100)}%` }}></div>
|
|
)}
|
|
<div className="relative z-10 flex flex-col items-center">
|
|
{isMicOn ? (
|
|
<svg className="w-5 md:w-6 h-5 md:h-6 mb-0.5 md:mb-1 drop-shadow-md" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path></svg>
|
|
) : (
|
|
<svg className="w-5 md:w-6 h-5 md:h-6 mb-0.5 md:mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 3l18 18M9 9v3m0 0v1m0-1a3 3 0 006 0v-1m0 0V9m0 0v1m0-1a3 3 0 00-6 0m6 3a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4"></path></svg>
|
|
)}
|
|
<span className="text-[8px] md:text-[9px] font-bold uppercase hidden sm:block drop-shadow-md">{isMicOn ? 'Mute' : 'Unmute'}</span>
|
|
</div>
|
|
</button>
|
|
)}
|
|
{uiMatrix?.['jvc.ui.camera'] !== false && (
|
|
<button onClick={handleToggleCamera} className={`flex flex-col items-center justify-center w-12 min-w-[48px] md:w-14 h-12 md:h-14 rounded-2xl transition-all ${!isCameraOn ? 'bg-red-500/20 text-red-500 border border-red-500/30' : 'hover:bg-white/5 text-gray-400 hover:text-white'}`}>
|
|
{isCameraOn ? (
|
|
<svg className="w-5 md:w-6 h-5 md:h-6 mb-0.5 md:mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
|
) : (
|
|
<svg className="w-5 md:w-6 h-5 md:h-6 mb-0.5 md:mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2zM3 3l18 18"></path></svg>
|
|
)}
|
|
<span className="text-[8px] md:text-[9px] font-bold uppercase hidden sm:block">{isCameraOn ? 'Video' : 'Video'}</span>
|
|
</button>
|
|
)}
|
|
{uiMatrix?.['jvc.ui.camera'] !== false && (
|
|
<button onClick={handleFlipCamera} className={`md:hidden flex flex-col items-center justify-center w-12 min-w-[48px] h-12 rounded-2xl transition-all hover:bg-white/5 text-gray-400 hover:text-white shrink-0`}>
|
|
<svg className="w-5 h-5 mb-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="w-[1px] h-6 md:h-8 bg-white/10 mx-1 md:mx-2"></div>
|
|
|
|
{uiMatrix?.['jvc.ui.people_panel'] !== false && (
|
|
<button onClick={() => { setIsParticipantsOpen(!isParticipantsOpen); setIsChatOpen(false); }} className={`flex flex-col items-center justify-center w-12 min-w-[48px] md:w-14 h-12 md:h-14 rounded-2xl transition-all shrink-0 ${isParticipantsOpen ? 'bg-white/10 text-emerald-500' : 'hover:bg-white/5 text-gray-400'}`}>
|
|
<svg className="w-5 md:w-6 h-5 md:h-6 mb-0.5 md:mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
|
|
<span className="text-[8px] md:text-[9px] font-bold uppercase hidden sm:block">People</span>
|
|
</button>
|
|
)}
|
|
|
|
{uiMatrix?.['jvc.ui.chat_panel'] !== false && (
|
|
<button onClick={() => { setIsChatOpen(!isChatOpen); setIsParticipantsOpen(false); }} className={`flex flex-col items-center justify-center w-12 min-w-[48px] md:w-14 h-12 md:h-14 rounded-2xl transition-all shrink-0 ${isChatOpen ? 'bg-white/10 text-emerald-500' : 'hover:bg-white/5 text-gray-400'}`}>
|
|
<svg className="w-5 md:w-6 h-5 md:h-6 mb-0.5 md:mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg>
|
|
<span className="text-[8px] md:text-[9px] font-bold uppercase hidden sm:block">Chat</span>
|
|
</button>
|
|
)}
|
|
|
|
{uiMatrix?.['jvc.ui.screenshare'] !== false && (
|
|
<button onClick={handleToggleScreen} className={`flex flex-col items-center justify-center w-12 min-w-[48px] md:w-14 h-12 md:h-14 rounded-2xl transition-all shrink-0 ${isScreenSharing ? 'bg-emerald-500/20 text-emerald-500' : 'hover:bg-white/5 text-emerald-500'}`}>
|
|
<svg className="w-5 md:w-6 h-5 md:h-6 mb-0.5 md:mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
|
|
<span className="text-[8px] md:text-[9px] font-bold uppercase hidden sm:block">Share</span>
|
|
</button>
|
|
)}
|
|
|
|
{/* PKEPX Zoom-Killer Buttons — ALL controlled by BYOK Matrix */}
|
|
{uiMatrix?.['jvc.ui.reactions'] !== false && (
|
|
<button onClick={() => { if(matrixRef.current) matrixRef.current.emitResonance('👍'); }} className="flex flex-col items-center justify-center w-12 min-w-[48px] md:w-14 h-12 md:h-14 rounded-2xl hover:bg-white/5 text-yellow-400 transition-all shrink-0">
|
|
<svg className="w-5 md:w-6 h-5 md:h-6 mb-0.5 md:mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
|
<span className="text-[8px] md:text-[9px] font-bold uppercase hidden sm:block">React</span>
|
|
</button>
|
|
)}
|
|
|
|
{uiMatrix?.['jvc.ui.breakout'] !== false && (
|
|
<button onClick={() => setIsBreakoutOpen(true)} className="flex flex-col items-center justify-center w-12 min-w-[48px] md:w-14 h-12 md:h-14 rounded-2xl hover:bg-white/5 text-purple-400 transition-all shrink-0">
|
|
<svg className="w-5 md:w-6 h-5 md:h-6 mb-0.5 md:mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
|
|
<span className="text-[8px] md:text-[9px] font-bold uppercase hidden sm:block">Breakout</span>
|
|
</button>
|
|
)}
|
|
|
|
{uiMatrix?.['jvc.ui.recording'] !== false && (
|
|
<button onClick={() => { setIsRecording(!isRecording); if (!isRecording && matrixRef.current) matrixRef.current.triggerQuantumRecording(); }} className={`flex flex-col items-center justify-center w-12 min-w-[48px] md:w-14 h-12 md:h-14 rounded-2xl transition-all shrink-0 ${isRecording ? 'bg-red-500/20 text-red-500 animate-pulse' : 'hover:bg-white/5 text-gray-400'}`}>
|
|
<svg className="w-5 md:w-6 h-5 md:h-6 mb-0.5 md:mb-1" fill="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"></circle></svg>
|
|
<span className="text-[8px] md:text-[9px] font-bold uppercase hidden sm:block">Record</span>
|
|
</button>
|
|
)}
|
|
|
|
<button onClick={() => setIsMoreMenuOpen(!isMoreMenuOpen)} className="flex flex-col items-center justify-center w-12 min-w-[48px] md:w-14 h-12 md:h-14 rounded-2xl hover:bg-white/5 text-gray-400 transition-all shrink-0">
|
|
<svg className="w-5 md:w-6 h-5 md:h-6 mb-0.5 md:mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z"></path></svg>
|
|
<span className="text-[8px] md:text-[9px] font-bold uppercase hidden sm:block">More</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end w-auto md:w-1/4 shrink-0 pl-2">
|
|
|
|
{/* MORE MENU PANEL — Fixed position popup */}
|
|
{isMoreMenuOpen && (
|
|
<div className="fixed inset-0 z-[90]" onClick={() => setIsMoreMenuOpen(false)}>
|
|
<div className="absolute bottom-28 right-4 bg-[#0a101d]/95 backdrop-blur-xl border border-white/10 rounded-2xl p-4 flex flex-col gap-3 min-w-[240px] max-w-[320px] shadow-[0_0_40px_rgba(0,0,0,0.5)]" onClick={(e) => e.stopPropagation()}>
|
|
<div className="text-xs font-bold text-white/50 uppercase tracking-wider mb-2">Settings</div>
|
|
{uiMatrix?.['jvc.ui.beauty_filter'] !== false && (
|
|
<button onClick={() => setUseBeautyFilter(!useBeautyFilter)} className={`flex items-center gap-3 text-sm hover:text-white border p-2 rounded-xl transition-all text-left ${useBeautyFilter ? 'bg-pink-500/20 border-pink-500/50 text-pink-300' : 'bg-white/5 border-transparent text-gray-300 hover:bg-white/10'}`}>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"></path></svg>
|
|
Neural Radiance Filter
|
|
</button>
|
|
)}
|
|
{uiMatrix?.['jvc.ui.virtual_bg'] !== false && (
|
|
<>
|
|
<button onClick={() => setUseVirtualBg(prev => prev === 2 ? 0 : 2)} className={`flex items-center gap-3 text-sm hover:text-white border p-2 rounded-xl transition-all text-left ${useVirtualBg === 2 ? 'bg-indigo-500/20 border-indigo-500/50 text-indigo-300' : 'bg-white/5 border-transparent text-gray-300 hover:bg-white/10'}`}>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
|
Quantum Hologram BG
|
|
</button>
|
|
<button onClick={() => setUseVirtualBg(prev => prev === 4 ? 0 : 4)} className={`flex items-center gap-3 text-sm hover:text-white border p-2 rounded-xl transition-all text-left ${useVirtualBg === 4 ? 'bg-blue-500/20 border-blue-500/50 text-blue-300' : 'bg-white/5 border-transparent text-gray-300 hover:bg-white/10'}`}>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
|
Bokeh Blur BG
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
<div className="h-[1px] bg-white/10 my-1"></div>
|
|
|
|
{uiMatrix?.['jvc.ui.timer'] !== false && (
|
|
<>
|
|
<button onClick={() => { if(matrixRef.current) matrixRef.current.emitResonance('TIMER:300'); setIsMoreMenuOpen(false); }} className="flex items-center gap-3 text-sm text-gray-300 hover:text-white bg-white/5 hover:bg-emerald-500/20 hover:border-emerald-500/50 border border-transparent p-2 rounded-xl transition-all text-left">
|
|
<svg className="w-5 h-5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
|
Start 5 Min Timer
|
|
</button>
|
|
<button onClick={() => { if(matrixRef.current) matrixRef.current.emitResonance('TIMER:600'); setIsMoreMenuOpen(false); }} className="flex items-center gap-3 text-sm text-gray-300 hover:text-white bg-white/5 hover:bg-emerald-500/20 hover:border-emerald-500/50 border border-transparent p-2 rounded-xl transition-all text-left">
|
|
<svg className="w-5 h-5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
|
Start 10 Min Timer
|
|
</button>
|
|
<button onClick={() => { setGlobalTimer(null); if(matrixRef.current) matrixRef.current.emitResonance('TIMER:0'); setIsMoreMenuOpen(false); }} className="flex items-center gap-3 text-sm text-gray-300 hover:text-white bg-white/5 hover:bg-red-500/20 hover:border-red-500/50 border border-transparent p-2 rounded-xl transition-all text-left">
|
|
<svg className="w-5 h-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
|
Clear Timer
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
<div className="h-[1px] bg-white/10 my-1"></div>
|
|
|
|
{uiMatrix?.['jvc.ui.matrix_command'] !== false && (
|
|
<button onClick={() => { setIsMatrixCommandOpen(true); setIsMoreMenuOpen(false); }} className="flex items-center gap-3 text-sm text-gray-300 hover:text-white bg-white/5 hover:bg-white/10 p-2 rounded-xl transition-all text-left">
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>
|
|
XCO Command Matrix
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* XCO MATRIX COMMAND PANEL */}
|
|
{isMatrixCommandOpen && (
|
|
<div className="absolute bottom-24 left-1/2 -translate-x-1/2 bg-black/95 backdrop-blur-2xl border border-emerald-500/30 rounded-2xl p-0 z-50 w-[95vw] max-w-[380px] md:w-[460px] shadow-[0_0_50px_rgba(16,185,129,0.2)] overflow-hidden">
|
|
<div className="flex items-center justify-between p-4 bg-emerald-950/30 border-b border-emerald-500/20">
|
|
<span className="text-[11px] font-black text-emerald-400 uppercase tracking-[0.2em] flex items-center gap-2">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg>
|
|
XCO Matrix Command
|
|
</span>
|
|
<button onClick={() => setIsMatrixCommandOpen(false)} className="text-gray-500 hover:text-white transition-colors">
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex border-b border-white/5">
|
|
<button onClick={() => setMatrixCommandTab('network')} className={`flex-1 py-3 text-[10px] font-bold uppercase tracking-widest transition-colors ${matrixCommandTab === 'network' ? 'bg-white/10 text-emerald-400 border-b-2 border-emerald-500' : 'text-gray-500 hover:bg-white/5 hover:text-gray-300'}`}>Network & Codec</button>
|
|
<button onClick={() => setMatrixCommandTab('quantum')} className={`flex-1 py-3 text-[10px] font-bold uppercase tracking-widest transition-colors ${matrixCommandTab === 'quantum' ? 'bg-white/10 text-fuchsia-400 border-b-2 border-fuchsia-500' : 'text-gray-500 hover:bg-white/5 hover:text-gray-300'}`}>Quantum SDK</button>
|
|
</div>
|
|
|
|
<div className="p-5 max-h-[60vh] overflow-y-auto custom-scroll">
|
|
{matrixCommandTab === 'network' ? (
|
|
<div className="space-y-4">
|
|
{/* REAL Traffic Monitor */}
|
|
<div className="bg-black/60 border border-emerald-500/10 rounded-xl p-3">
|
|
<div className="flex justify-between items-center mb-3">
|
|
<span className="text-[9px] font-black text-emerald-500 uppercase tracking-[0.2em]">Live Network I/O</span>
|
|
<span className={`text-[9px] font-bold px-2 py-0.5 rounded uppercase tracking-wider ${trafficStats.wsState === 'OPEN' ? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30' : 'bg-red-500/20 text-red-400 border border-red-500/30'}`}>{trafficStats.wsState}</span>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-2 text-[9px]">
|
|
<div className="text-gray-600 font-bold"></div>
|
|
<div className="text-center text-cyan-500 font-black tracking-widest">TX ▲</div>
|
|
<div className="text-center text-amber-500 font-black tracking-widest">RX ▼</div>
|
|
|
|
<div className="text-gray-400 font-bold">Video</div>
|
|
<div className="text-center text-cyan-400 font-mono tabular-nums bg-cyan-950/30 py-1 rounded">{formatRate(trafficStats.rates.txVideo)}</div>
|
|
<div className="text-center text-amber-400 font-mono tabular-nums bg-amber-950/30 py-1 rounded">{formatRate(trafficStats.rates.rxVideo)}</div>
|
|
|
|
<div className="text-gray-400 font-bold">Audio</div>
|
|
<div className="text-center text-cyan-400 font-mono tabular-nums bg-cyan-950/30 py-1 rounded">{formatRate(trafficStats.rates.txAudio)}</div>
|
|
<div className="text-center text-amber-400 font-mono tabular-nums bg-amber-950/30 py-1 rounded">{formatRate(trafficStats.rates.rxAudio)}</div>
|
|
|
|
<div className="text-white font-black border-t border-gray-700 pt-2 mt-1">Total</div>
|
|
<div className="text-center text-cyan-300 font-mono font-black border-t border-gray-700 pt-2 mt-1 tabular-nums">{formatRate(trafficStats.rates.txTotal)}</div>
|
|
<div className="text-center text-amber-300 font-mono font-black border-t border-gray-700 pt-2 mt-1 tabular-nums">{formatRate(trafficStats.rates.rxTotal)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Codec Selectors */}
|
|
{uiMatrix?.['jvc.xco.video_engine'] !== false && (
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-[9px] text-gray-500 font-bold uppercase tracking-widest mb-2 block">Video Engine</label>
|
|
<div className="flex flex-col gap-1.5">
|
|
{(['auto', 'canvas', 'webcodecs'] as const).map(mode => (
|
|
<button key={mode} onClick={() => { setVideoEngineMode(mode); if(matrixRef.current) matrixRef.current.hotSwapVideoEngine(mode); }}
|
|
className={`text-[9px] px-3 py-2 rounded-lg font-bold uppercase tracking-wider transition-all text-left ${videoEngineMode === mode ? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/50 shadow-[0_0_15px_rgba(16,185,129,0.2)]' : 'bg-white/5 text-gray-400 hover:text-white border border-transparent hover:bg-white/10'}`}>
|
|
<div className="flex justify-between items-center">
|
|
<span>{mode === 'auto' ? 'Auto-Pilot' : mode === 'canvas' ? 'Pulsar (JPEG)' : 'WebCodecs HW'}</span>
|
|
{videoEngineMode === mode && <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 shadow-[0_0_5px_#10b981]"></span>}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
{uiMatrix?.['jvc.xco.audio_engine'] !== false && (
|
|
<div>
|
|
<label className="text-[9px] text-gray-500 font-bold uppercase tracking-widest mb-2 block">Audio Engine</label>
|
|
<div className="flex flex-col gap-1.5">
|
|
{(['auto', 'xcu-neural', 'pcm'] as const).map(mode => (
|
|
<button key={mode} onClick={() => { setAudioEngineMode(mode as any); if(matrixRef.current) matrixRef.current.hotSwapAudioEngine(mode as any); }}
|
|
className={`text-[9px] px-3 py-2 rounded-lg font-bold uppercase tracking-wider transition-all text-left ${audioEngineMode === mode ? 'bg-cyan-500/20 text-cyan-400 border border-cyan-500/50 shadow-[0_0_15px_rgba(6,182,212,0.2)]' : 'bg-white/5 text-gray-400 hover:text-white border border-transparent hover:bg-white/10'}`}>
|
|
<div className="flex justify-between items-center">
|
|
<span>{mode === 'auto' ? 'Auto-Pilot' : mode === 'xcu-neural' ? 'Neural Matrix' : 'Raw PCM'}</span>
|
|
{audioEngineMode === mode && <span className="w-1.5 h-1.5 rounded-full bg-cyan-500 shadow-[0_0_5px_#06b6d4]"></span>}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* BUG FIX #1: FPS Selector */}
|
|
{uiMatrix?.['jvc.xco.fps_selector'] !== false && (
|
|
<div>
|
|
<label className="text-[9px] text-gray-500 font-bold uppercase tracking-widest mb-2 block">Frame Rate (FPS)</label>
|
|
<div className="flex gap-1.5">
|
|
{([15, 30, 60] as const).map(fps => (
|
|
<button key={fps} onClick={() => { setCurrentFps(fps); if(matrixRef.current) matrixRef.current.setFps(fps); }}
|
|
className={`flex-1 text-[10px] px-3 py-2.5 rounded-lg font-bold transition-all text-center ${currentFps === fps ? 'bg-amber-500/20 text-amber-400 border border-amber-500/50 shadow-[0_0_15px_rgba(245,158,11,0.2)]' : 'bg-white/5 text-gray-400 hover:text-white border border-transparent hover:bg-white/10'}`}>
|
|
<div className="text-sm font-black">{fps}</div>
|
|
<div className="text-[7px] opacity-60">{fps === 15 ? 'HEMAT' : fps === 30 ? 'NORMAL' : 'SMOOTH'}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
<p className="text-[8px] text-gray-600 mt-1.5">15fps = ringan • 30fps = default • 60fps = berat</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div className="bg-fuchsia-950/20 border border-fuchsia-500/20 p-3 rounded-xl mb-4">
|
|
<p className="text-[10px] text-fuchsia-300/80 leading-relaxed font-medium">Modul XCO WASM berjalan secara asinkron (Kernel-Bypass). Fungsionalitas berikut membutuhkan lisensi IAM yang sesuai pada Database BYOK.</p>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{wasmCapabilities.postQuantum && (
|
|
<div className={`p-3 rounded-xl border transition-all ${activeWasmModules.kyber ? 'bg-fuchsia-900/20 border-fuchsia-500/50' : 'bg-white/5 border-white/5'}`}>
|
|
<div className="flex justify-between items-start mb-1">
|
|
<div className="flex items-center gap-2">
|
|
<svg className={`w-4 h-4 ${activeWasmModules.kyber ? 'text-fuchsia-400' : 'text-gray-500'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
|
|
<span className={`text-[11px] font-black uppercase tracking-wider ${activeWasmModules.kyber ? 'text-white' : 'text-gray-400'}`}>Post-Quantum Shield</span>
|
|
</div>
|
|
<button onClick={() => {
|
|
const isActive = !activeWasmModules.kyber;
|
|
setActiveWasmModules(p => ({...p, kyber: isActive}));
|
|
if (isActive) console.log("[WASM] Kyber-1024 & XChaCha20-Poly1305 Activated.");
|
|
}} className={`w-10 h-5 rounded-full relative transition-colors ${activeWasmModules.kyber ? 'bg-fuchsia-500' : 'bg-gray-700'}`}>
|
|
<div className={`absolute top-1 left-1 bg-white w-3 h-3 rounded-full transition-transform ${activeWasmModules.kyber ? 'translate-x-5' : 'translate-x-0'}`}></div>
|
|
</button>
|
|
</div>
|
|
<p className="text-[9px] text-gray-500 pl-6">Melindungi aliran data dari dekripsi komputer kuantum masa depan menggunakan protokol CRYSTALS-Kyber.</p>
|
|
</div>
|
|
)}
|
|
|
|
{wasmCapabilities.aegis && (
|
|
<div className={`p-3 rounded-xl border transition-all ${activeWasmModules.aegis ? 'bg-emerald-900/20 border-emerald-500/50' : 'bg-white/5 border-white/5'}`}>
|
|
<div className="flex justify-between items-start mb-1">
|
|
<div className="flex items-center gap-2">
|
|
<svg className={`w-4 h-4 ${activeWasmModules.aegis ? 'text-emerald-400' : 'text-gray-500'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
|
|
<span className={`text-[11px] font-black uppercase tracking-wider ${activeWasmModules.aegis ? 'text-white' : 'text-gray-400'}`}>Aegis Forensic Watermark</span>
|
|
</div>
|
|
<button onClick={() => {
|
|
const isActive = !activeWasmModules.aegis;
|
|
setActiveWasmModules(p => ({...p, aegis: isActive}));
|
|
if (isActive) console.log(`[WASM] Aegis Matrix Injected. DNA Seed: ${participantId.toString(16)}`);
|
|
}} className={`w-10 h-5 rounded-full relative transition-colors ${activeWasmModules.aegis ? 'bg-emerald-500' : 'bg-gray-700'}`}>
|
|
<div className={`absolute top-1 left-1 bg-white w-3 h-3 rounded-full transition-transform ${activeWasmModules.aegis ? 'translate-x-5' : 'translate-x-0'}`}></div>
|
|
</button>
|
|
</div>
|
|
<p className="text-[9px] text-gray-500 pl-6">Menyuntikkan kedipan morse transparan 1% ke dalam kanvas video untuk mencegah kebocoran rekaman fisik kamera.</p>
|
|
</div>
|
|
)}
|
|
|
|
{wasmCapabilities.neuralWhisper && (
|
|
<div className={`p-3 rounded-xl border transition-all ${activeWasmModules.whisper ? 'bg-cyan-900/20 border-cyan-500/50' : 'bg-white/5 border-white/5'}`}>
|
|
<div className="flex justify-between items-start mb-1">
|
|
<div className="flex items-center gap-2">
|
|
<svg className={`w-4 h-4 ${activeWasmModules.whisper ? 'text-cyan-400' : 'text-gray-500'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path></svg>
|
|
<span className={`text-[11px] font-black uppercase tracking-wider ${activeWasmModules.whisper ? 'text-white' : 'text-gray-400'}`}>Neural Whisper (NPU AI)</span>
|
|
</div>
|
|
<button onClick={() => {
|
|
const isActive = !activeWasmModules.whisper;
|
|
setActiveWasmModules(p => ({...p, whisper: isActive}));
|
|
if (isActive) console.log("[WASM] Local NPU Speech-to-Text Engaged.");
|
|
}} className={`w-10 h-5 rounded-full relative transition-colors ${activeWasmModules.whisper ? 'bg-cyan-500' : 'bg-gray-700'}`}>
|
|
<div className={`absolute top-1 left-1 bg-white w-3 h-3 rounded-full transition-transform ${activeWasmModules.whisper ? 'translate-x-5' : 'translate-x-0'}`}></div>
|
|
</button>
|
|
</div>
|
|
<p className="text-[9px] text-gray-500 pl-6">Menerjemahkan suara ke dalam teks secara offline (Edge AI) tanpa menyentuh server cloud eksternal.</p>
|
|
</div>
|
|
)}
|
|
|
|
{wasmCapabilities.doppler && (
|
|
<div className={`p-3 rounded-xl border transition-all ${activeWasmModules.doppler ? 'bg-amber-900/20 border-amber-500/50' : 'bg-white/5 border-white/5'}`}>
|
|
<div className="flex justify-between items-start mb-1">
|
|
<div className="flex items-center gap-2">
|
|
<svg className={`w-4 h-4 ${activeWasmModules.doppler ? 'text-amber-400' : 'text-gray-500'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"></path><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2"></path></svg>
|
|
<span className={`text-[11px] font-black uppercase tracking-wider ${activeWasmModules.doppler ? 'text-white' : 'text-gray-400'}`}>Doppler Matrix (Ultrasonic)</span>
|
|
</div>
|
|
<button onClick={() => {
|
|
const isActive = !activeWasmModules.doppler;
|
|
setActiveWasmModules(p => ({...p, doppler: isActive}));
|
|
if (isActive) console.log("[WASM] Doppler Matrix (18kHz) Air-gapped fallback armed.");
|
|
}} className={`w-10 h-5 rounded-full relative transition-colors ${activeWasmModules.doppler ? 'bg-amber-500' : 'bg-gray-700'}`}>
|
|
<div className={`absolute top-1 left-1 bg-white w-3 h-3 rounded-full transition-transform ${activeWasmModules.doppler ? 'translate-x-5' : 'translate-x-0'}`}></div>
|
|
</button>
|
|
</div>
|
|
<p className="text-[9px] text-gray-500 pl-6">Saluran komunikasi darurat menggunakan frekuensi suara ultra-tinggi saat 4G/Internet terputus.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<button onClick={() => setIsMatrixCommandOpen(!isMatrixCommandOpen)} className={`flex flex-col items-center justify-center min-w-[56px] md:w-14 h-14 md:h-14 rounded-2xl transition-all ${isMatrixCommandOpen ? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 shadow-[0_0_20px_rgba(16,185,129,0.2)]' : 'hover:bg-white/5 text-gray-400 hover:text-white'}`}>
|
|
<svg className="w-5 h-5 md:w-6 md:h-6 mb-0.5 md:mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
|
<span className="text-[8px] md:text-[9px] font-bold uppercase hidden sm:block">Matrix</span>
|
|
</button>
|
|
<button onClick={() => setIsLeaving(true)} className="flex flex-col items-center justify-center min-w-[56px] md:w-14 h-14 md:h-14 rounded-2xl transition-all hover:bg-red-500/20 text-red-500 border border-transparent hover:border-red-500/30">
|
|
<svg className="w-5 h-5 md:w-6 md:h-6 mb-0.5 md:mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
|
|
<span className="text-[8px] md:text-[9px] font-bold uppercase hidden sm:block">Leave</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Leave Confirmation Popup */}
|
|
{isLeaving && (
|
|
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
|
<div className="bg-[#0f172a] border border-white/10 p-6 md:p-8 rounded-3xl max-w-sm w-full mx-4 shadow-[0_0_50px_rgba(0,0,0,0.5)]">
|
|
<h3 className="text-xl font-bold text-white mb-2">Akhiri Rapat?</h3>
|
|
<p className="text-gray-400 text-sm mb-8">Apakah Anda yakin ingin keluar dari ruangan ini? Koneksi audio dan video Anda akan diputuskan seketika.</p>
|
|
<div className="flex gap-3">
|
|
<button onClick={() => setIsLeaving(false)} className="flex-1 px-4 py-3 bg-white/5 hover:bg-white/10 text-white rounded-xl font-semibold transition-all">
|
|
Batal
|
|
</button>
|
|
<button onClick={() => window.location.href = '/dashboard'} className="flex-1 px-4 py-3 bg-red-600 hover:bg-red-700 text-white rounded-xl font-bold transition-all shadow-[0_0_20px_rgba(220,38,38,0.3)]">
|
|
Keluar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<style dangerouslySetInnerHTML={{__html: `
|
|
.custom-scroll::-webkit-scrollbar { width: 5px; height: 5px; }
|
|
.custom-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
.custom-scroll::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.05); border-radius: 10px; }
|
|
.custom-scroll::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.1); }
|
|
`}} />
|
|
</div>
|
|
);
|
|
}
|