import { useState, useEffect, useRef } from "react" import { useAuth } from "../context/AuthContext" import { useDialogs } from "tldraw" import { SettingsDialog } from "./SettingsDialog" import { getFathomApiKey, saveFathomApiKey, removeFathomApiKey, isFathomApiKeyConfigured } from "../lib/fathomApiKey" import { linkEmailToAccount, checkEmailStatus, type LookupResult } from "../lib/auth/cryptidEmailService" // AI tool model configurations const AI_TOOLS = [ { id: 'chat', name: 'Chat Assistant', icon: '💬', description: 'Conversational AI for questions and discussions', models: { primary: { name: 'Ollama (Local)', model: 'llama3.1:8b', type: 'local' }, fallback: { name: 'OpenAI', model: 'gpt-4o', type: 'cloud' }, } }, { id: 'make-real', name: 'Make Real', icon: '🔧', description: 'Convert wireframes to working prototypes', models: { primary: { name: 'Anthropic', model: 'claude-sonnet-4-5', type: 'cloud' }, fallback: { name: 'OpenAI', model: 'gpt-4o', type: 'cloud' }, } }, { id: 'image-gen', name: 'Image Generation', icon: '🎨', description: 'Generate images from text prompts', models: { primary: { name: 'RunPod', model: 'Stable Diffusion XL', type: 'gpu' }, } }, { id: 'video-gen', name: 'Video Generation', icon: '🎬', description: 'Generate videos from images', models: { primary: { name: 'RunPod', model: 'Wan2.1 I2V', type: 'gpu' }, } }, { id: 'transcription', name: 'Transcription', icon: '🎤', description: 'Transcribe audio to text', models: { primary: { name: 'Browser', model: 'Web Speech API', type: 'local' }, fallback: { name: 'Whisper', model: 'whisper-large-v3', type: 'local' }, } }, { id: 'mycelial', name: 'Mycelial Intelligence', icon: '🍄', description: 'Analyze connections between concepts', models: { primary: { name: 'Ollama (Local)', model: 'llama3.1:70b', type: 'local' }, fallback: { name: 'Anthropic', model: 'claude-sonnet-4-5', type: 'cloud' }, } }, ] interface UserSettingsModalProps { onClose: () => void isDarkMode: boolean onToggleDarkMode: () => void } export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: UserSettingsModalProps) { const { session, setSession } = useAuth() const { addDialog, removeDialog } = useDialogs() const modalRef = useRef(null) const [hasApiKey, setHasApiKey] = useState(false) const [hasFathomApiKey, setHasFathomApiKey] = useState(false) const [showFathomApiKeyInput, setShowFathomApiKeyInput] = useState(false) const [fathomApiKeyInput, setFathomApiKeyInput] = useState('') const [activeTab, setActiveTab] = useState<'general' | 'ai' | 'integrations'>('general') // Dark mode aware colors const colors = isDarkMode ? { cardBg: '#252525', cardBorder: '#404040', text: '#e4e4e4', textMuted: '#a1a1aa', textHeading: '#f4f4f5', warningBg: '#3d3620', warningBorder: '#665930', warningText: '#fbbf24', successBg: '#1a3d2e', successText: '#34d399', errorBg: '#3d2020', errorText: '#f87171', localBg: '#1a3d2e', localText: '#34d399', gpuBg: '#1e2756', gpuText: '#818cf8', cloudBg: '#3d3620', cloudText: '#fbbf24', fallbackBg: '#2d2d2d', fallbackText: '#a1a1aa', legendBg: '#252525', legendBorder: '#404040', linkColor: '#60a5fa', dividerColor: '#404040', } : { cardBg: '#f9fafb', cardBorder: '#e5e7eb', text: '#374151', textMuted: '#6b7280', textHeading: '#1f2937', warningBg: '#fef3c7', warningBorder: '#fcd34d', warningText: '#92400e', successBg: '#d1fae5', successText: '#065f46', errorBg: '#fee2e2', errorText: '#991b1b', localBg: '#d1fae5', localText: '#065f46', gpuBg: '#e0e7ff', gpuText: '#3730a3', cloudBg: '#fef3c7', cloudText: '#92400e', fallbackBg: '#f3f4f6', fallbackText: '#6b7280', legendBg: '#f8fafc', legendBorder: '#e2e8f0', linkColor: '#3b82f6', dividerColor: '#e5e7eb', } // Email linking state const [emailStatus, setEmailStatus] = useState(null) const [showEmailInput, setShowEmailInput] = useState(false) const [emailInput, setEmailInput] = useState('') const [emailLinkLoading, setEmailLinkLoading] = useState(false) const [emailLinkMessage, setEmailLinkMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) // Check API key status const checkApiKeys = () => { const settings = localStorage.getItem("openai_api_key") try { if (settings) { try { const parsed = JSON.parse(settings) if (parsed.keys) { const hasValidKey = Object.values(parsed.keys).some(key => typeof key === 'string' && key.trim() !== '' ) setHasApiKey(hasValidKey) } else { setHasApiKey(typeof settings === 'string' && settings.trim() !== '') } } catch (e) { setHasApiKey(typeof settings === 'string' && settings.trim() !== '') } } else { setHasApiKey(false) } } catch (e) { setHasApiKey(false) } } useEffect(() => { checkApiKeys() }, []) useEffect(() => { if (session.authed && session.username) { setHasFathomApiKey(isFathomApiKeyConfigured(session.username)) } }, [session.authed, session.username]) // Check email status when modal opens useEffect(() => { const fetchEmailStatus = async () => { if (session.authed && session.username) { const status = await checkEmailStatus(session.username) setEmailStatus(status) } } fetchEmailStatus() }, [session.authed, session.username]) // Handle email linking const handleLinkEmail = async () => { if (!emailInput.trim() || !session.username) return setEmailLinkLoading(true) setEmailLinkMessage(null) try { const result = await linkEmailToAccount(emailInput.trim(), session.username) if (result.success) { if (result.emailSent) { setEmailLinkMessage({ type: 'success', text: 'Verification email sent! Check your inbox to confirm.' }) } else if (result.emailVerified) { setEmailLinkMessage({ type: 'success', text: 'Email already verified and linked!' }) } else { setEmailLinkMessage({ type: 'success', text: 'Email linked successfully!' }) } setShowEmailInput(false) setEmailInput('') // Refresh status const status = await checkEmailStatus(session.username) setEmailStatus(status) } else { setEmailLinkMessage({ type: 'error', text: result.error || 'Failed to link email' }) } } catch (error) { setEmailLinkMessage({ type: 'error', text: 'An error occurred while linking email' }) } finally { setEmailLinkLoading(false) } } // Handle escape key and click outside useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape') { onClose() } } const handleClickOutside = (e: MouseEvent) => { if (modalRef.current && !modalRef.current.contains(e.target as Node)) { onClose() } } document.addEventListener('keydown', handleEscape) document.addEventListener('mousedown', handleClickOutside) return () => { document.removeEventListener('keydown', handleEscape) document.removeEventListener('mousedown', handleClickOutside) } }, [onClose]) const openApiKeysDialog = () => { addDialog({ id: "api-keys", component: ({ onClose: dialogClose }: { onClose: () => void }) => ( { dialogClose() removeDialog("api-keys") checkApiKeys() }} /> ), }) } const handleSetVault = () => { window.dispatchEvent(new CustomEvent('open-obsidian-browser')) onClose() } return (

