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:
John Remillard committed 2026-03-04 11:43:39 -05:00
1 parent e92c489db5
commit a4946ed04d
18 files changed
+3585 -320

No files matched your search

+91
View File
@@ -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;