import React, { useEffect, useRef, useState, useMemo, Suspense } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; interface ErrorBoundaryProps { children: React.ReactNode; fallback: React.ReactNode; } interface ErrorBoundaryState { hasError: boolean; } class ModelErrorBoundary extends React.Component { constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch(error: any, errorInfo: any) { console.error("🚨 [ModelErrorBoundary] Falha de renderização/rede no modelo:", error, errorInfo); } render() { if (this.state.hasError) { return this.props.fallback; } return this.props.children; } } import { Canvas, useFrame } from '@react-three/fiber'; import { OrbitControls, Text, useGLTF } from '@react-three/drei'; import * as THREE from 'three'; import { supabase } from '@/integrations/supabase/client'; import { useModelStore } from '@/stores/useModelStore'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; import { toast } from 'sonner'; import { Users, MessageSquare, Mic, MicOff, Home, Send, ArrowRight, ChevronLeft, ChevronRight, Share2, LogOut, RotateCw, Award, ShieldAlert } from 'lucide-react'; // Interfaces tipadas para evitar any interface AvatarPreset { id: number; name: string; desc: string; color: string; } interface PresentationState { rotation: number[]; scale: number; sectionEnabled: boolean; sectionAxis: string; sectionLevel: number; sectionInvert: boolean; } interface PeerData { username: string; avatarId: number; chairId: string; role: string; cargo: string; empresa: string; presence_ref: string; } // --- Configurações da Mesa de Reunião --- const CHAIRS = [ { id: '1', name: 'Assento 1', pos: [-1.2, 0.95, -1.45], lookAt: [0, 0.95, 0] }, { id: '2', name: 'Assento 2', pos: [-0.4, 0.95, -1.45], lookAt: [0, 0.95, 0] }, { id: '3', name: 'Assento 3', pos: [0.4, 0.95, -1.45], lookAt: [0, 0.95, 0] }, { id: '4', name: 'Assento 4', pos: [1.2, 0.95, -1.45], lookAt: [0, 0.95, 0] }, { id: '5', name: 'Assento 5', pos: [1.2, 0.95, 1.45], lookAt: [0, 0.95, 0] }, { id: '6', name: 'Assento 6', pos: [0.4, 0.95, 1.45], lookAt: [0, 0.95, 0] }, { id: '7', name: 'Assento 7', pos: [-0.4, 0.95, 1.45], lookAt: [0, 0.95, 0] }, { id: '8', name: 'Assento 8', pos: [-1.2, 0.95, 1.45], lookAt: [0, 0.95, 0] }, ]; const AVATAR_PRESETS: AvatarPreset[] = [ { id: 1, name: 'Cyber', desc: 'Cabelo curto preto e óculos vermelhos', color: '#ef4444' }, { id: 2, name: 'Classic', desc: 'Pele média com Cabelo castanho clássico', color: '#b45309' }, { id: 3, name: 'Goldie', desc: 'Cabelo loiro longo com olhos azuis expressivos', color: '#eab308' }, { id: 4, name: 'Sporty', desc: 'Boné azul virado para trás e sorriso aberto', color: '#3b82f6' }, { id: 5, name: 'Ginger', desc: 'Cabelo ruivo longo ondulado e expressivo', color: '#f97316' }, { id: 6, name: 'Sleek', desc: 'Pele parda com cabelo curto liso lateral', color: '#10b981' }, { id: 7, name: 'Beard', desc: 'Careca estilosa com barba e bigode', color: '#78350f' }, { id: 8, name: 'Sun', desc: 'Cabelo raspado e óculos escuros pretos', color: '#1e293b' }, { id: 9, name: 'Synth', desc: 'Cabelo roxo ondulado moderno', color: '#a855f7' }, { id: 10, name: 'Straw', desc: 'Chapéu de palha do campo clássico', color: '#ca8a04' }, ]; const SKIN_COLORS = ['#fed7aa', '#fdba74', '#f59e0b', '#d97706', '#9a3412', '#ffedd5']; // Classes Tailwind estáticas para evitar inline styles no list view dos avatares const getAvatarBgClass = (avatarId: number): string => { const classes = [ 'bg-red-500/20 text-red-400', 'bg-amber-800/20 text-amber-500', 'bg-yellow-500/20 text-yellow-400', 'bg-blue-500/20 text-blue-400', 'bg-orange-500/20 text-orange-400', 'bg-emerald-500/20 text-emerald-400', 'bg-amber-900/20 text-amber-600', 'bg-slate-700/20 text-slate-400', 'bg-purple-500/20 text-purple-400', 'bg-yellow-600/20 text-yellow-500' ]; return classes[(avatarId - 1) % 10] || classes[0]; }; // --- Subcomponentes 3D --- // Avatar estilo Mii (Nintendo Wii) - Versão Melhorada (Mais Feliz e Expressivo) function MiiAvatar({ avatarId, username, isTalking, scale = 1, ...props }: { avatarId: number; username: string; isTalking?: boolean; scale?: number } & Record) { const groupRef = useRef(null); const mii = useMemo(() => { const skin = SKIN_COLORS[(avatarId - 1) % SKIN_COLORS.length]; return { skin, hairColor: avatarId === 3 ? '#eab308' : avatarId === 9 ? '#a855f7' : avatarId === 5 ? '#f97316' : '#27272a', hasGlasses: avatarId === 1 || avatarId === 8, glassesColor: avatarId === 8 ? '#18181b' : '#ef4444', hasCap: avatarId === 4, capColor: '#3b82f6', hasHat: avatarId === 10, hatColor: '#b45309', hairStyle: avatarId === 7 ? 'bald' : (avatarId === 9 ? 'modern' : (avatarId % 3 === 0 ? 'long' : 'short')), beard: avatarId === 7, }; }, [avatarId]); useFrame((state) => { if (groupRef.current && props.position && Array.isArray(props.position) && props.position.length >= 2) { groupRef.current.position.y = (props.position[1] as number) + Math.sin(state.clock.getElapsedTime() * 2 + avatarId) * 0.012; } }); const positionVector = useMemo(() => { if (props.position && Array.isArray(props.position)) { return new THREE.Vector3(props.position[0] as number, props.position[1] as number, props.position[2] as number); } return new THREE.Vector3(0, 0, 0); }, [props.position]); const rotationEuler = useMemo(() => { if (props.rotation && Array.isArray(props.rotation)) { return new THREE.Euler(props.rotation[0] as number, props.rotation[1] as number, props.rotation[2] as number); } return new THREE.Euler(0, 0, 0); }, [props.rotation]); return ( {/* Balão de Nome acima da cabeça */} {username} {/* Indicador de Fala (anel pulsante) */} {isTalking && ( )} {/* CABEÇA AVATAR (Esfera ovalada de pele) */} {/* CABELO MELHORADO */} {mii.hairStyle === 'short' && ( {/* Topo do cabelo */} {/* Costeletas laterais */} )} {mii.hairStyle === 'long' && ( {/* Cabelo Superior */} {/* Franja frontal */} {/* Costeletas longas */} )} {mii.hairStyle === 'modern' && ( {/* Cabelo Superior */} {/* Topete Moderno */} {/* Costeletas */} )} {/* BONÉ */} {mii.hasCap && ( {/* Copa do Boné */} {/* Aba do Boné */} )} {/* CHAPÉU DE PALHA */} {mii.hasHat && ( {/* Copa do Chapéu */} {/* Aba do Chapéu */} )} {/* OLHOS MELHORADOS (Mais expressivos e com brilho) */} {/* SOBRANCELHAS */} {/* NARIZ */} {/* BOCHECHAS ROSADAS (Deixa o avatar super amigável e feliz) */} {/* BOCA FELIZ E SORRIDENTE (Sem aparência de triste) */} {/* BARBA E BIGODE */} {mii.beard && ( )} {/* ÓCULOS */} {mii.hasGlasses && ( )} ); } // Carregador e Escalador Inteligente da Maquete Holográfica function HologramModel({ model, presentationState }: { model: { url: string }; presentationState: PresentationState }) { const { scene } = useGLTF(model.url); const clone = useMemo(() => scene.clone(), [scene]); const groupRef = useRef(null); // Calcula escala de aproximadamente 1 metro para caber na mesa useEffect(() => { if (!groupRef.current) return; const box = new THREE.Box3().setFromObject(clone); const size = new THREE.Vector3(); box.getSize(size); const maxDim = Math.max(size.x, size.y, size.z); if (maxDim > 0) { const targetSize = 0.95; const scaleFactor = targetSize / maxDim; groupRef.current.scale.setScalar(scaleFactor); const center = new THREE.Vector3(); box.getCenter(center); clone.position.copy(center).multiplyScalar(-scaleFactor); clone.position.y += 0.02; } }, [clone]); // Aplica transformações de Rotação e Escala compartilhadas useFrame(() => { if (groupRef.current && presentationState) { const { rotation, scale } = presentationState; if (rotation) { groupRef.current.rotation.x = rotation[0]; groupRef.current.rotation.y = rotation[1]; groupRef.current.rotation.z = rotation[2]; } if (scale) { const box = new THREE.Box3().setFromObject(clone); const size = new THREE.Vector3(); box.getSize(size); const maxDim = Math.max(size.x, size.y, size.z); if (maxDim > 0) { const baseScale = 0.95 / maxDim; groupRef.current.scale.setScalar(baseScale * scale); } } } }); // Aplica Planos de Corte (clippingPlanes) nos materiais useEffect(() => { if (!clone || !presentationState) return; const { sectionEnabled, sectionAxis, sectionLevel, sectionInvert } = presentationState; if (!sectionEnabled) { clone.traverse((obj) => { if (obj instanceof THREE.Mesh) { const mats = Array.isArray(obj.material) ? obj.material : [obj.material]; mats.forEach((mat) => { if (mat) { mat.clippingPlanes = []; mat.needsUpdate = true; } }); } }); return; } const normal = new THREE.Vector3(); if (sectionAxis === 'x') normal.set(sectionInvert ? -1 : 1, 0, 0); else if (sectionAxis === 'y') normal.set(0, sectionInvert ? -1 : 1, 0); else if (sectionAxis === 'z') normal.set(0, 0, sectionInvert ? -1 : 1); const plane = new THREE.Plane(normal, sectionLevel); clone.traverse((obj) => { if (obj instanceof THREE.Mesh) { const mats = Array.isArray(obj.material) ? obj.material : [obj.material]; mats.forEach((mat) => { if (mat) { mat.clippingPlanes = [plane]; mat.clipShadows = true; mat.needsUpdate = true; } }); } }); }, [clone, presentationState]); return ( ); } // Cenário da Sala de Reunião Clara function ThreeMeetingRoomScene({ currentActiveModel, presentationState }: { currentActiveModel: { url: string; fileName: string } | null; presentationState: PresentationState }) { const tableGeom = useMemo(() => { // Geometria da mesa em U const shape = new THREE.Shape(); shape.moveTo(-1.6, -1.1); shape.lineTo(1.6, -1.1); shape.quadraticCurveTo(1.9, -1.1, 1.9, -0.8); shape.lineTo(1.9, 0.8); shape.quadraticCurveTo(1.9, 1.1, 1.6, 1.1); shape.lineTo(-1.6, 1.1); shape.quadraticCurveTo(-1.9, 1.1, -1.9, 0.8); shape.lineTo(-1.9, -0.8); shape.quadraticCurveTo(-1.9, -1.1, -1.6, -1.1); const hole = new THREE.Path(); hole.absellipse(0, 0, 0.9, 0.45, 0, Math.PI * 2, true); shape.holes.push(hole); return new THREE.ExtrudeGeometry(shape, { depth: 0.06, bevelEnabled: true, bevelThickness: 0.01, bevelSize: 0.01, bevelSegments: 3 }); }, []); return ( {/* Piso Clara */} {/* Tapete Central */} {/* Paredes Claras */} {/* Janelas Panorâmicas */} {/* Mesa Elíptica em U (Madeira Mel Carvalho Clara) */} {/* Pés da mesa (Cromados) */} {[-1.3, 1.3].map((x) => [-0.7, 0.7].map((z) => ( )) )} {/* Projetor de Holograma (Centro da mesa) */} {/* Modelo 3D da Maquete Holográfica */} }> }> {currentActiveModel ? ( ) : ( // Holograma Padrão Geométrico se não houver maquete carregada )} {/* Renderizar as Cadeiras Físicas da mesa */} {CHAIRS.map((chair) => ( {/* Base giratória metálica */} {/* Pés base */} {/* Assento */} {/* Encosto */} ))} {/* Iluminação Clara da Sala */} ); } // --- Componente Principal da Tela --- export default function MeetingRoom() { const { roomId } = useParams<{ roomId?: string }>(); const navigate = useNavigate(); const activeModel = useModelStore((s) => s.model); // Estados do Lobby const [inRoom, setInRoom] = useState(false); const [name, setName] = useState(''); const [cargo, setCargo] = useState(''); const [empresa, setEmpresa] = useState(''); const [isPresenter, setIsPresenter] = useState(!roomId); // Se não há roomId na URL, ele é o host const [selectedAvatar, setSelectedAvatar] = useState(1); const [selectedChair, setSelectedChair] = useState('1'); const [inputRoomId, setInputRoomId] = useState(roomId || ''); // Estados da Sala (Conexão Supabase) const [peers, setPeers] = useState>({}); const [chatMessages, setChatMessages] = useState<{ id: string; sender: string; text: string; time: string }[]>([]); const [newMessage, setNewMessage] = useState(''); const [isMuted, setIsMuted] = useState(false); const [isTalking, setIsTalking] = useState(false); const [activeUsersCount, setActiveUsersCount] = useState(1); const [roomCode, setRoomCode] = useState(roomId || ''); const [activeTab, setActiveTab] = useState<'chat' | 'participants'>('participants'); // Estado compartilhado da Apresentação da maquete const [presentationState, setPresentationState] = useState({ rotation: [0, 0, 0], scale: 1, sectionEnabled: false, sectionAxis: 'y', sectionLevel: 0.0, sectionInvert: false }); // Carrega modificações da peça vindas do Viewer se houver useEffect(() => { const savedState = localStorage.getItem('tsxr_initial_meeting_state'); if (savedState) { try { const parsed = JSON.parse(savedState) as PresentationState; setPresentationState(parsed); console.log('🔄 [MeetingRoom] Estado inicial carregado do Viewer:', parsed); } catch (err) { console.error('Falha ao carregar estado inicial do Viewer', err); } localStorage.removeItem('tsxr_initial_meeting_state'); } }, []); const channelRef = useRef | null>(null); const chatScrollRef = useRef(null); // Se o roomId mudar na url, atualiza o checkbox isPresenter e o inputRoomId useEffect(() => { setIsPresenter(!roomId); setInputRoomId(roomId || ''); }, [roomId]); // Simulação de detecção de áudio useEffect(() => { if (!inRoom || isMuted) { setIsTalking(false); return; } const interval = setInterval(() => { setIsTalking(Math.random() > 0.7); }, 1500); return () => clearInterval(interval); }, [inRoom, isMuted]); useEffect(() => { if (chatScrollRef.current) { chatScrollRef.current.scrollTop = chatScrollRef.current.scrollHeight; } }, [chatMessages]); // --- ESCUTA DO LOBBY EM TEMPO REAL --- // Conecta ao canal antes do lobby finalizar para ler assentos e avatares ocupados na sala digitada useEffect(() => { if (!inputRoomId || inputRoomId.trim().length < 3) { setPeers({}); return; } const roomIdUpper = inputRoomId.trim().toUpperCase(); console.log(`🔌 [Lobby Watcher] Escutando sala: ${roomIdUpper}`); const tempChannel = supabase.channel(`meeting_room_${roomIdUpper}`, { config: { presence: { key: 'lobby_watcher_' + Math.random().toString(36).substring(3, 8) } } }); tempChannel .on('presence', { event: 'sync' }, () => { const state = tempChannel.presenceState(); const formattedPeers: Record = {}; Object.keys(state).forEach((chairKey) => { // Ignora outros watchers do lobby if (chairKey.startsWith('lobby_watcher_')) return; const presences = state[chairKey] as unknown as PeerData[]; if (presences && presences.length > 0) { formattedPeers[chairKey] = presences[0]; } }); setPeers(formattedPeers); }) .subscribe(); return () => { tempChannel.unsubscribe(); }; }, [inputRoomId, inRoom]); // Envia as atualizações de apresentação via Broadcast para a sala inteira const handlePresentationChange = (updatedFields: Partial) => { if (!isPresenter || !channelRef.current) return; const newState = { ...presentationState, ...updatedFields } as PresentationState; setPresentationState(newState); channelRef.current.send({ type: 'broadcast', event: 'presentation_update', payload: newState }); }; // Inicializa o Supabase Presence & Broadcast para conexões oficiais const connectToRoom = async (targetRoomId: string) => { if (!name.trim()) { toast.error('Digite seu nome primeiro'); return; } // Validar se o avatar já está em uso por outra pessoa const avatarInUse = Object.values(peers).find((p) => p.avatarId === selectedAvatar); if (avatarInUse) { toast.error(`O avatar "${AVATAR_PRESETS[selectedAvatar - 1].name}" já está em uso por ${avatarInUse.username}. Escolha outro!`); return; } // Validar se a cadeira está ocupada if (Object.keys(peers).includes(selectedChair)) { toast.error('O assento selecionado já está ocupado por outro participante. Por favor, selecione outro assento.'); return; } const finalRoomId = targetRoomId.trim().toUpperCase() || Math.random().toString(36).substring(3, 8).toUpperCase(); setRoomCode(finalRoomId); // Evita o erro "cannot add presence callbacks ... after subscribe()" removendo canais antigos com o mesmo tópico const oldChannels = supabase.getChannels().filter(c => c.topic === `realtime:meeting_room_${finalRoomId}`); for (const old of oldChannels) { console.log(`🔌 [MeetingRoom] Removendo canal antigo ativo para evitar conflitos: ${old.topic}`); await supabase.removeChannel(old); } console.log(`🔌 [MeetingRoom] Conectando à sala: ${finalRoomId} como ${isPresenter ? 'Apresentador' : 'Convidado'}`); const channel = supabase.channel(`meeting_room_${finalRoomId}`, { config: { broadcast: { self: true }, presence: { key: selectedChair }, }, }); channelRef.current = channel; // Sincroniza participantes online channel .on('presence', { event: 'sync' }, () => { const state = channel.presenceState(); const formattedPeers: Record = {}; let count = 0; Object.keys(state).forEach((chairKey) => { if (chairKey.startsWith('lobby_watcher_')) return; const presences = state[chairKey] as unknown as PeerData[]; if (presences && presences.length > 0) { formattedPeers[chairKey] = presences[0]; count++; } }); setPeers(formattedPeers); setActiveUsersCount(count); }) .on('presence', { event: 'join' }, ({ key, newPresences }) => { if (key.startsWith('lobby_watcher_')) return; toast.info(`"${(newPresences[0] as unknown as PeerData)?.username}" sentou-se no assento ${key}`); }) .on('presence', { event: 'leave' }, ({ key, leftPresences }) => { if (key.startsWith('lobby_watcher_')) return; toast.info(`"${(leftPresences[0] as unknown as PeerData)?.username}" liberou o assento ${key}`); }); // Escuta mensagens do Chat via Broadcast channel.on('broadcast', { event: 'chat_msg' }, (payload) => { setChatMessages((prev) => [ ...prev, { id: Math.random().toString(), sender: payload.payload.sender, text: payload.payload.text, time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), }, ]); }); // Escuta atualizações de Apresentação da Maquete via Broadcast (apenas para convidados) channel.on('broadcast', { event: 'presentation_update' }, (payload) => { if (!isPresenter) { setPresentationState(payload.payload as PresentationState); } }); // Inscrever no WebSocket channel.subscribe(async (status) => { if (status === 'SUBSCRIBED') { // Envia as informações completas para o Presence do Supabase await channel.track({ username: name.substring(0, 10), avatarId: selectedAvatar, chairId: selectedChair, role: isPresenter ? 'presenter' : 'guest', cargo: cargo.trim() || 'Colaborador', empresa: empresa.trim() || 'SteelXR Corp', joinedAt: new Date().toISOString(), }); // Se for o apresentador, propaga o estado inicial de apresentação (vinda do Viewer) para todos if (isPresenter) { channel.send({ type: 'broadcast', event: 'presentation_update', payload: presentationState }); } setInRoom(true); toast.success(`Entrou na sala de reunião: ${finalRoomId}`); navigate(`/meeting/${finalRoomId}`, { replace: true }); } else if (status === 'CHANNEL_ERROR') { toast.error('Erro ao conectar ao canal de reuniões.'); } }); }; // Enviar Mensagem de Chat const handleSendMessage = () => { if (!newMessage.trim() || !channelRef.current) return; channelRef.current.send({ type: 'broadcast', event: 'chat_msg', payload: { sender: name.substring(0, 10), text: newMessage.trim(), }, }); setNewMessage(''); }; // Sair da Sala de Reunião const handleLeaveRoom = async () => { if (channelRef.current) { await supabase.removeChannel(channelRef.current); } setInRoom(false); setPeers({}); setChatMessages([]); navigate('/'); }; const occupiedChairs = useMemo(() => { return Object.keys(peers).filter(k => !k.startsWith('lobby_watcher_')); }, [peers]); // Checar se o avatar selecionado no lobby está em uso const isAvatarInUse = useMemo(() => { return Object.values(peers).find((p) => p.avatarId === selectedAvatar); }, [peers, selectedAvatar]); const nextAvatar = () => { setSelectedAvatar((prev) => (prev === 10 ? 1 : prev + 1)); }; const prevAvatar = () => { setSelectedAvatar((prev) => (prev === 1 ? 10 : prev - 1)); }; // --- Renderização do Lobby --- if (!inRoom) { const currentAvatar = AVATAR_PRESETS[selectedAvatar - 1]; const canJoin = name.trim().length > 0 && !isAvatarInUse && !occupiedChairs.includes(selectedChair); return (
TrackSteelXR

