Introduces `characterName` to the `Scenario` type, enabling AI personas to be explicitly named. This change also updates the `ScenarioCard` component to display the character name and adds foundational support for potential future scenario deletion functionality. The README has been updated with a new AI Studio link.
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
|
|
import React, { useEffect, useState } from 'react';
|
|
import { Mic, Check, AlertCircle } from 'lucide-react';
|
|
|
|
interface AudioDeviceSelectorProps {
|
|
selectedDeviceId?: string;
|
|
onSelect: (deviceId: string) => void;
|
|
className?: string;
|
|
}
|
|
|
|
const AudioDeviceSelector: React.FC<AudioDeviceSelectorProps> = ({
|
|
selectedDeviceId,
|
|
onSelect,
|
|
className = ""
|
|
}) => {
|
|
const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const getDevices = async () => {
|
|
try {
|
|
// Request permission first to get labels
|
|
await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
const allDevices = await navigator.mediaDevices.enumerateDevices();
|
|
const audioInputs = allDevices.filter(device => device.kind === 'audioinput');
|
|
setDevices(audioInputs);
|
|
|
|
// If no device is selected but we have devices, select the first one
|
|
if (!selectedDeviceId && audioInputs.length > 0) {
|
|
onSelect(audioInputs[0].deviceId);
|
|
}
|
|
} catch (err) {
|
|
console.error("Error accessing audio devices:", err);
|
|
setError("Microphone access denied. Please check your browser permissions.");
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
getDevices();
|
|
|
|
// Listen for device changes (e.g., plugging in a new mic)
|
|
navigator.mediaDevices.addEventListener('devicechange', getDevices);
|
|
return () => {
|
|
navigator.mediaDevices.removeEventListener('devicechange', getDevices);
|
|
};
|
|
}, []);
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex items-center gap-2 p-3 bg-red-50 border border-red-100 rounded-xl text-red-600 text-xs">
|
|
<AlertCircle size={14} />
|
|
<span>{error}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`space-y-2 ${className}`}>
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-2 mb-1">
|
|
<Mic size={12} />
|
|
Input Device
|
|
</label>
|
|
<div className="grid grid-cols-1 gap-2">
|
|
{devices.length === 0 ? (
|
|
<p className="text-xs text-slate-400 italic p-2">Searching for devices...</p>
|
|
) : (
|
|
devices.map((device) => (
|
|
<button
|
|
key={device.deviceId}
|
|
onClick={() => onSelect(device.deviceId)}
|
|
className={`flex items-center justify-between p-3 rounded-xl border transition-all text-left text-xs ${
|
|
selectedDeviceId === device.deviceId || (device.deviceId === 'default' && !selectedDeviceId)
|
|
? 'bg-teal-50 border-teal-500 text-teal-900'
|
|
: 'bg-white border-slate-100 hover:border-slate-200 text-slate-600'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-3 truncate">
|
|
<Mic size={14} className={selectedDeviceId === device.deviceId ? 'text-teal-600' : 'text-slate-400'} />
|
|
<span className="truncate font-medium">{device.label || `Microphone ${device.deviceId.slice(0, 5)}...`}</span>
|
|
</div>
|
|
{(selectedDeviceId === device.deviceId || (device.deviceId === 'default' && !selectedDeviceId)) && (
|
|
<Check size={14} className="text-teal-600 flex-shrink-0" />
|
|
)}
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AudioDeviceSelector;
|