[TSM.ID].[11031972] PXE : Platform X Ecosystem I [118 Module -LIVE-]
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user