783 lines
31 KiB
TypeScript
783 lines
31 KiB
TypeScript
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<HTMLDivElement>(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<LookupResult | null>(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 }) => (
|
||
<SettingsDialog
|
||
onClose={() => {
|
||
dialogClose()
|
||
removeDialog("api-keys")
|
||
checkApiKeys()
|
||
}}
|
||
/>
|
||
),
|
||
})
|
||
}
|
||
|
||
const handleSetVault = () => {
|
||
window.dispatchEvent(new CustomEvent('open-obsidian-browser'))
|
||
onClose()
|
||
}
|
||
|
||
return (
|
||
<div className="settings-modal-overlay">
|
||
<div className="settings-modal" ref={modalRef}>
|
||
<div className="settings-modal-header">
|
||
<h2>Settings</h2>
|
||
<button className="settings-close-btn" onClick={onClose} title="Close (Esc)">
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="settings-tabs">
|
||
<button
|
||
className={`settings-tab ${activeTab === 'general' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('general')}
|
||
>
|
||
General
|
||
</button>
|
||
<button
|
||
className={`settings-tab ${activeTab === 'ai' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('ai')}
|
||
>
|
||
AI Models
|
||
</button>
|
||
<button
|
||
className={`settings-tab ${activeTab === 'integrations' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('integrations')}
|
||
>
|
||
Integrations
|
||
</button>
|
||
</div>
|
||
|
||
<div className="settings-content">
|
||
{activeTab === 'general' && (
|
||
<div className="settings-section">
|
||
<div className="settings-item">
|
||
<div className="settings-item-info">
|
||
<span className="settings-item-label">Appearance</span>
|
||
<span className="settings-item-description">Toggle between light and dark mode</span>
|
||
</div>
|
||
<button
|
||
className="settings-toggle-btn"
|
||
onClick={onToggleDarkMode}
|
||
>
|
||
<span className="toggle-icon">{isDarkMode ? '🌙' : '☀️'}</span>
|
||
<span>{isDarkMode ? 'Dark' : 'Light'}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="settings-divider" />
|
||
|
||
{/* CryptID Account Section */}
|
||
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: colors.text }}>
|
||
CryptID Account
|
||
</h3>
|
||
|
||
{session.authed && session.username ? (
|
||
<div
|
||
style={{
|
||
padding: '12px',
|
||
backgroundColor: colors.cardBg,
|
||
borderRadius: '8px',
|
||
border: `1px solid ${colors.cardBorder}`,
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||
<span style={{ fontSize: '20px' }}>🔐</span>
|
||
<div style={{ flex: 1 }}>
|
||
<span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>
|
||
{session.username}
|
||
</span>
|
||
<p style={{ fontSize: '11px', color: colors.textMuted, marginTop: '2px' }}>
|
||
Your CryptID username - cryptographically secured
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Email Section */}
|
||
<div style={{ marginTop: '12px', paddingTop: '12px', borderTop: `1px solid ${colors.dividerColor}` }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||
<span style={{ fontSize: '16px' }}>✉️</span>
|
||
<span style={{ fontSize: '12px', fontWeight: '500', color: colors.text }}>Email Recovery</span>
|
||
<span
|
||
className={`status-badge ${emailStatus?.emailVerified ? 'success' : 'warning'}`}
|
||
style={{ fontSize: '10px', marginLeft: 'auto' }}
|
||
>
|
||
{emailStatus?.emailVerified ? 'Verified' : emailStatus?.email ? 'Pending' : 'Not Set'}
|
||
</span>
|
||
</div>
|
||
|
||
{emailStatus?.email && (
|
||
<p style={{ fontSize: '11px', color: emailStatus.emailVerified ? colors.successText : colors.warningText, marginBottom: '8px' }}>
|
||
{emailStatus.email}
|
||
{!emailStatus.emailVerified && ' (verification pending)'}
|
||
</p>
|
||
)}
|
||
|
||
<p style={{ fontSize: '11px', color: colors.textMuted, marginBottom: '8px', lineHeight: '1.4' }}>
|
||
Link an email to recover your account on new devices. You'll receive a verification link.
|
||
</p>
|
||
|
||
{emailLinkMessage && (
|
||
<div
|
||
style={{
|
||
padding: '8px 12px',
|
||
borderRadius: '6px',
|
||
marginBottom: '8px',
|
||
backgroundColor: emailLinkMessage.type === 'success' ? colors.successBg : colors.errorBg,
|
||
color: emailLinkMessage.type === 'success' ? colors.successText : colors.errorText,
|
||
fontSize: '11px',
|
||
}}
|
||
>
|
||
{emailLinkMessage.text}
|
||
</div>
|
||
)}
|
||
|
||
{showEmailInput ? (
|
||
<div>
|
||
<input
|
||
type="email"
|
||
value={emailInput}
|
||
onChange={(e) => 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}
|
||
/>
|
||
<div style={{ display: 'flex', gap: '8px' }}>
|
||
<button
|
||
className="settings-btn-sm primary"
|
||
style={{ flex: 1 }}
|
||
onClick={handleLinkEmail}
|
||
disabled={emailLinkLoading || !emailInput.trim()}
|
||
>
|
||
{emailLinkLoading ? 'Sending...' : 'Send Verification'}
|
||
</button>
|
||
<button
|
||
className="settings-btn-sm"
|
||
style={{ flex: 1 }}
|
||
onClick={() => {
|
||
setShowEmailInput(false)
|
||
setEmailInput('')
|
||
setEmailLinkMessage(null)
|
||
}}
|
||
disabled={emailLinkLoading}
|
||
>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<button
|
||
className="settings-action-btn"
|
||
style={{ width: '100%' }}
|
||
onClick={() => setShowEmailInput(true)}
|
||
>
|
||
{emailStatus?.email ? 'Update Email' : 'Link Email'}
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div
|
||
style={{
|
||
padding: '12px',
|
||
backgroundColor: colors.warningBg,
|
||
borderRadius: '8px',
|
||
border: `1px solid ${colors.warningBorder}`,
|
||
}}
|
||
>
|
||
<p style={{ fontSize: '12px', color: colors.warningText }}>
|
||
Sign in to manage your CryptID account settings
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'ai' && (
|
||
<div className="settings-section">
|
||
{/* AI Tools Overview */}
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: colors.text }}>
|
||
AI Tools & Models
|
||
</h3>
|
||
<p style={{ fontSize: '12px', color: colors.textMuted, marginBottom: '16px', lineHeight: '1.4' }}>
|
||
Each tool uses optimized AI models. Local models run on your private server for free, cloud models require API keys.
|
||
</p>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||
{AI_TOOLS.map((tool) => (
|
||
<div
|
||
key={tool.id}
|
||
style={{
|
||
padding: '12px',
|
||
backgroundColor: colors.cardBg,
|
||
borderRadius: '8px',
|
||
border: `1px solid ${colors.cardBorder}`,
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
|
||
<span style={{ fontSize: '16px' }}>{tool.icon}</span>
|
||
<span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>{tool.name}</span>
|
||
</div>
|
||
<p style={{ fontSize: '11px', color: colors.textMuted, marginBottom: '8px' }}>{tool.description}</p>
|
||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
||
<span
|
||
style={{
|
||
fontSize: '10px',
|
||
padding: '3px 8px',
|
||
borderRadius: '12px',
|
||
backgroundColor: tool.models.primary.type === 'local' ? colors.localBg : tool.models.primary.type === 'gpu' ? colors.gpuBg : colors.cloudBg,
|
||
color: tool.models.primary.type === 'local' ? colors.localText : tool.models.primary.type === 'gpu' ? colors.gpuText : colors.cloudText,
|
||
fontWeight: '500',
|
||
}}
|
||
>
|
||
{tool.models.primary.name}: {tool.models.primary.model}
|
||
</span>
|
||
{tool.models.fallback && (
|
||
<span
|
||
style={{
|
||
fontSize: '10px',
|
||
padding: '3px 8px',
|
||
borderRadius: '12px',
|
||
backgroundColor: colors.fallbackBg,
|
||
color: colors.fallbackText,
|
||
fontWeight: '500',
|
||
}}
|
||
>
|
||
Fallback: {tool.models.fallback.model}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="settings-divider" />
|
||
|
||
{/* API Keys Configuration */}
|
||
<div className="settings-item">
|
||
<div className="settings-item-info">
|
||
<span className="settings-item-label">AI API Keys</span>
|
||
<span className="settings-item-description">
|
||
{hasApiKey ? 'Your cloud AI models are configured and ready' : 'Configure API keys to use cloud AI features'}
|
||
</span>
|
||
</div>
|
||
<div className="settings-item-status">
|
||
<span className={`status-badge ${hasApiKey ? 'success' : 'warning'}`}>
|
||
{hasApiKey ? 'Configured' : 'Not Set'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<button className="settings-action-btn" onClick={openApiKeysDialog}>
|
||
{hasApiKey ? 'Manage API Keys' : 'Add API Keys'}
|
||
</button>
|
||
|
||
{/* Model type legend */}
|
||
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: colors.legendBg, borderRadius: '6px', border: `1px solid ${colors.legendBorder}` }}>
|
||
<div style={{ fontSize: '11px', color: colors.textMuted, display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
|
||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: colors.localText }}></span>
|
||
Local (Free)
|
||
</span>
|
||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: colors.gpuText }}></span>
|
||
GPU (RunPod)
|
||
</span>
|
||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: colors.cloudText }}></span>
|
||
Cloud (API Key)
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'integrations' && (
|
||
<div className="settings-section">
|
||
{/* Knowledge Management Section */}
|
||
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: colors.text }}>
|
||
Knowledge Management
|
||
</h3>
|
||
|
||
{/* Obsidian Vault - Local Files */}
|
||
<div
|
||
style={{
|
||
padding: '12px',
|
||
backgroundColor: colors.cardBg,
|
||
borderRadius: '8px',
|
||
border: `1px solid ${colors.cardBorder}`,
|
||
marginBottom: '12px',
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||
<span style={{ fontSize: '20px' }}>📁</span>
|
||
<div style={{ flex: 1 }}>
|
||
<span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>Obsidian Vault (Local)</span>
|
||
<p style={{ fontSize: '11px', color: colors.textMuted, marginTop: '2px' }}>
|
||
Import notes directly from your local Obsidian vault
|
||
</p>
|
||
</div>
|
||
<span className={`status-badge ${session.obsidianVaultName ? 'success' : 'warning'}`} style={{ fontSize: '10px' }}>
|
||
{session.obsidianVaultName ? 'Connected' : 'Not Set'}
|
||
</span>
|
||
</div>
|
||
{session.obsidianVaultName && (
|
||
<p style={{ fontSize: '11px', color: colors.successText, marginBottom: '8px' }}>
|
||
Current vault: {session.obsidianVaultName}
|
||
</p>
|
||
)}
|
||
<button className="settings-action-btn" onClick={handleSetVault} style={{ width: '100%' }}>
|
||
{session.obsidianVaultName ? 'Change Vault' : 'Connect Vault'}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Obsidian Quartz - Published Notes */}
|
||
<div
|
||
style={{
|
||
padding: '12px',
|
||
backgroundColor: colors.cardBg,
|
||
borderRadius: '8px',
|
||
border: `1px solid ${colors.cardBorder}`,
|
||
marginBottom: '12px',
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||
<span style={{ fontSize: '20px' }}>🌐</span>
|
||
<div style={{ flex: 1 }}>
|
||
<span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>Obsidian Quartz (Web)</span>
|
||
<p style={{ fontSize: '11px', color: colors.textMuted, marginTop: '2px' }}>
|
||
Import notes from your published Quartz site via GitHub
|
||
</p>
|
||
</div>
|
||
<span className="status-badge success" style={{ fontSize: '10px' }}>
|
||
Available
|
||
</span>
|
||
</div>
|
||
<p style={{ fontSize: '11px', color: colors.textMuted, marginBottom: '8px', lineHeight: '1.4' }}>
|
||
Quartz is a static site generator for Obsidian. If you publish your notes with Quartz, you can browse and import them here.
|
||
</p>
|
||
<a
|
||
href="https://quartz.jzhao.xyz/"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
style={{
|
||
fontSize: '11px',
|
||
color: colors.linkColor,
|
||
textDecoration: 'none',
|
||
}}
|
||
>
|
||
Learn more about Quartz →
|
||
</a>
|
||
</div>
|
||
|
||
<div className="settings-divider" />
|
||
|
||
{/* Meeting & Communication Section */}
|
||
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', marginTop: '8px', color: colors.text }}>
|
||
Meeting & Communication
|
||
</h3>
|
||
|
||
{/* Fathom Meetings */}
|
||
<div
|
||
style={{
|
||
padding: '12px',
|
||
backgroundColor: colors.cardBg,
|
||
borderRadius: '8px',
|
||
border: `1px solid ${colors.cardBorder}`,
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||
<span style={{ fontSize: '20px' }}>🎥</span>
|
||
<div style={{ flex: 1 }}>
|
||
<span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>Fathom Meetings</span>
|
||
<p style={{ fontSize: '11px', color: colors.textMuted, marginTop: '2px' }}>
|
||
Import meeting transcripts and AI summaries
|
||
</p>
|
||
</div>
|
||
<span className={`status-badge ${hasFathomApiKey ? 'success' : 'warning'}`} style={{ fontSize: '10px' }}>
|
||
{hasFathomApiKey ? 'Connected' : 'Not Set'}
|
||
</span>
|
||
</div>
|
||
|
||
{showFathomApiKeyInput ? (
|
||
<div style={{ marginTop: '8px' }}>
|
||
<input
|
||
type="password"
|
||
value={fathomApiKeyInput}
|
||
onChange={(e) => 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
|
||
/>
|
||
<div style={{ display: 'flex', gap: '8px' }}>
|
||
<button
|
||
className="settings-btn-sm primary"
|
||
style={{ flex: 1 }}
|
||
onClick={() => {
|
||
if (fathomApiKeyInput.trim()) {
|
||
saveFathomApiKey(fathomApiKeyInput.trim(), session.username)
|
||
setHasFathomApiKey(true)
|
||
setShowFathomApiKeyInput(false)
|
||
setFathomApiKeyInput('')
|
||
}
|
||
}}
|
||
>
|
||
Save
|
||
</button>
|
||
<button
|
||
className="settings-btn-sm"
|
||
style={{ flex: 1 }}
|
||
onClick={() => {
|
||
setShowFathomApiKeyInput(false)
|
||
setFathomApiKeyInput('')
|
||
}}
|
||
>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
<a
|
||
href="https://app.usefathom.com/settings/integrations"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
style={{
|
||
display: 'block',
|
||
fontSize: '11px',
|
||
color: colors.linkColor,
|
||
textDecoration: 'none',
|
||
marginTop: '8px',
|
||
}}
|
||
>
|
||
Get your API key from Fathom Settings →
|
||
</a>
|
||
</div>
|
||
) : (
|
||
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
|
||
<button
|
||
className="settings-action-btn"
|
||
style={{ flex: 1 }}
|
||
onClick={() => {
|
||
setShowFathomApiKeyInput(true)
|
||
const currentKey = getFathomApiKey(session.username)
|
||
if (currentKey) setFathomApiKeyInput(currentKey)
|
||
}}
|
||
>
|
||
{hasFathomApiKey ? 'Change API Key' : 'Add API Key'}
|
||
</button>
|
||
{hasFathomApiKey && (
|
||
<button
|
||
className="settings-action-btn secondary"
|
||
onClick={() => {
|
||
removeFathomApiKey(session.username)
|
||
setHasFathomApiKey(false)
|
||
}}
|
||
>
|
||
Disconnect
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Future Integrations Placeholder */}
|
||
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: colors.legendBg, borderRadius: '6px', border: `1px dashed ${colors.cardBorder}` }}>
|
||
<p style={{ fontSize: '12px', color: colors.textMuted, textAlign: 'center' }}>
|
||
More integrations coming soon: Google Calendar, Notion, and more
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|