change icon, make transition animation during analyzing image
This commit is contained in:
@@ -28,17 +28,76 @@ export function PhoneSimulator({ onConnect, phoneMode = "normal", onCapture }: P
|
|||||||
const tracks = useTracks();
|
const tracks = useTracks();
|
||||||
const voiceAssistant = useVoiceAssistant();
|
const voiceAssistant = useVoiceAssistant();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const phoneContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const visualizerRef = useRef<HTMLDivElement>(null);
|
||||||
const [showCameraMenu, setShowCameraMenu] = useState(false);
|
const [showCameraMenu, setShowCameraMenu] = useState(false);
|
||||||
const [cameras, setCameras] = useState<MediaDeviceInfo[]>([]);
|
const [cameras, setCameras] = useState<MediaDeviceInfo[]>([]);
|
||||||
|
const [processingImage, setProcessingImage] = useState<string | null>(null);
|
||||||
|
const [isCapturing, setIsCapturing] = useState(false);
|
||||||
|
const [processingSource, setProcessingSource] = useState<
|
||||||
|
"camera" | "upload" | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const [currentTime, setCurrentTime] = useState("");
|
const [currentTime, setCurrentTime] = useState("");
|
||||||
|
|
||||||
|
const [visualizerPosition, setVisualizerPosition] = useState({
|
||||||
|
x: 16,
|
||||||
|
y: 56,
|
||||||
|
});
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const dragOffset = useRef({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
const handleDragStart = (e: React.MouseEvent) => {
|
||||||
|
setIsDragging(true);
|
||||||
|
dragOffset.current = {
|
||||||
|
x: e.clientX - visualizerPosition.x,
|
||||||
|
y: e.clientY - visualizerPosition.y,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragMove = (e: MouseEvent) => {
|
||||||
|
if (!isDragging || !phoneContainerRef.current || !visualizerRef.current) return;
|
||||||
|
|
||||||
|
const containerRect = phoneContainerRef.current.getBoundingClientRect();
|
||||||
|
const visualizerRect = visualizerRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
let newX = e.clientX - dragOffset.current.x;
|
||||||
|
let newY = e.clientY - dragOffset.current.y;
|
||||||
|
|
||||||
|
// Constrain within container
|
||||||
|
const maxX = containerRect.width - visualizerRect.width;
|
||||||
|
const maxY = containerRect.height - visualizerRect.height;
|
||||||
|
const statusBarHeight = 48; // h-12 = 48px
|
||||||
|
|
||||||
|
newX = Math.max(0, Math.min(newX, maxX));
|
||||||
|
newY = Math.max(statusBarHeight, Math.min(newY, maxY));
|
||||||
|
|
||||||
|
setVisualizerPosition({
|
||||||
|
x: newX,
|
||||||
|
y: newY,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDragging) {
|
||||||
|
window.addEventListener("mouseup", handleDragEnd);
|
||||||
|
window.addEventListener("mousemove", handleDragMove);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("mouseup", handleDragEnd);
|
||||||
|
window.removeEventListener("mousemove", handleDragMove);
|
||||||
|
};
|
||||||
|
}, [isDragging]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showCameraMenu) {
|
if (showCameraMenu) {
|
||||||
Room.getLocalDevices("videoinput").then(setCameras);
|
Room.getLocalDevices("videoinput").then(setCameras);
|
||||||
}
|
}
|
||||||
}, [showCameraMenu]);
|
}, [showCameraMenu]);
|
||||||
|
|
||||||
// Close menu when clicking outside
|
// Close menu when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
@@ -54,6 +113,13 @@ export function PhoneSimulator({ onConnect, phoneMode = "normal", onCapture }: P
|
|||||||
};
|
};
|
||||||
}, [showCameraMenu]);
|
}, [showCameraMenu]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (voiceAssistant.state === "speaking") {
|
||||||
|
setProcessingImage(null);
|
||||||
|
setProcessingSource(null);
|
||||||
|
}
|
||||||
|
}, [voiceAssistant.state]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateTime = () => {
|
const updateTime = () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -92,22 +158,128 @@ export function PhoneSimulator({ onConnect, phoneMode = "normal", onCapture }: P
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCapture = async () => {
|
const handleCapture = async () => {
|
||||||
if (!localCameraTrack || !onCapture) return;
|
if (!localCameraTrack || !onCapture || isCapturing) return;
|
||||||
|
setIsCapturing(true);
|
||||||
|
|
||||||
|
const trackReference = localCameraTrack as any;
|
||||||
|
// Handle both TrackReference (from useTracks) and potential direct Track objects
|
||||||
|
const track =
|
||||||
|
trackReference.publication?.track?.mediaStreamTrack ||
|
||||||
|
trackReference.mediaStreamTrack;
|
||||||
|
|
||||||
|
if (!track) {
|
||||||
|
console.error("No media stream track found");
|
||||||
|
setIsCapturing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const video = document.createElement("video");
|
||||||
|
video.srcObject = new MediaStream([track]);
|
||||||
|
video.muted = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.autoplay = true;
|
||||||
|
// Element needs to be in the DOM for some browsers to play it properly
|
||||||
|
video.style.position = "absolute";
|
||||||
|
video.style.top = "-9999px";
|
||||||
|
video.style.left = "-9999px";
|
||||||
|
document.body.appendChild(video);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await video.play();
|
||||||
|
|
||||||
|
// Wait for video dimensions to be available
|
||||||
|
if (video.videoWidth === 0 || video.videoHeight === 0) {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
video.onloadedmetadata = () => resolve();
|
||||||
|
// Timeout to prevent hanging
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
// Default to video dimensions
|
||||||
|
let renderWidth = video.videoWidth;
|
||||||
|
let renderHeight = video.videoHeight;
|
||||||
|
let sourceX = 0;
|
||||||
|
let sourceY = 0;
|
||||||
|
let sourceWidth = video.videoWidth;
|
||||||
|
let sourceHeight = video.videoHeight;
|
||||||
|
|
||||||
|
// If the video is landscape but we want a portrait crop (like a phone)
|
||||||
|
// We want an aspect ratio of roughly 9:19.5 (from the container styles)
|
||||||
|
const targetAspect = 9 / 19.5;
|
||||||
|
const videoAspect = video.videoWidth / video.videoHeight;
|
||||||
|
|
||||||
|
if (videoAspect > targetAspect) {
|
||||||
|
// Video is wider than target - crop width
|
||||||
|
const newWidth = video.videoHeight * targetAspect;
|
||||||
|
sourceX = (video.videoWidth - newWidth) / 2;
|
||||||
|
sourceWidth = newWidth;
|
||||||
|
renderWidth = newWidth;
|
||||||
|
} else {
|
||||||
|
// Video is taller than target - crop height (less common for landscape webcam)
|
||||||
|
const newHeight = video.videoWidth / targetAspect;
|
||||||
|
sourceY = (video.videoHeight - newHeight) / 2;
|
||||||
|
sourceHeight = newHeight;
|
||||||
|
renderHeight = newHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = renderWidth;
|
||||||
|
canvas.height = renderHeight;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
if (ctx) {
|
||||||
|
// Mirror the image to match the preview
|
||||||
|
ctx.translate(canvas.width, 0);
|
||||||
|
ctx.scale(-1, 1);
|
||||||
|
|
||||||
|
// Draw only the cropped portion of the video
|
||||||
|
ctx.drawImage(
|
||||||
|
video,
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
sourceWidth,
|
||||||
|
sourceHeight,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
renderWidth,
|
||||||
|
renderHeight
|
||||||
|
);
|
||||||
|
// Reset transform
|
||||||
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
|
||||||
|
// Use toDataURL for immediate preview feedback
|
||||||
|
const dataUrl = canvas.toDataURL("image/jpeg");
|
||||||
|
setProcessingImage(dataUrl);
|
||||||
|
setProcessingSource("camera");
|
||||||
|
|
||||||
|
// Create a new canvas for the final output (unmirrored if needed, but user requested mirrored)
|
||||||
|
// The user requested to mirror the shuttled photo, which we did above for the canvas.
|
||||||
|
// So the blob created from this canvas will also be mirrored.
|
||||||
|
|
||||||
const videoElement = localCameraTrack.attach() as HTMLVideoElement;
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = videoElement.videoWidth;
|
|
||||||
canvas.height = videoElement.videoHeight;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
|
|
||||||
if (ctx) {
|
|
||||||
ctx.drawImage(videoElement, 0, 0);
|
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
if (blob && onCapture) {
|
if (blob && onCapture) {
|
||||||
const file = new File([blob], "camera-capture.jpg", { type: "image/jpeg" });
|
const file = new File([blob], "camera-capture.jpg", {
|
||||||
onCapture(file);
|
type: "image/jpeg",
|
||||||
}
|
});
|
||||||
|
onCapture(file);
|
||||||
|
}
|
||||||
|
setIsCapturing(false);
|
||||||
}, "image/jpeg");
|
}, "image/jpeg");
|
||||||
|
} else {
|
||||||
|
setIsCapturing(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to capture image", e);
|
||||||
|
setIsCapturing(false);
|
||||||
|
} finally {
|
||||||
|
// Cleanup
|
||||||
|
video.pause();
|
||||||
|
video.srcObject = null;
|
||||||
|
if (document.body.contains(video)) {
|
||||||
|
document.body.removeChild(video);
|
||||||
|
}
|
||||||
|
video.remove();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,6 +303,8 @@ export function PhoneSimulator({ onConnect, phoneMode = "normal", onCapture }: P
|
|||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (file && onCapture) {
|
if (file && onCapture) {
|
||||||
onCapture(file);
|
onCapture(file);
|
||||||
|
setProcessingImage(URL.createObjectURL(file));
|
||||||
|
setProcessingSource("upload");
|
||||||
}
|
}
|
||||||
// Reset input so the same file can be selected again
|
// Reset input so the same file can be selected again
|
||||||
if (event.target) {
|
if (event.target) {
|
||||||
@@ -182,6 +356,14 @@ export function PhoneSimulator({ onConnect, phoneMode = "normal", onCapture }: P
|
|||||||
.mirror-video video {
|
.mirror-video video {
|
||||||
transform: scaleX(-1);
|
transform: scaleX(-1);
|
||||||
}
|
}
|
||||||
|
@keyframes scan {
|
||||||
|
0% { top: 0%; }
|
||||||
|
50% { top: 100%; }
|
||||||
|
100% { top: 0%; }
|
||||||
|
}
|
||||||
|
.scan-animation {
|
||||||
|
animation: scan 3s linear infinite;
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
{/* Status Bar */}
|
{/* Status Bar */}
|
||||||
<div className="h-12 w-full bg-black/20 absolute top-0 left-0 z-20 flex items-center justify-between px-6 text-white text-xs font-medium backdrop-blur-sm">
|
<div className="h-12 w-full bg-black/20 absolute top-0 left-0 z-20 flex items-center justify-between px-6 text-white text-xs font-medium backdrop-blur-sm">
|
||||||
@@ -196,8 +378,28 @@ export function PhoneSimulator({ onConnect, phoneMode = "normal", onCapture }: P
|
|||||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-32 h-7 bg-black rounded-b-2xl z-30"></div>
|
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-32 h-7 bg-black rounded-b-2xl z-30"></div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-grow relative bg-gray-950 w-full h-full">
|
<div ref={phoneContainerRef} className="flex-grow relative bg-gray-950 w-full h-full">
|
||||||
{videoContent}
|
{videoContent}
|
||||||
|
{processingImage && (
|
||||||
|
<div className="absolute inset-0 z-10 bg-black flex items-center justify-center">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={processingImage}
|
||||||
|
alt="Processing"
|
||||||
|
className={`w-full h-full opacity-50 ${
|
||||||
|
processingSource === "camera"
|
||||||
|
? "object-cover scale-x-[-1]"
|
||||||
|
: "object-contain"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<div className="w-full h-[2px] bg-blue-500 shadow-[0_0_15px_rgba(59,130,246,1)] scan-animation absolute"></div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full text-center text-blue-400 font-mono text-sm animate-pulse z-20">
|
||||||
|
正在分析...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -208,7 +410,7 @@ export function PhoneSimulator({ onConnect, phoneMode = "normal", onCapture }: P
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Capture Guide Lines */}
|
{/* Capture Guide Lines */}
|
||||||
{roomState === ConnectionState.Connected && phoneMode === "capture" && (
|
{roomState === ConnectionState.Connected && phoneMode === "capture" && !processingImage && (
|
||||||
<div className="absolute inset-0 pointer-events-none z-10">
|
<div className="absolute inset-0 pointer-events-none z-10">
|
||||||
{/* Thirds Grid */}
|
{/* Thirds Grid */}
|
||||||
<div className="absolute top-1/3 left-0 w-full h-[1px] bg-white/20"></div>
|
<div className="absolute top-1/3 left-0 w-full h-[1px] bg-white/20"></div>
|
||||||
@@ -222,93 +424,104 @@ export function PhoneSimulator({ onConnect, phoneMode = "normal", onCapture }: P
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Agent Audio Visualizer (Top Left) */}
|
{/* Agent Audio Visualizer (Draggable) */}
|
||||||
{roomState === ConnectionState.Connected && voiceAssistant.audioTrack && (
|
{roomState === ConnectionState.Connected && voiceAssistant.audioTrack && (
|
||||||
<div className="absolute top-14 left-4 z-20 p-2 bg-black/40 backdrop-blur-md rounded-lg border border-white/10 shadow-lg">
|
<div
|
||||||
<div className="h-8 w-24 flex items-center justify-center [--lk-va-bar-width:3px] [--lk-va-bar-gap:2px] [--lk-fg:white]">
|
ref={visualizerRef}
|
||||||
<BarVisualizer
|
className="absolute z-20 p-2 bg-black/40 backdrop-blur-md rounded-lg border border-white/10 shadow-lg cursor-move select-none"
|
||||||
state={voiceAssistant.state}
|
style={{
|
||||||
trackRef={voiceAssistant.audioTrack}
|
left: visualizerPosition.x,
|
||||||
barCount={7}
|
top: visualizerPosition.y,
|
||||||
options={{ minHeight: 5 }}
|
}}
|
||||||
/>
|
onMouseDown={handleDragStart}
|
||||||
</div>
|
>
|
||||||
|
<div className="h-8 w-24 flex items-center justify-center [--lk-va-bar-width:3px] [--lk-va-bar-gap:2px] [--lk-fg:white]">
|
||||||
|
<BarVisualizer
|
||||||
|
state={voiceAssistant.state}
|
||||||
|
trackRef={voiceAssistant.audioTrack}
|
||||||
|
barCount={7}
|
||||||
|
options={{ minHeight: 5 }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Call Controls Overlay */}
|
{/* Call Controls Overlay */}
|
||||||
{roomState === ConnectionState.Connected && (
|
{roomState === ConnectionState.Connected && (
|
||||||
<div className="absolute bottom-8 left-0 w-full px-8 flex items-center justify-between z-20">
|
<div className="absolute bottom-8 left-0 w-full px-8 z-20">
|
||||||
{phoneMode === "capture" ? (
|
{phoneMode === "capture" ? (
|
||||||
<div className="w-full grid grid-cols-3 items-center px-8">
|
<div className="w-full grid grid-cols-3 items-center">
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<button
|
<button
|
||||||
className="p-4 rounded-full bg-gray-800/50 text-white hover:bg-gray-800/70 transition-colors shrink-0"
|
className="p-4 rounded-full bg-gray-800/50 text-white hover:bg-gray-800/70 transition-colors shrink-0"
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
>
|
>
|
||||||
<ImageIcon className="w-6 h-6" />
|
<ImageIcon className="w-6 h-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<button
|
<button
|
||||||
className="w-20 h-20 rounded-full border-4 border-white p-1 hover:scale-105 transition-transform shrink-0 aspect-square"
|
className="w-20 h-20 rounded-full border-4 border-white p-1 hover:scale-105 transition-transform shrink-0 aspect-square"
|
||||||
onClick={handleCapture}
|
onClick={handleCapture}
|
||||||
>
|
>
|
||||||
<div className="w-full h-full bg-white rounded-full"></div>
|
<div className="w-full h-full bg-white rounded-full"></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end relative">
|
<div className="flex justify-end relative">
|
||||||
<button
|
<button
|
||||||
className="p-4 rounded-full bg-gray-800/50 text-white hover:bg-gray-800/70 transition-colors shrink-0"
|
className="p-4 rounded-full bg-gray-800/50 text-white hover:bg-gray-800/70 transition-colors shrink-0"
|
||||||
onClick={handleSwitchCamera}
|
onClick={handleSwitchCamera}
|
||||||
>
|
>
|
||||||
<SwitchCameraIcon className="w-6 h-6" />
|
<SwitchCameraIcon className="w-6 h-6" />
|
||||||
</button>
|
</button>
|
||||||
{showCameraMenu && (
|
{showCameraMenu && (
|
||||||
<div className="absolute bottom-16 right-0 bg-gray-900 border border-gray-800 rounded-lg shadow-xl py-2 w-48 z-50">
|
<div className="absolute bottom-16 right-0 bg-gray-900 border border-gray-800 rounded-lg shadow-xl py-2 w-48 z-50">
|
||||||
{cameras.length === 0 ? (
|
{cameras.length === 0 ? (
|
||||||
<div className="px-4 py-2 text-gray-500 text-sm">No cameras found</div>
|
<div className="px-4 py-2 text-gray-500 text-sm">
|
||||||
) : (
|
No cameras found
|
||||||
cameras.map((device) => (
|
</div>
|
||||||
<button
|
|
||||||
key={device.deviceId}
|
|
||||||
onClick={() => handleSelectCamera(device.deviceId)}
|
|
||||||
className="w-full text-left px-4 py-2 text-sm text-white hover:bg-gray-800 transition-colors truncate"
|
|
||||||
>
|
|
||||||
{device.label || `Camera ${cameras.indexOf(device) + 1}`}
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className={`p-4 rounded-full backdrop-blur-md transition-colors ${
|
|
||||||
!isMicEnabled
|
|
||||||
? "bg-white text-black"
|
|
||||||
: "bg-gray-600/50 text-white hover:bg-gray-600/70"
|
|
||||||
}`}
|
|
||||||
onClick={handleMicToggle}
|
|
||||||
>
|
|
||||||
{isMicEnabled ? (
|
|
||||||
<MicIcon className="w-6 h-6" />
|
|
||||||
) : (
|
) : (
|
||||||
<MicOffIcon className="w-6 h-6" />
|
cameras.map((device) => (
|
||||||
|
<button
|
||||||
|
key={device.deviceId}
|
||||||
|
onClick={() => handleSelectCamera(device.deviceId)}
|
||||||
|
className="w-full text-left px-4 py-2 text-sm text-white hover:bg-gray-800 transition-colors truncate"
|
||||||
|
>
|
||||||
|
{device.label ||
|
||||||
|
`Camera ${cameras.indexOf(device) + 1}`}
|
||||||
|
</button>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</button>
|
</div>
|
||||||
|
)}
|
||||||
<button
|
</div>
|
||||||
className="p-4 rounded-full bg-red-500 text-white hover:bg-red-600 transition-colors"
|
</div>
|
||||||
onClick={handleDisconnect}
|
) : (
|
||||||
>
|
<div className="w-full flex items-center justify-center gap-8">
|
||||||
<PhoneOffIcon className="w-6 h-6" />
|
<button
|
||||||
</button>
|
className={`p-4 rounded-full backdrop-blur-md transition-colors ${
|
||||||
</>
|
!isMicEnabled
|
||||||
)}
|
? "bg-white text-black"
|
||||||
|
: "bg-gray-600/50 text-white hover:bg-gray-600/70"
|
||||||
|
}`}
|
||||||
|
onClick={handleMicToggle}
|
||||||
|
>
|
||||||
|
{isMicEnabled ? (
|
||||||
|
<MicIcon className="w-6 h-6" />
|
||||||
|
) : (
|
||||||
|
<MicOffIcon className="w-6 h-6" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="p-4 rounded-full bg-red-500 text-white hover:bg-red-600 transition-colors"
|
||||||
|
onClick={handleDisconnect}
|
||||||
|
>
|
||||||
|
<PhoneOffIcon className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export const SwitchCameraIcon = ({ className }: { className?: string }) => (
|
|||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
className={className}
|
className={className}
|
||||||
>
|
>
|
||||||
<path d="M20 4v5h-.58c-.89-3.1-3.85-5.23-7.17-5.23-4.26 0-7.74 3.4-7.74 7.6 0 .65.09 1.29.26 1.91l1.69-1.69c-.14-.22-.2-.46-.2-.72 0-3.2 2.63-5.81 5.87-5.81 2.67 0 4.96 1.75 5.6 4.23h-1.73l2.5 2.5 2.5-2.5h-1z" />
|
<path d="M4 12v-3a3 3 0 0 1 3-3h13m-3-3 3 3-3 3" />
|
||||||
<path d="M4 20v-5h.58c.89 3.1 3.85 5.23 7.17 5.23 4.26 0 7.74-3.4 7.74-7.6 0-.65-.09-1.29-.26-1.91l-1.69 1.69c.14.22.2.46.2.72 0 3.2-2.63 5.81-5.87 5.81-2.67 0-4.96-1.75-5.6-4.23h1.73l-2.5-2.5-2.5 2.5h1z" />
|
<path d="M20 12v3a3 3 0 0 1-3 3H4m3 3-3-3 3-3" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user