feat: Add character names to scenarios and UI
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.
This commit is contained in:
1 parent
e92c489db5
commit
a4946ed04d
18 files changed
+3585
-320
No files matched your search
@@ -0,0 +1,91 @@
|
||||
|
||||
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;
|
||||
Reference in new issue
Block a user