From e390fd0f59e64f168a615d61952082300c8f61b2 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Mon, 25 May 2026 00:19:23 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Auto-deploy:=20melhoria=20no=20s?= =?UTF-8?q?nap=20e=20medi=C3=A7=C3=A3o=20AR=20em=2025/05/2026=2000:19:23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MeetingRoom.tsx | 665 ++++++++++++++++++++++++++++---------- 1 file changed, 491 insertions(+), 174 deletions(-) diff --git a/src/pages/MeetingRoom.tsx b/src/pages/MeetingRoom.tsx index 19c3a51..ddfd371 100644 --- a/src/pages/MeetingRoom.tsx +++ b/src/pages/MeetingRoom.tsx @@ -1,34 +1,38 @@ import { useEffect, useRef, useState, useMemo, Suspense } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { Canvas, useFrame } from '@react-three/fiber'; +import { Canvas, useFrame, useThree } 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 { Label } from '@/components/ui/label'; +import { Checkbox } from '@/components/ui/checkbox'; import { toast } from 'sonner'; import { - Users, MessageSquare, Mic, MicOff, Home, Send, - ArrowRight, ChevronLeft, ChevronRight, Share2, LogOut + Users, MessageSquare, Mic, MicOff, Home, Send, Slider, + ArrowRight, ChevronLeft, ChevronRight, Share2, LogOut, Check, + Scissors, RotateCw, Maximize2, ShieldAlert, Award } from 'lucide-react'; // --- Configurações da Mesa de Reunião --- -// Coordenadas das 8 cadeiras ao redor da mesa elíptica +// Coordenadas das 8 cadeiras posicionadas organizadamente FORA da mesa +// A mesa vai de X = -1.9 a 1.9 e Z = -1.1 a 1.1. As cadeiras ficam em Z = -1.45 e Z = 1.45. const CHAIRS = [ - { id: '1', name: 'Assento 1', pos: [-1.2, 0.95, -0.6], lookAt: [0, 0.95, 0] }, - { id: '2', name: 'Assento 2', pos: [-0.6, 0.95, -0.9], lookAt: [0, 0.95, 0] }, - { id: '3', name: 'Assento 3', pos: [0.6, 0.95, -0.9], lookAt: [0, 0.95, 0] }, - { id: '4', name: 'Assento 4', pos: [1.2, 0.95, -0.6], lookAt: [0, 0.95, 0] }, - { id: '5', name: 'Assento 5', pos: [1.2, 0.95, 0.6], lookAt: [0, 0.95, 0] }, - { id: '6', name: 'Assento 6', pos: [0.6, 0.95, 0.9], lookAt: [0, 0.95, 0] }, - { id: '7', name: 'Assento 7', pos: [-0.6, 0.95, 0.9], lookAt: [0, 0.95, 0] }, - { id: '8', name: 'Assento 8', pos: [-1.2, 0.95, 0.6], lookAt: [0, 0.95, 0] }, + { 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] }, ]; -// 10 Modelos de Avatar estilo Nintendo Wii (Mii) com características humanas +// 10 Modelos de Mii com características e cores de cabelo humanas const AVATAR_PRESETS = [ - { id: 1, name: 'Mii Cyber', desc: 'Pele clara com óculos vermelhos e cabelo curto', color: '#ef4444' }, + { id: 1, name: 'Mii Cyber', desc: 'Cabelo curto preto e óculos vermelhos', color: '#ef4444' }, { id: 2, name: 'Mii Classic', desc: 'Pele média com cabelo castanho clássico', color: '#b45309' }, { id: 3, name: 'Mii Goldie', desc: 'Cabelo loiro longo com olhos azuis expressivos', color: '#eab308' }, { id: 4, name: 'Mii Sporty', desc: 'Boné azul virado para trás e sorriso aberto', color: '#3b82f6' }, @@ -40,7 +44,7 @@ const AVATAR_PRESETS = [ { id: 10, name: 'Mii Straw', desc: 'Chapéu de palha do campo clássico', color: '#ca8a04' }, ]; -// Cores de pele padrão para os Miis +// Cores de pele Mii const SKIN_COLORS = ['#fed7aa', '#fdba74', '#f59e0b', '#d97706', '#9a3412', '#ffedd5']; // --- Subcomponentes 3D --- @@ -49,7 +53,7 @@ const SKIN_COLORS = ['#fed7aa', '#fdba74', '#f59e0b', '#d97706', '#9a3412', '#ff function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: number; username: string; isTalking?: boolean } & any) { const groupRef = useRef(null); - // Parâmetros do rosto e cabelo com base no ID selecionado + // Parâmetros do rosto e cabelo com base no ID const mii = useMemo(() => { const skin = SKIN_COLORS[(avatarId - 1) % SKIN_COLORS.length]; return { @@ -68,14 +72,13 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb useFrame((state) => { if (groupRef.current) { - // Pequena flutuação para dar vida groupRef.current.position.y = props.position[1] + Math.sin(state.clock.getElapsedTime() * 2 + avatarId) * 0.012; } }); return ( - {/* Balão com Nome acima da cabeça */} + {/* Balão de Nome acima da cabeça */} @@ -185,7 +188,7 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb - {/* BOCA (Sorriso) */} + {/* BOCA */} @@ -231,39 +234,99 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb ); } -// Carregador e Escalador Inteligente do Modelo Real (Adaptado para 1 Metro de tamanho) -function HologramModel({ model }: { model: { url: string } }) { +// Carregador e Escalador Inteligente da Maquete Holográfica +// Sincroniza transformações de rotação, escala e planos de corte do Apresentador +function HologramModel({ model, presentationState }: { model: { url: string }; presentationState: any }) { 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; - - // Medir limites tridimensionais da maquete importada 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) { - // Ajusta a escala de forma inteligente para ter aproximadamente 1 metro (ex: 0.95m) const targetSize = 0.95; const scaleFactor = targetSize / maxDim; - groupRef.current.scale.setScalar(scaleFactor); - - // Centralizar o pivot em (0,0,0) const center = new THREE.Vector3(); box.getCenter(center); - - // Centraliza e levanta ligeiramente acima da mesa 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) { + // Multiplica a escala base inteligente pelo fator da apresentação + 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; + } + + // Vetor normal baseado no eixo e na inversão do corte + 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); + + // O plano corta na coordenada desejada + 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 ( @@ -271,8 +334,8 @@ function HologramModel({ model }: { model: { url: string } }) { ); } -// Cenário Claro, Limpo e Iluminado da Sala de Reunião -function ThreeMeetingRoomScene({ currentActiveModel }: { currentActiveModel: any }) { +// Cenário da Sala de Reunião Clara +function ThreeMeetingRoomScene({ currentActiveModel, presentationState }: { currentActiveModel: any; presentationState: any }) { const tableGeom = useMemo(() => { // Geometria da mesa em U const shape = new THREE.Shape(); @@ -286,7 +349,6 @@ function ThreeMeetingRoomScene({ currentActiveModel }: { currentActiveModel: any shape.lineTo(-1.9, -0.8); shape.quadraticCurveTo(-1.9, -1.1, -1.6, -1.1); - // Furo elíptico central para projetar o holograma const hole = new THREE.Path(); hole.absellipse(0, 0, 0.9, 0.45, 0, Math.PI * 2, true); shape.holes.push(hole); @@ -296,7 +358,7 @@ function ThreeMeetingRoomScene({ currentActiveModel }: { currentActiveModel: any return ( - {/* Piso de Laca Clara (Sala muito mais clara e iluminada!) */} + {/* Piso Clara */} @@ -313,7 +375,7 @@ function ThreeMeetingRoomScene({ currentActiveModel }: { currentActiveModel: any - {/* Janelas Panorâmicas Ensolaradas */} + {/* Janelas Panorâmicas */} @@ -323,7 +385,7 @@ function ThreeMeetingRoomScene({ currentActiveModel }: { currentActiveModel: any - {/* Mesa Elíptica em U (Madeira Mel/Carvalho Clara e Acolhedora) */} + {/* Mesa Elíptica em U (Madeira Mel Carvalho Clara) */} @@ -346,31 +408,28 @@ function ThreeMeetingRoomScene({ currentActiveModel }: { currentActiveModel: any - {/* Anel de luz azul brilhante */} - {/* Feixe de Luz Holográfico */} {/* Modelo 3D da Maquete Holográfica */} - + }> {currentActiveModel ? ( - + ) : ( // Holograma Padrão Geométrico se não houver maquete carregada - + @@ -411,7 +470,7 @@ function ThreeMeetingRoomScene({ currentActiveModel }: { currentActiveModel: any ))} - {/* Iluminação Super Clara e Acolhedora da Sala */} + {/* Iluminação Clara da Sala */} @@ -430,42 +489,74 @@ export default function MeetingRoom() { // 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 - const [peers, setPeers] = useState>({}); + 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 + }); const channelRef = useRef(null); const chatScrollRef = useRef(null); - // Gerencia o loop de simulação de fala + // Se o roomId mudar na url, atualiza o checkbox isPresenter + useEffect(() => { + setIsPresenter(!roomId); + setInputRoomId(roomId || ''); + }, [roomId]); + + // Simulação simples de detecção de áudio useEffect(() => { if (!inRoom || isMuted) { setIsTalking(false); return; } const interval = setInterval(() => { - // Simula detecção de microfone com probabilidade de atividade setIsTalking(Math.random() > 0.7); }, 1500); return () => clearInterval(interval); }, [inRoom, isMuted]); - // Rola o chat para o fim quando houver mensagens useEffect(() => { if (chatScrollRef.current) { chatScrollRef.current.scrollTop = chatScrollRef.current.scrollHeight; } }, [chatMessages]); + // 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 }; + setPresentationState(newState); + + channelRef.current.send({ + type: 'broadcast', + event: 'presentation_update', + payload: newState + }); + }; + // Inicializa o Supabase Presence & Broadcast para conexões const connectToRoom = (targetRoomId: string) => { if (!name.trim()) { @@ -475,7 +566,7 @@ export default function MeetingRoom() { const finalRoomId = targetRoomId.trim().toUpperCase() || Math.random().toString(36).substring(3, 8).toUpperCase(); setRoomCode(finalRoomId); - console.log(`🔌 [MeetingRoom] Conectando à sala: ${finalRoomId}`); + console.log(`🔌 [MeetingRoom] Conectando à sala: ${finalRoomId} como ${isPresenter ? 'Apresentador' : 'Convidado'}`); const channel = supabase.channel(`meeting_room_${finalRoomId}`, { config: { @@ -486,7 +577,7 @@ export default function MeetingRoom() { channelRef.current = channel; - // Sincroniza participantes + // Sincroniza participantes online channel .on('presence', { event: 'sync' }, () => { const state = channel.presenceState(); @@ -504,10 +595,10 @@ export default function MeetingRoom() { setActiveUsersCount(count); }) .on('presence', { event: 'join' }, ({ key, newPresences }) => { - toast.info(`Participante "${newPresences[0]?.username}" entrou no assento ${key}`); + toast.info(`"${newPresences[0]?.username}" sentou-se no assento ${key}`); }) .on('presence', { event: 'leave' }, ({ key, leftPresences }) => { - toast.info(`Participante "${leftPresences[0]?.username}" desocupou o assento ${key}`); + toast.info(`"${leftPresences[0]?.username}" liberou o assento ${key}`); }); // Escuta mensagens do Chat via Broadcast @@ -523,21 +614,31 @@ export default function MeetingRoom() { ]); }); + // 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); + } + }); + // Inscrever no WebSocket channel.subscribe(async (status) => { if (status === 'SUBSCRIBED') { - // Envia as informações do usuário atual para a presença + // 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(), }); setInRoom(true); toast.success(`Entrou na sala de reunião: ${finalRoomId}`); navigate(`/meeting/${finalRoomId}`, { replace: true }); } else if (status === 'CHANNEL_ERROR') { - toast.error('Erro de conexão ao canal de reuniões.'); + toast.error('Erro ao conectar ao canal de reuniões.'); } }); }; @@ -559,7 +660,6 @@ export default function MeetingRoom() { // Sair da Sala de Reunião const handleLeaveRoom = async () => { if (channelRef.current) { - console.log('🔌 [MeetingRoom] Saindo da sala...'); await supabase.removeChannel(channelRef.current); } setInRoom(false); @@ -568,12 +668,10 @@ export default function MeetingRoom() { navigate('/'); }; - // Identifica quais cadeiras já estão ocupadas por outros participantes const occupiedChairs = useMemo(() => { return Object.keys(peers); }, [peers]); - // Escolha do Preset de Avatar no Lobby (Navegação) const nextAvatar = () => { setSelectedAvatar((prev) => (prev === 10 ? 1 : prev + 1)); }; @@ -595,22 +693,21 @@ export default function MeetingRoom() {
-
+

