"use client"; import { useState, useEffect, useCallback } from "react"; import { createPortal } from "react-dom"; import { getSettings, saveSettings, type BeadsMapSettings, } from "@/lib/settings"; interface SettingsModalProps { isOpen: boolean; onClose: () => void; } export function SettingsModal({ isOpen, onClose }: SettingsModalProps) { const [apiKey, setApiKey] = useState(""); const [voiceId, setVoiceId] = useState(""); const [model, setModel] = useState(""); const [showKey, setShowKey] = useState(false); const [mounted, setMounted] = useState(false); const [isGateProtected, setIsGateProtected] = useState(false); // SSR guard for createPortal useEffect(() => { setMounted(true); }, []); // Load current settings when modal opens useEffect(() => { if (isOpen) { const s = getSettings(); setApiKey(s.elevenLabsApiKey || ""); setVoiceId(s.elevenLabsVoiceId); setModel(s.elevenLabsModel); setShowKey(false); // Check if dashboard is password-protected fetch("/api/auth") .then((r) => r.json()) .then((data) => setIsGateProtected(data.protected === true)) .catch(() => setIsGateProtected(false)); } }, [isOpen]); // Escape key useEffect(() => { if (!isOpen) return; const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [isOpen, onClose]); const handleLockDashboard = useCallback(async () => { try { await fetch("/api/auth", { method: "DELETE" }); } catch { // If delete fails, still redirect — middleware will catch it } window.location.href = "/login"; }, []); const handleSave = useCallback(() => { const updates: Partial = { elevenLabsApiKey: apiKey.trim() || undefined, elevenLabsVoiceId: voiceId.trim() || "UgBBYS2sOqTuMpoF3BR0", elevenLabsModel: model || "eleven_flash_v2_5", }; saveSettings(updates); onClose(); }, [apiKey, voiceId, model, onClose]); if (!isOpen || !mounted) return null; return createPortal(
e.stopPropagation()} > {/* Header */}

Settings

{/* Body */}
{/* Section header */}

Text-to-Speech

{/* API Key */}
setApiKey(e.target.value)} placeholder="sk_..." className="w-full rounded-lg border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-emerald-500/30 focus:border-emerald-500 pr-10" />

Get your key at{" "} elevenlabs.io/app/settings

{/* Voice ID */}
setVoiceId(e.target.value)} placeholder="UgBBYS2sOqTuMpoF3BR0" className="w-full rounded-lg border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-emerald-500/30 focus:border-emerald-500" />

Default: Mark - Natural Conversations. Find voice IDs in the{" "} ElevenLabs Voice Library .

{/* Model */}
{/* Divider */}

Public API

Heartbeads exposes a read-only REST API for AI agents, CI/CD bots, and integrations.{" "} {isGateProtected ? "API requests require the dashboard password as a Bearer token. " : "No authentication required. "} View API documentation

{/* Dashboard Access — only shown when password-protected */} {isGateProtected && (

Dashboard Access

This instance is password-protected. The{" "} Sign In{" "} button in the navbar is for{" "} ATProto/Bluesky {" "} — used for posting comments, likes, and claiming tasks. The dashboard password controls who can view the graph and access the API.

)}
{/* Footer */}
, document.body ); }