[TSM.ID].[11031972] PXE : Platform X Ecosystem I [118 Module -LIVE-]

This commit is contained in:
TSM.ID
2026-05-25 03:50:05 +07:00
commit e820143b3c
673 changed files with 101320 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
"use client";
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
import React, { createContext, useContext, useEffect, useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
type Theme = 'dark' | 'light';
type Currency = 'Rp' | 'USD' | 'Crypto';
type Locale = 'id' | 'en';
interface OmniContextProps {
theme: Theme;
setTheme: (t: Theme) => void;
currency: Currency;
setCurrency: (c: Currency) => void;
locale: Locale;
setLocale: (l: Locale) => void;
}
const OmniContext = createContext<OmniContextProps | null>(null);
export const useOmni = () => {
const ctx = useContext(OmniContext);
if (!ctx) throw new Error("useOmni must be used within OmniSyncProvider");
return ctx;
};
export function OmniSyncProvider({ children, initialLocale }: { children: React.ReactNode, initialLocale: Locale }) {
const [theme, setThemeState] = useState<Theme>(() => {
if (typeof window === 'undefined') return 'dark';
const value = `; ${document.cookie}`;
const parts = value.split(`; omni_theme=`);
if (parts.length === 2) return (parts.pop()?.split(';').shift() as Theme) || 'dark';
return 'dark';
});
const [currency, setCurrencyState] = useState<Currency>(() => {
if (typeof window === 'undefined') return 'Rp';
const value = `; ${document.cookie}`;
const parts = value.split(`; omni_currency=`);
if (parts.length === 2) return (parts.pop()?.split(';').shift() as Currency) || 'Rp';
return 'Rp';
});
const [locale, setLocaleState] = useState<Locale>(initialLocale);
const router = useRouter();
const pathname = usePathname();
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
const setCookie = (name: string, value: string) => {
const host = window.location.hostname;
const cookieDomain = process.env.NEXT_PUBLIC_COOKIE_DOMAIN || (host === 'localhost' || host === '127.0.0.1' ? host : `.${host}`);
document.cookie = `${name}=${value}; path=/; domain=${cookieDomain}; max-age=31536000`;
// For local dev fallback
if (window.location.hostname === 'localhost') {
document.cookie = `${name}=${value}; path=/; max-age=31536000`;
}
};
const setTheme = (t: Theme) => {
setThemeState(t);
setCookie('omni_theme', t);
document.documentElement.setAttribute('data-theme', t);
if (channel) channel.postMessage({ type: 'SYNC_THEME', payload: t });
};
const setCurrency = (c: Currency) => {
setCurrencyState(c);
setCookie('omni_currency', c);
if (channel) channel.postMessage({ type: 'SYNC_CURRENCY', payload: c });
};
const setLocale = (l: Locale) => {
setLocaleState(l);
setCookie('NEXT_LOCALE', l); // next-intl standard cookie
if (channel) channel.postMessage({ type: 'SYNC_LOCALE', payload: l });
};
// Broadcast Channel for Cross-Tab Sync
const [channel] = useState<BroadcastChannel | null>(() => typeof window !== 'undefined' ? new BroadcastChannel('omni_sync_channel') : null);
useEffect(() => {
if (channel) {
const handleMessage = (event: MessageEvent) => {
const { type, payload } = event.data;
if (type === 'SYNC_THEME') {
setThemeState(payload);
document.documentElement.setAttribute('data-theme', payload);
}
if (type === 'SYNC_CURRENCY') {
setCurrencyState(payload);
}
if (type === 'SYNC_LOCALE') {
setLocaleState(payload);
}
};
channel.addEventListener('message', handleMessage);
return () => {
channel.removeEventListener('message', handleMessage);
};
}
}, [channel, pathname, router]);
return (
<OmniContext.Provider value={{ theme, setTheme, currency, setCurrency, locale, setLocale }}>
{children}
</OmniContext.Provider>
);
}
+363
View File
@@ -0,0 +1,363 @@
/* eslint-disable */
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { Socket } from '../lib/zero-socket';
interface QuantumCallProps {
room: string;
socket: Socket | null;
username: string;
onClose: () => void;
isAudioOnly?: boolean;
}
export function QuantumP2PCall({ room, socket, username, onClose, isAudioOnly = false }: QuantumCallProps) {
const localVideoRef = useRef<HTMLVideoElement>(null);
const remoteVideoRef = useRef<HTMLVideoElement>(null);
const pcRef = useRef<RTCPeerConnection | null>(null);
const localStreamRef = useRef<MediaStream | null>(null);
const remoteStreamRef = useRef<MediaStream | null>(null);
// Audio Analyser Refs
const audioContextRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const dataArrayRef = useRef<Uint8Array | null>(null);
const animationFrameRef = useRef<number>(0);
const [isMicOn, setIsMicOn] = useState(true);
const [isCameraOn, setIsCameraOn] = useState(!isAudioOnly);
const [connectionState, setConnectionState] = useState<string>('Membuka Terowongan Kuantum...');
// Audio Aura Level (0 to 100)
const [remoteAudioLevel, setRemoteAudioLevel] = useState<number>(0);
// Zero-UI Cinematic Mode
const [showControls, setShowControls] = useState(true);
const idleTimerRef = useRef<NodeJS.Timeout | null>(null);
// Draggable PiP (Picture in Picture)
const [pipPos, setPipPos] = useState({ x: window.innerWidth - 220, y: window.innerHeight - 320 });
const [isDragging, setIsDragging] = useState(false);
const dragOffset = useRef({ x: 0, y: 0 });
const resetIdleTimer = useCallback(() => {
setShowControls(true);
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
idleTimerRef.current = setTimeout(() => setShowControls(false), 3000);
}, []);
useEffect(() => {
window.addEventListener('mousemove', resetIdleTimer);
resetIdleTimer();
return () => {
window.removeEventListener('mousemove', resetIdleTimer);
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
};
}, [resetIdleTimer]);
useEffect(() => {
if (!socket) return;
// Phase 83: Omni-Relay Inject
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{
urls: 'turn:160.187.143.172:3478',
username: 'xcu ULTRA',
credential: 'quantum_mesh'
}
]
};
const pc = new RTCPeerConnection(configuration);
pcRef.current = pc;
pc.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('quantum_candidate', {
target: getTargetFromRoom(room),
sender: username,
candidate: event.candidate,
room
});
}
};
pc.onconnectionstatechange = () => {
setConnectionState(pc.connectionState);
};
pc.ontrack = (event) => {
if (remoteVideoRef.current && event.streams[0]) {
remoteStreamRef.current = event.streams[0];
remoteVideoRef.current.srcObject = event.streams[0];
setupAudioAnalyser(event.streams[0]);
}
};
navigator.mediaDevices.getUserMedia({
video: !isAudioOnly ? { width: { ideal: 1280 }, height: { ideal: 720 } } : false,
audio: true
}).then((stream) => {
localStreamRef.current = stream;
if (localVideoRef.current && !isAudioOnly) {
localVideoRef.current.srcObject = stream;
}
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
createOffer();
}).catch(e => {
console.error("Gagal mengakses media:", e);
setConnectionState("Akses Kamera/Mic Ditolak!");
});
socket.on('quantum_offer_received', async (data: any) => {
if (data.caller !== username && pcRef.current) {
await pcRef.current.setRemoteDescription(new RTCSessionDescription(data.sdp));
const answer = await pcRef.current.createAnswer();
await pcRef.current.setLocalDescription(answer);
socket.emit('quantum_answer', {
target: data.caller,
responder: username,
sdp: answer,
room
});
}
});
socket.on('quantum_answer_received', async (data: any) => {
if (data.responder !== username && pcRef.current) {
await pcRef.current.setRemoteDescription(new RTCSessionDescription(data.sdp));
}
});
socket.on('quantum_candidate_received', async (data: any) => {
if (data.sender !== username && pcRef.current) {
try {
await pcRef.current.addIceCandidate(new RTCIceCandidate(data.candidate));
} catch(e) { console.error(e); }
}
});
socket.on('quantum_call_ended_broadcast', (data: any) => {
if (data.sender !== username) {
handleEndCall();
}
});
return () => {
socket.off('quantum_offer_received');
socket.off('quantum_answer_received');
socket.off('quantum_candidate_received');
socket.off('quantum_call_ended_broadcast');
cleanup();
};
}, [socket, room]);
const setupAudioAnalyser = (stream: MediaStream) => {
try {
const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
const analyser = audioCtx.createAnalyser();
analyser.fftSize = 256;
const source = audioCtx.createMediaStreamSource(stream);
source.connect(analyser);
audioContextRef.current = audioCtx;
analyserRef.current = analyser;
dataArrayRef.current = new Uint8Array(analyser.frequencyBinCount);
const updateAura = () => {
if (!analyserRef.current || !dataArrayRef.current) return;
analyserRef.current.getByteFrequencyData(dataArrayRef.current as any);
let sum = 0;
for (let i = 0; i < dataArrayRef.current.length; i++) {
sum += dataArrayRef.current[i];
}
const average = sum / dataArrayRef.current.length;
// Normalize 0-100
const level = Math.min(100, Math.round((average / 255) * 100 * 2));
setRemoteAudioLevel(level);
animationFrameRef.current = requestAnimationFrame(updateAura);
};
updateAura();
} catch (e) {
console.error("Audio Context Error", e);
}
};
const cleanup = () => {
if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
if (audioContextRef.current) audioContextRef.current.close();
if (localStreamRef.current) localStreamRef.current.getTracks().forEach(track => track.stop());
if (pcRef.current) pcRef.current.close();
};
const createOffer = async () => {
if (!pcRef.current || !socket) return;
try {
const offer = await pcRef.current.createOffer();
await pcRef.current.setLocalDescription(offer);
socket.emit('quantum_offer', {
target: getTargetFromRoom(room),
caller: username,
sdp: offer,
room
});
} catch (e) {
console.error("Offer creation failed", e);
}
};
const getTargetFromRoom = (roomId: string) => {
if (roomId.startsWith('DM_')) {
const users = roomId.replace('DM_', '').split('_');
return users.find(u => u !== username) || '';
}
return '';
};
const toggleMic = () => {
if (localStreamRef.current) {
localStreamRef.current.getAudioTracks().forEach(track => track.enabled = !isMicOn);
setIsMicOn(!isMicOn);
}
};
const toggleCamera = () => {
if (localStreamRef.current) {
localStreamRef.current.getVideoTracks().forEach(track => track.enabled = !isCameraOn);
setIsCameraOn(!isCameraOn);
}
};
const handleEndCall = () => {
if (socket) {
socket.emit('quantum_call_ended', { sender: username, room, target: getTargetFromRoom(room) });
}
cleanup();
onClose();
};
// Drag Logic
const handleMouseDown = (e: React.MouseEvent) => {
setIsDragging(true);
dragOffset.current = {
x: e.clientX - pipPos.x,
y: e.clientY - pipPos.y
};
};
const handleMouseMove = (e: React.MouseEvent) => {
if (!isDragging) return;
setPipPos({
x: e.clientX - dragOffset.current.x,
y: e.clientY - dragOffset.current.y
});
};
const handleMouseUp = () => setIsDragging(false);
// Deep Sapphire Blue Aura Calculation
const auraBlur = 20 + (remoteAudioLevel * 0.8);
const auraSpread = remoteAudioLevel * 0.5;
const boxShadowStyle = `0 0 ${auraBlur}px ${auraSpread}px rgba(15, 82, 186, ${remoteAudioLevel > 10 ? 0.6 : 0.1})`;
return (
<div
className="absolute inset-0 z-[100] flex items-center justify-center pointer-events-auto bg-black/70 backdrop-blur-xl overflow-hidden"
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
{/* Status Bar */}
<div className={`absolute top-6 left-6 text-blue-300 text-xs font-mono bg-blue-900/30 px-4 py-2 rounded-full border border-blue-500/20 shadow-[0_0_15px_rgba(15,82,186,0.5)] transition-opacity duration-700 ${showControls ? 'opacity-100' : 'opacity-0'}`}>
<span className="animate-pulse mr-2"></span> {connectionState}
</div>
<div className="w-full h-full relative flex items-center justify-center p-8">
{/* Main Remote Video (Deep Sapphire Blue Aura) */}
<div
className="relative w-full max-w-5xl aspect-video rounded-[2rem] overflow-hidden transition-all duration-300"
style={{ boxShadow: boxShadowStyle }}
>
{/* Glassmorphism Border */}
<div className="absolute inset-0 rounded-[2rem] border-[1px] border-white/10 z-10 pointer-events-none"></div>
<video
ref={remoteVideoRef}
autoPlay
playsInline
className="w-full h-full object-cover"
style={{ filter: 'contrast(1.05) saturate(1.1)' }}
/>
{/* Fallback Audio Only Avatar */}
{isAudioOnly && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-900">
<div className="w-32 h-32 rounded-full bg-blue-600/20 flex items-center justify-center" style={{ transform: `scale(${1 + remoteAudioLevel/100})`, transition: 'transform 0.1s ease-out' }}>
<span className="text-5xl text-blue-400 font-bold">{getTargetFromRoom(room).charAt(0).toUpperCase()}</span>
</div>
</div>
)}
</div>
{/* Draggable PiP (Local Video) */}
<div
className="absolute w-48 h-72 rounded-2xl overflow-hidden cursor-move border-[1px] border-white/20 shadow-[0_20px_50px_rgba(0,0,0,0.5)] z-30 transition-shadow hover:shadow-[0_0_20px_rgba(255,255,255,0.2)]"
style={{
left: pipPos.x, top: pipPos.y,
backdropFilter: 'blur(20px)',
backgroundColor: 'rgba(15, 23, 42, 0.4)'
}}
onMouseDown={handleMouseDown}
>
{isCameraOn ? (
<video
ref={localVideoRef}
autoPlay
playsInline
muted
className="w-full h-full object-cover transform -scale-x-100"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<svg className="w-10 h-10 text-white/30" 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>
</div>
)}
</div>
{/* Cinematic Controls (Zero-UI) */}
<div className={`absolute bottom-10 left-1/2 -translate-x-1/2 flex items-center gap-6 bg-slate-900/60 backdrop-blur-2xl px-10 py-5 rounded-full border border-white/5 shadow-2xl z-40 transition-all duration-700 transform ${showControls ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0 pointer-events-none'}`}>
{/* Mic Toggle */}
<button onClick={toggleMic} className={`relative group w-14 h-14 rounded-full flex items-center justify-center transition-all duration-300 ${isMicOn ? 'bg-white/10 hover:bg-white/20 text-white' : 'bg-red-500/80 text-white shadow-[0_0_20px_rgba(239,68,68,0.4)]'}`}>
{isMicOn ? (
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5-3c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-1.7z"/></svg>
) : (
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c-.37-.05-.74-.12-1.1-.22l2.83 2.83 1.27-1.27L4.27 3z"/></svg>
)}
</button>
{/* Camera Toggle */}
<button onClick={toggleCamera} className={`relative group w-14 h-14 rounded-full flex items-center justify-center transition-all duration-300 ${isCameraOn ? 'bg-white/10 hover:bg-white/20 text-white' : 'bg-red-500/80 text-white shadow-[0_0_20px_rgba(239,68,68,0.4)]'}`}>
{isCameraOn ? (
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>
) : (
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M21 6.5l-4 4V7c0-.55-.45-1-1-1H9.82L21 17.18V6.5zM3.27 2L2 3.27 4.73 6H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 .64-.15 1.14-.39L20.73 22 22 20.73 3.27 2z"/></svg>
)}
</button>
{/* End Call */}
<button onClick={handleEndCall} className="w-16 h-16 rounded-full flex items-center justify-center bg-gradient-to-r from-red-600 to-rose-600 hover:from-red-500 hover:to-rose-500 text-white shadow-[0_0_30px_rgba(225,29,72,0.6)] transition-all duration-300 transform hover:scale-110">
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24"><path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08c-.18-.17-.29-.42-.29-.7 0-.28.11-.53.29-.71C3.34 8.78 7.46 7 12 7s8.66 1.78 11.71 4.67c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-2.48 2.48c-.18.18-.43.29-.71.29-.27 0-.52-.11-.7-.28-.79-.74-1.69-1.36-2.67-1.85-.33-.16-.56-.5-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z"/></svg>
</button>
</div>
</div>
</div>
);
}
+251
View File
@@ -0,0 +1,251 @@
/* eslint-disable */
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
"use client";
import { useEffect, useState, useRef } from "react";
import { XCUTelepathyMatrix, DecryptedMessage } from "../lib/xcu-telepathy-matrix";
export function XCUltraChat({ roomName, username }: { roomName: string, username: string }) {
const [logs, setLogs] = useState<string[]>([]);
const [isMatrixActive, setIsMatrixActive] = useState(false);
const [chatInput, setChatInput] = useState("");
const [chatMessages, setChatMessages] = useState<{id: string, sender: string, text: string, time: string}[]>([]);
const [typingUsers, setTypingUsers] = useState<string[]>([]);
const [activeReactions, setActiveReactions] = useState<{id: number, type: string, ts: number}[]>([]);
const chatEndRef = useRef<HTMLDivElement>(null);
const matrixRef = useRef<XCUTelepathyMatrix | null>(null);
const addLog = (msg: string) => {
setLogs(prev => [...prev, msg]);
};
const setStatus = (s: string) => addLog(`[SYS] ${s}`);
useEffect(() => {
if (chatEndRef.current) {
chatEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [chatMessages, typingUsers]);
useEffect(() => {
let matrix: XCUTelepathyMatrix | null = null;
const initMatrix = async () => {
try {
setStatus("Memulai Koneksi XCU Telepathy Matrix (XTM) E2EE...");
matrix = new XCUTelepathyMatrix(roomName);
matrixRef.current = matrix;
matrix.onMessagesUpdate = (messages: DecryptedMessage[]) => {
// Format time based on timestamp
const formatted = messages.map(m => {
const date = new Date(m.timestamp);
return {
id: m.id,
sender: m.sender,
text: m.content,
time: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
};
});
// CRDT arrays are append-only mostly for chat, but we sync everything
setChatMessages(formatted);
};
matrix.onTypingUpdate = (typings: Record<string, number>) => {
const active = Object.keys(typings).filter(key => key !== username);
setTypingUsers(active);
};
// PKEPX Zoom-Killer Handlers
matrix.onQuantumResonance = (id, type) => {
setActiveReactions(prev => [...prev, { id, type, ts: Date.now() }]);
setTimeout(() => {
setActiveReactions(prev => prev.filter(r => Date.now() - r.ts < 4000));
}, 4000);
};
matrix.onSovereignSignal = (command, targetId) => {
if (command === 'MUTE_ALL') {
setStatus("Host telah mengunci matriks komunikasi (Sovereign Lock).");
}
};
// Determine server URL dynamically for backup WebSocket
const proto = window.location.protocol;
const host = window.location.host;
let serverUrl = `${proto}//${host}`;
if (typeof window === 'undefined' || host === '') {
serverUrl = 'http://localhost:4003';
}
matrix.ignite(serverUrl, username);
setIsMatrixActive(true);
setStatus("XTM CRDT Engine Siap. Enkripsi AES-256 Aktif.");
} catch (e: any) {
console.error("XTM ERROR DETAILS:", e, e.name, e.message);
setStatus("Koneksi Gagal: " + e.name + " - " + e.message);
}
};
initMatrix();
return () => {
// Dihapus untuk mencegah React 18 race condition yang memotong koneksi QUIC secara prematur
if (matrixRef.current) {
// matrixRef.current.shutdown();
}
};
}, []);
// (Moved setStatus up)
const handleSendChat = async (e?: React.FormEvent) => {
if (e) e.preventDefault();
if (!chatInput.trim() || !matrixRef.current) return;
const currentInput = chatInput;
// XTM Send Message (Automatically encrypted and synced via CRDT)
matrixRef.current.sendMessage(username, currentInput);
setChatInput("");
// FASE 87: OMNIBRAIN AVATAR PROTOCOL (CLIENT-SIDE TELEPATHY)
if (currentInput.includes('@OmniBrain')) {
// The browser acts as the medium for the AI to preserve E2EE
matrixRef.current.sendMessage('OmniBrain', 'Sedang mengurai matriks kuantum...');
try {
// Take last 5 decrypted messages for context
const recentHistory = chatMessages.slice(-5);
const res = await fetch('/api/omnibrain', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
history: recentHistory,
prompt: currentInput.replace('@OmniBrain', '').trim(),
sender: username
})
});
const data = await res.json();
if (data.reply && matrixRef.current) {
matrixRef.current.sendMessage('OmniBrain', data.reply);
}
} catch (err) {
if (matrixRef.current) {
matrixRef.current.sendMessage('OmniBrain', 'Gagal memanggil entitas di peladen pusat.');
}
}
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
setChatInput(val);
if (matrixRef.current && val.length > 0) {
matrixRef.current.setTyping(username, val.length);
}
};
return (
<div className="h-dvh w-full bg-[#0a0c10] text-slate-200 font-sans flex flex-col overflow-hidden relative">
{/* Top Banner */}
<div className="h-16 border-b border-white/5 bg-[#14161f] flex items-center justify-between px-6 shadow-md z-20 shrink-0">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-gradient-to-br from-cyan-600 to-blue-700 rounded-xl flex items-center justify-center text-white shadow-[0_0_15px_rgba(8,145,178,0.4)]">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"></path></svg>
</div>
<div>
<h1 className="text-base font-bold tracking-wider uppercase text-cyan-400">XCU OMNI CHAT</h1>
<p className="text-xs text-slate-500 font-mono">{roomName}</p>
</div>
</div>
<div className="flex items-center gap-4 text-xs font-medium">
{/* PKEPX Resonance Button */}
<button onClick={() => { if(matrixRef.current) matrixRef.current.emitResonance('👍'); }} className="p-2 bg-yellow-500/10 hover:bg-yellow-500/20 text-yellow-500 rounded-lg transition-colors border border-yellow-500/20 shadow-[0_0_10px_rgba(234,179,8,0.2)]">
👍 React
</button>
<span className="bg-cyan-900/30 text-cyan-400 px-3 py-1.5 rounded-lg border border-cyan-500/30 flex items-center gap-2 shadow-[0_0_10px_rgba(8,145,178,0.2)]">
<div className="w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
CRDT QUANTUM ENGINE
</span>
</div>
</div>
{/* PKEPX Floating Emojis */}
{activeReactions.map(r => (
<div key={r.ts} className="absolute inset-0 flex items-center justify-center pointer-events-none z-[100] animate-[ping_1s_ease-out]">
<span className="text-6xl drop-shadow-[0_0_30px_rgba(255,255,255,0.5)] animate-bounce">{r.type}</span>
</div>
))}
{!isMatrixActive ? (
<div className="flex-1 flex flex-col items-center justify-center gap-4 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-slate-900 to-[#0a0c10]">
<div className="w-16 h-16 border-4 border-slate-800 border-t-cyan-500 rounded-full animate-spin shadow-[0_0_30px_rgba(8,145,178,0.5)]"></div>
<div className="text-sm font-mono text-cyan-500 animate-pulse tracking-widest">{logs[logs.length-1] || "INisialisasi..."}</div>
</div>
) : (
<div className="flex-1 flex flex-col bg-transparent overflow-hidden">
<div className="flex-1 overflow-y-auto p-6 space-y-6 custom-scroll">
{chatMessages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-slate-600 opacity-50">
<svg className="w-16 h-16 mb-4 text-cyan-900" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path></svg>
<div className="text-center text-sm font-mono tracking-widest uppercase">P2P Kuantum Siap. 0ms Latensi.</div>
</div>
) : (
chatMessages.map(msg => {
const isMe = msg.sender === username;
return (
<div key={msg.id} className={`flex flex-col ${isMe ? 'items-end' : 'items-start'} animate-in fade-in slide-in-from-bottom-2`}>
<div className={`px-5 py-3 rounded-2xl max-w-[80%] text-[15px] leading-relaxed shadow-lg ${isMe ? 'bg-gradient-to-br from-cyan-600 to-blue-700 text-white rounded-br-sm shadow-cyan-900/20' : 'bg-[#1a1d24] text-slate-200 border border-white/5 rounded-bl-sm'}`}>
<div className="text-[10px] opacity-60 mb-1.5 font-mono tracking-wider uppercase flex items-center gap-2">
{isMe ? 'Anda' : msg.sender}
<span className="w-1 h-1 bg-white/30 rounded-full"></span>
{msg.time}
</div>
<p className="break-words">{msg.text}</p>
</div>
</div>
);
})
)}
{/* FASE 86: XTM Typing Indicator */}
{typingUsers.length > 0 && (
<div className="text-xs text-cyan-500 font-mono tracking-widest px-2 animate-pulse mt-1 mb-2">
{typingUsers.join(", ")} sedang merajut matriks teks...
</div>
)}
<div ref={chatEndRef} />
</div>
<div className="p-5 border-t border-white/5 bg-[#14161f] shrink-0">
<form onSubmit={handleSendChat} className="flex items-center gap-3">
<div className="flex-1 bg-[#0a0c10] rounded-xl px-5 py-3 border border-white/10 focus-within:border-cyan-500/50 focus-within:shadow-[0_0_15px_rgba(8,145,178,0.2)] transition-all flex items-center">
<input
type="text"
value={chatInput}
onChange={handleInputChange}
className="flex-1 bg-transparent text-sm text-slate-200 outline-none placeholder:text-slate-600"
placeholder="Transmisikan matriks teks E2EE..."
autoFocus
/>
</div>
<button title="Aksi" type="submit" disabled={!chatInput.trim()} className="bg-cyan-600 hover:bg-cyan-500 text-white p-3.5 rounded-xl disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-[0_0_15px_rgba(8,145,178,0.3)] flex items-center justify-center hover:scale-105 active:scale-95">
<svg className="w-5 h-5 translate-x-0.5" fill="currentColor" viewBox="0 0 20 20"><path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"></path></svg>
</button>
</form>
</div>
</div>
)}
<style dangerouslySetInnerHTML={{__html: `
.custom-scroll::-webkit-scrollbar { width: 6px; }
.custom-scroll::-webkit-scrollbar-track { background: transparent; }
.custom-scroll::-webkit-scrollbar-thumb { background: rgba(34, 211, 238, 0.2); border-radius: 6px; }
.custom-scroll::-webkit-scrollbar-thumb:hover { background: rgba(34, 211, 238, 0.4); }
`}} />
</div>
);
}