Lobby de Reunião Virtual

-

- Escolha seu visual Mii e selecione seu assento +

+ Configure suas credenciais e escolha seu visual Mii

-
- {/* Seletor de Avatar (Lado Esquerdo - 5 colunas) */} -
- Modelo de Avatar +
+ {/* Avatar Mii Selector (Lado Esquerdo - 5 colunas) */} +
+ Visual Mii - {/* Modelo Virtual do Mii no Centro */} -
+
@@ -618,7 +715,6 @@ export default function MeetingRoom() { - {/* Navegadores de carrossel de avatar */} @@ -627,33 +723,56 @@ export default function MeetingRoom() {
- {/* Informações do avatar */} -
+
{currentAvatar.name} -

{currentAvatar.desc}

+

{currentAvatar.desc}

{/* Nome */} -
- +
+ setName(e.target.value)} - className="font-semibold text-center uppercase font-mono tracking-wider" + className="font-semibold text-center uppercase font-mono tracking-wider text-xs h-9" />
- {/* Configurações de Assento e Conexão (Lado Direito - 7 colunas) */} -
+ {/* Credenciais, Assento e Conexão (Lado Direito - 7 colunas) */} +
+ {/* Form de Cargo e Empresa */} +
+
+ + setCargo(e.target.value)} + className="text-xs h-9" + /> +
+
+ + setEmpresa(e.target.value)} + className="text-xs h-9" + /> +
+
+ {/* Seleção do Assento */} -
- Selecione seu Assento na Mesa +
+ Selecione seu Assento na Mesa
{CHAIRS.map((chair) => { const isOccupied = occupiedChairs.includes(chair.id); @@ -662,15 +781,15 @@ export default function MeetingRoom() { ); @@ -678,43 +797,42 @@ export default function MeetingRoom() {
- {/* ID da Sala / Ação */} + {/* Papel e Conexão */}
-
- -
- setInputRoomId(e.target.value.toUpperCase())} - className="font-mono tracking-widest font-bold text-center" - /> -
+
+ setIsPresenter(!!checked)} + /> +
-
- -
- - {/* Aviso da Peça Ativa */} -
-
- - - {activeModel ? `Maquete Ativa: ${activeModel.fileName}` : 'Nenhuma maquete ativa. Sala iniciará com maquete padrão.'} - -
- {activeModel && ( - IFC/GLB - )} -
@@ -725,28 +843,27 @@ export default function MeetingRoom() { // --- Renderização da Sala de Reunião Ativa --- return ( -
- {/* 3D Canvas - Sala Virtual Clara */} +
+ {/* 3D Canvas - Sala Clara */}
- - + + - {/* Renderizar avatares dos outros participantes na sala */} + {/* Renderizar avatares de TODOS na sala (incluindo o próprio usuário para ele se ver sentado na cadeira) */} {Object.keys(peers).map((chairKey) => { const peer = peers[chairKey]; const chairConf = CHAIRS.find((c) => c.id === peer.chairId); if (!chairConf) return null; - // Não renderiza a nós mesmos na cena como avatar, nossa câmera já está na cadeira! - if (peer.chairId === selectedChair) return null; + const isLocalUser = peer.chairId === selectedChair; return ( 0.5} // Simulação de fala + isTalking={isLocalUser ? isTalking : (isTalking && Math.random() > 0.5)} /> ); })} @@ -757,19 +874,146 @@ export default function MeetingRoom() { dampingFactor={0.05} minDistance={0.5} maxDistance={8} - maxPolarAngle={Math.PI / 2 - 0.05} // Não permite atravessar o chão - target={[0, 0.95, 0]} // Foca no centro da mesa + maxPolarAngle={Math.PI / 2 - 0.05} + target={[0, 0.95, 0]} /> - {/* HUD Superior / Detalhes da Conexão */} + {/* 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 - Membros Online: {activeUsersCount}/8 + Online: {activeUsersCount}/8
@@ -785,7 +1029,7 @@ export default function MeetingRoom() { className="h-5 w-5 text-muted-foreground hover:text-foreground" onClick={() => { navigator.clipboard.writeText(window.location.href); - toast.success('Link do convite copiado!'); + toast.success('Link de convite copiado!'); }} title="Copiar link do convite" > @@ -800,7 +1044,7 @@ export default function MeetingRoom() {
- {/* HUD Inferior - Controles de Áudio e Instrução */} + {/* HUD Inferior - Controles de Áudio */}
- {/* Painel Lateral Direito - Chat e Lista de Usuários (Largura 320px) */} + {/* Painel Lateral Direito (Abas Chat / Participantes) */}
- {/* Header do Painel */} -
-
- - Chat da Reunião -
- Tempo Real -
- - {/* Histórico do 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" - /> - +
+ + {/* ABA PARTICIPANTES */} + {activeTab === 'participants' && ( +
+ Quem está na sala + +
+ {Object.keys(peers).map((chairKey) => { + const peer = peers[chairKey]; + const isHost = peer.role === 'presenter'; + const avatar = AVATAR_PRESETS[peer.avatarId - 1] || AVATAR_PRESETS[0]; + const isMe = peer.chairId === selectedChair; + + return ( +
+ {/* Indicador da cor do avatar do Mii */} +
+ {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" + /> + +
+ + )}
);