Settings

{activeTab === 'general' && (
Appearance Toggle between light and dark mode
{/* CryptID Account Section */}

CryptID Account

{session.authed && session.username ? (
🔐
{session.username}

Your CryptID username - cryptographically secured

{/* Email Section */}
✉️ Email Recovery {emailStatus?.emailVerified ? 'Verified' : emailStatus?.email ? 'Pending' : 'Not Set'}
{emailStatus?.email && (

{emailStatus.email} {!emailStatus.emailVerified && ' (verification pending)'}

)}

Link an email to recover your account on new devices. You'll receive a verification link.

{emailLinkMessage && (
{emailLinkMessage.text}
)} {showEmailInput ? (
setEmailInput(e.target.value)} placeholder="Enter your email address..." className="settings-input" style={{ width: '100%', marginBottom: '8px' }} onKeyDown={(e) => { if (e.key === 'Enter' && emailInput.trim()) { handleLinkEmail() } else if (e.key === 'Escape') { setShowEmailInput(false) setEmailInput('') } }} autoFocus disabled={emailLinkLoading} />
) : ( )}
) : (

Sign in to manage your CryptID account settings

)}
)} {activeTab === 'ai' && (
{/* AI Tools Overview */}

AI Tools & Models

Each tool uses optimized AI models. Local models run on your private server for free, cloud models require API keys.

{AI_TOOLS.map((tool) => (
{tool.icon} {tool.name}

{tool.description}

{tool.models.primary.name}: {tool.models.primary.model} {tool.models.fallback && ( Fallback: {tool.models.fallback.model} )}
))}
{/* API Keys Configuration */}
AI API Keys {hasApiKey ? 'Your cloud AI models are configured and ready' : 'Configure API keys to use cloud AI features'}
{hasApiKey ? 'Configured' : 'Not Set'}
{/* Model type legend */}
Local (Free) GPU (RunPod) Cloud (API Key)
)} {activeTab === 'integrations' && (
{/* Knowledge Management Section */}

Knowledge Management

{/* Obsidian Vault - Local Files */}
📁
Obsidian Vault (Local)

Import notes directly from your local Obsidian vault

{session.obsidianVaultName ? 'Connected' : 'Not Set'}
{session.obsidianVaultName && (

Current vault: {session.obsidianVaultName}

)}
{/* Obsidian Quartz - Published Notes */}
🌐
Obsidian Quartz (Web)

Import notes from your published Quartz site via GitHub

Available

Quartz is a static site generator for Obsidian. If you publish your notes with Quartz, you can browse and import them here.

Learn more about Quartz →
{/* Meeting & Communication Section */}

Meeting & Communication

{/* Fathom Meetings */}
🎥
Fathom Meetings

Import meeting transcripts and AI summaries

{hasFathomApiKey ? 'Connected' : 'Not Set'}
{showFathomApiKeyInput ? (
setFathomApiKeyInput(e.target.value)} placeholder="Enter Fathom API key..." className="settings-input" style={{ width: '100%', marginBottom: '8px' }} onKeyDown={(e) => { if (e.key === 'Enter' && fathomApiKeyInput.trim()) { saveFathomApiKey(fathomApiKeyInput.trim(), session.username) setHasFathomApiKey(true) setShowFathomApiKeyInput(false) setFathomApiKeyInput('') } else if (e.key === 'Escape') { setShowFathomApiKeyInput(false) setFathomApiKeyInput('') } }} autoFocus />
Get your API key from Fathom Settings →
) : (
{hasFathomApiKey && ( )}
)}
{/* Future Integrations Placeholder */}

More integrations coming soon: Google Calendar, Notion, and more

)}
) }