Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,27 @@ import StrobeCanvas from './components/StrobeCanvas';
import { AppState, StrobeStyle } from './types';

const App: React.FC = () => {
const SAFE_MAX_HZ = 20;
const ABSOLUTE_MAX_HZ = 60;
const [state, setState] = useState<AppState>(AppState.DISCLAIMER);
const [frequency, setFrequency] = useState(10);
const [colors, setColors] = useState(['#ffffff', '#000000']);
const [style, setStyle] = useState<StrobeStyle>(StrobeStyle.FIXED);
const [isActive, setIsActive] = useState(false);
const [audioLevel, setAudioLevel] = useState(0);
const [safetyLock, setSafetyLock] = useState(true);

const audioContextRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const streamRef = useRef<MediaStream | null>(null);

const handleStart = () => setState(AppState.IDLE);

const safeCloseAudioContext = async () => {
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop());
streamRef.current = null;
}
if (audioContextRef.current && audioContextRef.current.state !== 'closed') {
try {
await audioContextRef.current.close();
Expand All @@ -37,6 +45,7 @@ const App: React.FC = () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
if (!isMounted) return;
streamRef.current = stream;

const ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
const analyser = ctx.createAnalyser();
Expand Down Expand Up @@ -136,6 +145,7 @@ const App: React.FC = () => {
isActive={isActive}
style={style}
audioLevel={audioLevel}
maxFrequency={safetyLock ? SAFE_MAX_HZ : ABSOLUTE_MAX_HZ}
/>

{/* Persistent Play/Pause Button - Responsive to selected theme color */}
Expand Down Expand Up @@ -171,10 +181,31 @@ const App: React.FC = () => {
<span className="text-white font-mono text-xl">{style === StrobeStyle.AUDIO ? 'VOX' : `${frequency.toFixed(1)}Hz`}</span>
</div>
<input
type="range" min="0.5" max="60" step="0.5" value={frequency}
onChange={(e) => { setFrequency(parseFloat(e.target.value)); setStyle(StrobeStyle.FIXED); }}
type="range" min="0.5" max={safetyLock ? SAFE_MAX_HZ : ABSOLUTE_MAX_HZ} step="0.5" value={frequency}
onChange={(e) => {
const next = parseFloat(e.target.value);
setFrequency(next);
setStyle(StrobeStyle.FIXED);
}}
className="w-full h-1.5 bg-zinc-800 rounded-full appearance-none cursor-pointer accent-blue-500"
/>
<div className="flex items-center justify-between pt-2 text-[10px]">
<label className="flex items-center gap-2 text-zinc-400 uppercase tracking-wider font-bold cursor-pointer">
<input
type="checkbox"
checked={safetyLock}
onChange={(e) => {
const locked = e.target.checked;
setSafetyLock(locked);
if (locked && frequency > SAFE_MAX_HZ) {
setFrequency(SAFE_MAX_HZ);
}
}}
/>
Safety Lock ({SAFE_MAX_HZ}Hz max)
</label>
{!safetyLock && <span className="text-amber-400 uppercase tracking-wider font-black">Advanced mode</span>}
</div>
</div>

<div className="grid grid-cols-4 gap-2">
Expand Down
7 changes: 5 additions & 2 deletions components/StrobeCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ interface StrobeCanvasProps {
isActive: boolean;
style: StrobeStyle;
audioLevel?: number; // 0 to 1
maxFrequency?: number;
}

const StrobeCanvas: React.FC<StrobeCanvasProps> = ({
frequency,
colors,
isActive,
style,
audioLevel = 0
audioLevel = 0,
maxFrequency = 60
}) => {
const canvasRef = useRef<HTMLDivElement>(null);
const lastToggleTimeRef = useRef<number>(0);
Expand All @@ -33,8 +35,9 @@ const StrobeCanvas: React.FC<StrobeCanvasProps> = ({

let effectiveFreq = frequency;
if (style === StrobeStyle.AUDIO) {
effectiveFreq = audioLevel * 60; // Up to 60Hz
effectiveFreq = audioLevel * maxFrequency;
}
effectiveFreq = Math.min(effectiveFreq, maxFrequency);

const cycleMs = effectiveFreq > 0 ? 1000 / (effectiveFreq * 2) : Infinity;

Expand Down
9 changes: 8 additions & 1 deletion services/proxyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

const PROXY_URL = import.meta.env.VITE_PROXY_URL || 'https://gemini-proxy-572556903588.us-central1.run.app';
const PROXY_TIMEOUT_MS = Number(import.meta.env.VITE_PROXY_TIMEOUT_MS || 20000);

interface GenerateContentRequest {
model: string;
Expand All @@ -27,12 +28,16 @@ interface GenerateContentResponse {
export const generateContent = async (
request: GenerateContentRequest
): Promise<GenerateContentResponse> => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), PROXY_TIMEOUT_MS);

try {
const response = await fetch(`${PROXY_URL}/v1/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
signal: controller.signal,
body: JSON.stringify({
model: request.model,
prompt: request.contents,
Expand All @@ -43,7 +48,7 @@ export const generateContent = async (
});

if (!response.ok) {
const errorText = await response.text();
const errorText = await response.text().catch(() => '');
throw new Error(`Proxy request failed: ${response.status} - ${errorText}`);
}

Expand All @@ -55,6 +60,8 @@ export const generateContent = async (
} catch (error) {
console.error('Proxy service error:', error);
throw error;
} finally {
clearTimeout(timeoutId);
}
};

Expand Down