Lobby de Reunião Virtual

Configure suas credenciais e escolha seu avatar

{/* Seletor de Avatar (Lado Esquerdo - 5 colunas) */}
Visual Avatar {/* Enquadramento centralizado perfeito (Camera apontando e fov adequado) */}
{/* Aviso se o avatar estiver em uso na sala */} {isAvatarInUse && (
Avatar em Uso Este visual já está ocupado por {isAvatarInUse.username}
)}
{currentAvatar.name}

{currentAvatar.desc}

{/* Formulário de Identificação: Nome, Cargo e Empresa agrupados sequencialmente na esquerda */}
setName(e.target.value)} className="font-semibold text-center uppercase font-mono tracking-wider text-xs h-9" />
setCargo(e.target.value)} className="text-xs h-9 text-center" />
setEmpresa(e.target.value)} className="text-xs h-9 text-center" />
{/* Seleção do Assento, Host e Conexão (Lado Direito - 7 colunas) */}
{/* Seleção do Assento (Exibe quem está ocupando em tempo real) */}
Selecione seu Assento na Mesa
{CHAIRS.map((chair) => { const occupier = Object.values(peers).find((p) => p.chairId === chair.id); const isOccupied = !!occupier; const isSelected = selectedChair === chair.id; return ( ); })}
{/* Papel e Código de Conexão */}
setIsPresenter(!!checked)} />
setInputRoomId(e.target.value.toUpperCase())} className="font-mono tracking-widest font-bold text-center text-xs h-9" />
{isAvatarInUse && (

* Escolha outro avatar para liberar o acesso.

)}
); } // --- Renderização da Sala de Reunião Ativa --- return (
{/* 3D Canvas - Sala Clara */}
{/* Renderizar avatares de TODOS na sala */} {Object.keys(peers).map((chairKey) => { const peer = peers[chairKey]; const chairConf = CHAIRS.find((c) => c.id === peer.chairId); if (!chairConf) return null; const isLocalUser = peer.chairId === selectedChair; return ( 0.5)} scale={1.2} /> ); })} {/* Painel do Apresentador (Controles de Manipulação Sincronizados) */} {isPresenter && (
Painel do Apresentador Sincronizado
{/* Rotação Y */}
Giro da Peça (Horizontal) {Math.round((presentationState.rotation[1] * 180) / Math.PI)}°
handlePresentationChange({ rotation: [0, parseFloat(e.target.value), 0] })} className="w-full h-1 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-primary" />
{/* Escala */}
Escala (Zoom da Peça) {presentationState.scale.toFixed(1)}x
handlePresentationChange({ scale: parseFloat(e.target.value) })} className="w-full h-1 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-primary" />
{/* Plano de Corte */}
Corte de Seção (Clipping) handlePresentationChange({ sectionEnabled: e.target.checked })} className="rounded border-slate-800 bg-slate-950 accent-primary cursor-pointer h-3.5 w-3.5" />
{presentationState.sectionEnabled && (
{/* Seletor Eixo */}
{['x', 'y', 'z'].map((ax) => ( ))}
{/* Slider de Nível */}
Posição do Corte {(presentationState.sectionLevel * 100).toFixed(0)}%
handlePresentationChange({ sectionLevel: parseFloat(e.target.value) })} className="w-full h-1 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-primary" />
{/* Inverter Corte */}
handlePresentationChange({ sectionInvert: e.target.checked })} className="rounded border-slate-800 bg-slate-950 accent-primary cursor-pointer h-3 w-3" />
)}
{/* Botão de Reset */}
)} {/* HUD Superior */}
Reunião Conectada Online: {activeUsersCount}/8
{/* Código da sala */}
CÓDIGO: {roomCode}
{/* Sair */}
{/* HUD Inferior - Controles de Áudio */}
{isPresenter ? 'Apresentação Ativa' : 'Observador'} {isPresenter ? 'Você está transmitindo os cortes e rotações da maquete.' : 'Aguarde as rotações e planos de corte efetuados pelo Host.'}
{/* Painel Lateral Direito (Abas Chat / Participantes) */}
{/* Seletor de Abas */}
{/* ABA PARTICIPANTES */} {activeTab === 'participants' && (
Quem está na sala
{Object.keys(peers).map((chairKey) => { const peer = peers[chairKey]; const isHost = peer.role === 'presenter'; const isMe = peer.chairId === selectedChair; return (
{/* Indicador da cor do avatar do Mii via classe estática Tailwind */}
{peer.username.substring(0, 2).toUpperCase()}
{/* Detalhes do Usuário */}
{peer.username} {isMe && '(Você)'} {isHost && ( Host )}
{/* Cargo e Empresa */} 💼 {peer.cargo} 🏢 {peer.empresa}
Chair {peer.chairId}
); })}
)} {/* ABA CHAT */} {activeTab === 'chat' && ( <>
{chatMessages.length === 0 ? (

Nenhuma mensagem enviada. Seja o primeiro a digitar no chat da reunião!

) : ( chatMessages.map((msg) => (
{msg.sender} {msg.time}

{msg.text}

)) )}
{/* Input do Chat */}
setNewMessage(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleSendMessage(); }} className="flex-1 text-xs border-slate-800 bg-slate-950/65" />
)}
); }