UX overhaul: navigation, category grouping, setup guidance, checklist
Fix 1: list_providers returns category, warning, sync_status
Fix 2: Gear icon in header (offset from HelpButton) links to /settings
Fix 3: Integrations page grouped by category (Devices → AI → Nutrition →
Notifications) with section headers, explanatory text, sync status
badges, COROS/coming-soon handling, link to /agent, hash-scroll
Fix 4: Coach page two-path setup (built-in prominent, external agent
subtle link to /agent), anchor to #ai-coaching
Fix 5: Settings 'all 11 providers' → 'All integrations', AI Agent link
Fix 6: AgentConnect back-link to integrations
Fix 7: Dashboard getting-started checklist (dismiss to localStorage)
This commit is contained in:
parent
c56d6d5816
commit
04e2af1a85
7 changed files with 303 additions and 82 deletions
|
|
@ -235,12 +235,23 @@ async def list_providers():
|
||||||
"""Return the provider registry with setup instructions."""
|
"""Return the provider registry with setup instructions."""
|
||||||
result = {}
|
result = {}
|
||||||
for key, info in PROVIDER_REGISTRY.items():
|
for key, info in PROVIDER_REGISTRY.items():
|
||||||
|
# Determine sync status for device providers
|
||||||
|
if info.get("type") == "mcp_bridge":
|
||||||
|
sync_status = "mcp_bridge"
|
||||||
|
elif info.get("category") == "device" and not info.get("sync_service"):
|
||||||
|
sync_status = "coming_soon"
|
||||||
|
else:
|
||||||
|
sync_status = "active"
|
||||||
|
|
||||||
result[key] = {
|
result[key] = {
|
||||||
"name": info["name"],
|
"name": info["name"],
|
||||||
"type": info["type"],
|
"type": info["type"],
|
||||||
|
"category": info.get("category", "other"),
|
||||||
"fields": info["fields"],
|
"fields": info["fields"],
|
||||||
"help_url": info.get("help_url", ""),
|
"help_url": info.get("help_url", ""),
|
||||||
"help_text": info.get("help_text", "").replace("{BASE_URL}", settings.base_url),
|
"help_text": info.get("help_text", "").replace("{BASE_URL}", settings.base_url),
|
||||||
|
"warning": info.get("warning", ""),
|
||||||
|
"sync_status": sync_status,
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,18 @@ export default function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{hasToken() && <HelpButton />}
|
{hasToken() && <HelpButton />}
|
||||||
|
{hasToken() && (
|
||||||
|
<NavLink to="/settings" style={{
|
||||||
|
position: 'fixed', top: '12px', right: '52px', zIndex: 90,
|
||||||
|
width: '36px', height: '36px', borderRadius: '50%',
|
||||||
|
background: 'var(--card)', border: '1px solid var(--card-border)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
textDecoration: 'none', fontSize: '1.1rem', boxShadow: 'var(--shadow-1)',
|
||||||
|
color: 'var(--text-3)',
|
||||||
|
}}>
|
||||||
|
⚙️
|
||||||
|
</NavLink>
|
||||||
|
)}
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/onboarding" element={<ProtectedRoute><Onboarding /></ProtectedRoute>} />
|
<Route path="/onboarding" element={<ProtectedRoute><Onboarding /></ProtectedRoute>} />
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,12 @@ export default function AgentConnect() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
|
<a href="/settings/integrations" style={{
|
||||||
|
fontSize: '0.82rem', color: 'var(--accent)', textDecoration: 'none',
|
||||||
|
display: 'inline-block', marginBottom: '12px',
|
||||||
|
}}>
|
||||||
|
← Back to Integrations
|
||||||
|
</a>
|
||||||
<div style={{ marginBottom: '20px' }}>
|
<div style={{ marginBottom: '20px' }}>
|
||||||
<h1 style={{
|
<h1 style={{
|
||||||
fontFamily: 'var(--font-display)', fontWeight: 800,
|
fontFamily: 'var(--font-display)', fontWeight: 800,
|
||||||
|
|
|
||||||
|
|
@ -94,17 +94,22 @@ export default function ChatCoach() {
|
||||||
<h1 className="page-title">AI Coach</h1>
|
<h1 className="page-title">AI Coach</h1>
|
||||||
<div className="card" style={{ textAlign: 'center', padding: '32px 20px' }}>
|
<div className="card" style={{ textAlign: 'center', padding: '32px 20px' }}>
|
||||||
<div style={{ fontSize: '2rem', marginBottom: '12px' }}>🤖</div>
|
<div style={{ fontSize: '2rem', marginBottom: '12px' }}>🤖</div>
|
||||||
<h3 style={{ marginBottom: '8px' }}>No AI provider connected</h3>
|
<h3 style={{ marginBottom: '8px' }}>Set up your AI coach</h3>
|
||||||
<p style={{ color: 'var(--text-2)', fontSize: '0.9rem', marginBottom: '16px' }}>
|
<p style={{ color: 'var(--text-2)', fontSize: '0.9rem', marginBottom: '20px' }}>
|
||||||
Connect OpenAI, OpenRouter, Claude, Ollama, or any other provider to start chatting with your AI fitness coach.
|
Connect an AI provider to chat with your fitness coach right here. OpenRouter, Groq, and Hugging Face all have free tiers.
|
||||||
</p>
|
</p>
|
||||||
<a href="/settings/integrations" className="btn-primary" style={{
|
<a href="/settings/integrations#ai-coaching" className="btn-primary" style={{
|
||||||
display: 'inline-block', padding: '12px 24px', borderRadius: 'var(--r)',
|
display: 'inline-block', padding: '12px 24px', borderRadius: 'var(--r)',
|
||||||
background: 'var(--accent)', color: 'var(--text-inv)', textDecoration: 'none',
|
background: 'var(--accent)', color: 'var(--text-inv)', textDecoration: 'none',
|
||||||
fontWeight: 600, fontSize: '0.88rem',
|
fontWeight: 600, fontSize: '0.88rem',
|
||||||
}}>
|
}}>
|
||||||
Configure AI Provider
|
Set up AI Provider
|
||||||
</a>
|
</a>
|
||||||
|
<p style={{ marginTop: '16px', fontSize: '0.78rem', color: 'var(--text-3)' }}>
|
||||||
|
Advanced: <a href="/agent" style={{ color: 'var(--text-3)', textDecoration: 'underline' }}>
|
||||||
|
connect Claude Desktop, Cursor, or Claude Code via MCP
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ function Tip({ text, children }) {
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [status, setStatus] = useState(null)
|
const [status, setStatus] = useState(null)
|
||||||
const [integrations, setIntegrations] = useState(null)
|
const [integrations, setIntegrations] = useState(null)
|
||||||
|
const [aiStatus, setAiStatus] = useState(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [syncing, setSyncing] = useState(null)
|
const [syncing, setSyncing] = useState(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
@ -39,9 +40,10 @@ export default function Dashboard() {
|
||||||
useEffect(() => { loadAll() }, [])
|
useEffect(() => { loadAll() }, [])
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
try {
|
try {
|
||||||
const [s, intg] = await Promise.all([api.today(), api.integrationStatus()])
|
const [s, intg, ai] = await Promise.all([api.today(), api.integrationStatus(), api.getAIStatus().catch(() => ({ configured: false }))])
|
||||||
setStatus(s)
|
setStatus(s)
|
||||||
setIntegrations(intg)
|
setIntegrations(intg)
|
||||||
|
setAiStatus(ai)
|
||||||
} catch (err) { console.error(err) }
|
} catch (err) { console.error(err) }
|
||||||
finally { setLoading(false) }
|
finally { setLoading(false) }
|
||||||
}
|
}
|
||||||
|
|
@ -83,8 +85,77 @@ export default function Dashboard() {
|
||||||
{ key: 'daily_checkin', label: 'Check-in', icon: '✅', pts: 10, color: '#00BCD4', tip: 'Just show up and check in. The easiest points — consistency matters more than intensity.' },
|
{ key: 'daily_checkin', label: 'Check-in', icon: '✅', pts: 10, color: '#00BCD4', tip: 'Just show up and check in. The easiest points — consistency matters more than intensity.' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Getting-started checklist — shows until dismissed
|
||||||
|
const [showChecklist, setShowChecklist] = useState(() => {
|
||||||
|
return localStorage.getItem('diligence_setup_dismissed') !== 'true'
|
||||||
|
})
|
||||||
|
|
||||||
|
const setupItems = [
|
||||||
|
{ done: true, label: 'Create account', link: null },
|
||||||
|
{ done: !!status.program_name, label: 'Join a program', link: '/programs' },
|
||||||
|
{ done: aiStatus?.configured, label: 'Connect an AI coach', link: '/settings/integrations#ai-coaching' },
|
||||||
|
{ done: Object.values(integrations || {}).some(v => v?.connected), label: 'Connect a fitness device', link: '/settings/integrations#fitness-devices' },
|
||||||
|
{ done: false, label: 'Set up rewards', link: '/rewards' },
|
||||||
|
]
|
||||||
|
const completedCount = setupItems.filter(i => i.done).length
|
||||||
|
const allDone = completedCount === setupItems.length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
|
{/* Getting started checklist */}
|
||||||
|
{showChecklist && !allDone && (
|
||||||
|
<div style={{
|
||||||
|
background: 'var(--card)', borderRadius: 'var(--r)', padding: '16px 18px',
|
||||||
|
marginBottom: '14px', border: '1px solid var(--card-border)',
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '0.72rem', fontWeight: 700, textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.08em', color: 'var(--accent)', fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
Getting Started
|
||||||
|
</div>
|
||||||
|
<button onClick={() => { setShowChecklist(false); localStorage.setItem('diligence_setup_dismissed', 'true') }}
|
||||||
|
style={{ background: 'none', border: 'none', color: 'var(--text-3)', cursor: 'pointer', fontSize: '1rem', padding: 0 }}>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{setupItems.map((item, i) => (
|
||||||
|
<div key={i}
|
||||||
|
onClick={() => item.link && !item.done && navigate(item.link)}
|
||||||
|
style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: '10px',
|
||||||
|
padding: '7px 0',
|
||||||
|
cursor: item.link && !item.done ? 'pointer' : 'default',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{
|
||||||
|
width: '18px', height: '18px', borderRadius: '50%', display: 'flex',
|
||||||
|
alignItems: 'center', justifyContent: 'center', fontSize: '0.65rem', flexShrink: 0,
|
||||||
|
background: item.done ? 'var(--green)' : 'transparent',
|
||||||
|
border: item.done ? 'none' : '2px solid var(--card-border)',
|
||||||
|
color: item.done ? 'var(--text-inv)' : 'var(--text-3)',
|
||||||
|
}}>
|
||||||
|
{item.done ? '✓' : ''}
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: '0.88rem', fontWeight: 500,
|
||||||
|
color: item.done ? 'var(--text-3)' : 'var(--text)',
|
||||||
|
textDecoration: item.done ? 'line-through' : 'none',
|
||||||
|
}}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
{item.link && !item.done && (
|
||||||
|
<span style={{ marginLeft: 'auto', color: 'var(--accent)', fontSize: '0.78rem', fontWeight: 600 }}>›</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div style={{ fontSize: '0.75rem', color: 'var(--text-3)', marginTop: '8px', fontFamily: 'var(--font-mono)' }}>
|
||||||
|
{completedCount} of {setupItems.length} complete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Program bar */}
|
{/* Program bar */}
|
||||||
{status.program_name && (
|
{status.program_name && (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,22 @@ export default function Settings() {
|
||||||
</div>
|
</div>
|
||||||
<span style={{ color: 'var(--text-3)', fontSize: '1.1rem' }}>›</span>
|
<span style={{ color: 'var(--text-3)', fontSize: '1.1rem' }}>›</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => navigate('/agent')}
|
||||||
|
style={{
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
|
padding: '14px 0', cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<span style={{ fontSize: '1.2rem' }}>🤖</span>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 700, fontSize: '0.95rem' }}>Connect AI Agent</div>
|
||||||
|
<div style={{ fontSize: '0.78rem', color: 'var(--text-3)', fontWeight: 500 }}>Claude Desktop, Cursor, Claude Code setup</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span style={{ color: 'var(--text-3)', fontSize: '1.1rem' }}>›</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Point Rules */}
|
{/* Point Rules */}
|
||||||
|
|
@ -192,7 +208,7 @@ export default function Settings() {
|
||||||
onClick={() => navigate('/settings/integrations')}
|
onClick={() => navigate('/settings/integrations')}
|
||||||
style={{ padding: '12px 0', textAlign: 'center', cursor: 'pointer', color: 'var(--accent)', fontSize: '0.88rem', fontWeight: 600 }}
|
style={{ padding: '12px 0', textAlign: 'center', cursor: 'pointer', color: 'var(--accent)', fontSize: '0.88rem', fontWeight: 600 }}
|
||||||
>
|
>
|
||||||
Configure all 11 providers →
|
All integrations →
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
|
|
||||||
const STATUS_COLORS = {
|
const STATUS_COLORS = {
|
||||||
|
|
@ -13,16 +14,51 @@ const STATUS_LABELS = {
|
||||||
not_configured: 'Not configured',
|
not_configured: 'Not configured',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CATEGORY_ORDER = ['device', 'ai_provider', 'nutrition', 'notifications']
|
||||||
|
|
||||||
|
const CATEGORY_INFO = {
|
||||||
|
device: {
|
||||||
|
title: 'Fitness Devices',
|
||||||
|
description: 'Sync workouts automatically from your watch or tracker.',
|
||||||
|
},
|
||||||
|
ai_provider: {
|
||||||
|
title: 'AI Coaching',
|
||||||
|
description: 'Connect an LLM to power the built-in AI coach in the Coach tab. One provider is enough.',
|
||||||
|
},
|
||||||
|
nutrition: {
|
||||||
|
title: 'Nutrition',
|
||||||
|
description: 'Food databases for accurate macro tracking.',
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
title: 'Notifications',
|
||||||
|
description: 'Get alerts when things happen.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const SYNC_BADGE = {
|
||||||
|
coming_soon: { label: 'Coming soon', color: 'var(--text-3)' },
|
||||||
|
mcp_bridge: { label: 'MCP bridge', color: 'var(--accent)' },
|
||||||
|
}
|
||||||
|
|
||||||
export default function SettingsIntegrations() {
|
export default function SettingsIntegrations() {
|
||||||
const [providers, setProviders] = useState({})
|
const [providers, setProviders] = useState({})
|
||||||
const [status, setStatus] = useState({})
|
const [status, setStatus] = useState({})
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [configuring, setConfiguring] = useState(null) // provider key being configured
|
const [configuring, setConfiguring] = useState(null)
|
||||||
const [formData, setFormData] = useState({})
|
const [formData, setFormData] = useState({})
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
useEffect(() => { loadAll() }, [])
|
useEffect(() => { loadAll() }, [])
|
||||||
|
|
||||||
|
// Handle hash scrolling (e.g. /settings/integrations#ai-coaching)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading && location.hash) {
|
||||||
|
const el = document.getElementById(location.hash.slice(1))
|
||||||
|
if (el) setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 100)
|
||||||
|
}
|
||||||
|
}, [loading, location.hash])
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
try {
|
try {
|
||||||
const [p, s] = await Promise.all([api.listProviders(), api.fullIntegrationStatus()])
|
const [p, s] = await Promise.all([api.listProviders(), api.fullIntegrationStatus()])
|
||||||
|
|
@ -50,20 +86,70 @@ export default function SettingsIntegrations() {
|
||||||
|
|
||||||
if (loading) return <div className="page"><div className="loading">Loading...</div></div>
|
if (loading) return <div className="page"><div className="loading">Loading...</div></div>
|
||||||
|
|
||||||
|
// Group providers by category
|
||||||
|
const grouped = {}
|
||||||
|
for (const [key, info] of Object.entries(providers)) {
|
||||||
|
const cat = info.category || 'other'
|
||||||
|
if (!grouped[cat]) grouped[cat] = []
|
||||||
|
grouped[cat].push({ key, ...info })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page" style={{ maxWidth: 700, margin: '0 auto' }}>
|
<div className="page" style={{ maxWidth: 700, margin: '0 auto' }}>
|
||||||
<h1 style={{ fontSize: '1.4rem', fontWeight: 700, marginBottom: 24 }}>Integrations</h1>
|
<h1 style={{ fontSize: '1.4rem', fontWeight: 700, marginBottom: 8 }}>Integrations</h1>
|
||||||
<p style={{ color: 'var(--text-2)', marginBottom: 24, fontSize: '0.9rem' }}>
|
<p style={{ color: 'var(--text-2)', marginBottom: 24, fontSize: '0.9rem' }}>
|
||||||
Configure your fitness devices and services. Credentials are encrypted and stored locally — never sent to any external server.
|
Configure your fitness devices and services. Credentials are encrypted and stored locally — never sent to any external server.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{Object.entries(providers).map(([key, info]) => (
|
{CATEGORY_ORDER.map(cat => {
|
||||||
|
const items = grouped[cat]
|
||||||
|
if (!items || items.length === 0) return null
|
||||||
|
const catInfo = CATEGORY_INFO[cat] || { title: cat, description: '' }
|
||||||
|
const sectionId = catInfo.title.toLowerCase().replace(/\s+/g, '-')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={cat} id={sectionId} style={{ marginBottom: 32 }}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '0.72rem', fontWeight: 700, textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.08em', color: 'var(--accent)',
|
||||||
|
fontFamily: 'var(--font-mono)', marginBottom: 4,
|
||||||
|
}}>
|
||||||
|
{catInfo.title}
|
||||||
|
</div>
|
||||||
|
<p style={{ color: 'var(--text-3)', fontSize: '0.82rem', marginBottom: 12 }}>
|
||||||
|
{catInfo.description}
|
||||||
|
{cat === 'ai_provider' && (
|
||||||
|
<span> <a href="/agent" style={{ color: 'var(--accent)', textDecoration: 'none', fontWeight: 600 }}>
|
||||||
|
Or connect Claude Desktop / Cursor via MCP →
|
||||||
|
</a></span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{items.map(info => {
|
||||||
|
const { key } = info
|
||||||
|
const badge = SYNC_BADGE[info.sync_status]
|
||||||
|
const isMcpBridge = info.sync_status === 'mcp_bridge'
|
||||||
|
const isComingSoon = info.sync_status === 'coming_soon'
|
||||||
|
|
||||||
|
return (
|
||||||
<div key={key} style={{
|
<div key={key} style={{
|
||||||
background: 'var(--surface-2)', borderRadius: 'var(--r-md)', padding: 20, marginBottom: 12,
|
background: 'var(--card)', borderRadius: 'var(--r)', padding: 20, marginBottom: 10,
|
||||||
border: status[key] === 'connected' ? '2px solid var(--accent)' : '1px solid var(--border)',
|
border: status[key] === 'connected' ? '2px solid var(--accent)' : '1px solid var(--card-border)',
|
||||||
|
opacity: isComingSoon ? 0.7 : 1,
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<strong style={{ fontSize: '1rem' }}>{info.name}</strong>
|
<strong style={{ fontSize: '1rem' }}>{info.name}</strong>
|
||||||
|
{badge && (
|
||||||
|
<span style={{
|
||||||
|
fontSize: '0.65rem', padding: '2px 8px', borderRadius: 20,
|
||||||
|
fontWeight: 600, fontFamily: 'var(--font-mono)',
|
||||||
|
background: badge.color + '18', color: badge.color,
|
||||||
|
}}>
|
||||||
|
{badge.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<span style={{
|
<span style={{
|
||||||
fontSize: '0.75rem', padding: '3px 10px', borderRadius: 20, fontWeight: 600,
|
fontSize: '0.75rem', padding: '3px 10px', borderRadius: 20, fontWeight: 600,
|
||||||
background: STATUS_COLORS[status[key] || 'not_configured'] + '22',
|
background: STATUS_COLORS[status[key] || 'not_configured'] + '22',
|
||||||
|
|
@ -73,19 +159,29 @@ export default function SettingsIntegrations() {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style={{ color: 'var(--text-2)', fontSize: '0.85rem', margin: '0 0 12px' }}>
|
<p style={{ color: 'var(--text-2)', fontSize: '0.85rem', margin: '0 0 8px' }}>
|
||||||
{info.help_text}
|
{info.help_text}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{info.warning && (
|
||||||
|
<p style={{
|
||||||
|
fontSize: '0.78rem', color: 'var(--amber)', margin: '0 0 8px',
|
||||||
|
padding: '6px 10px', background: 'var(--amber-bg)', borderRadius: 'var(--r-sm)',
|
||||||
|
}}>
|
||||||
|
⚠ {info.warning}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{info.help_url && (
|
{info.help_url && (
|
||||||
<a href={info.help_url} target="_blank" rel="noopener noreferrer"
|
<a href={info.help_url} target="_blank" rel="noopener noreferrer"
|
||||||
style={{ fontSize: '0.8rem', color: 'var(--accent)' }}>
|
style={{ fontSize: '0.8rem', color: 'var(--accent)' }}>
|
||||||
Developer portal →
|
{isMcpBridge ? 'MCP setup docs →' : 'Developer portal →'}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{configuring === key ? (
|
{/* Configuration form — skip for mcp_bridge and coming_soon */}
|
||||||
<div style={{ marginTop: 12, padding: 16, background: 'var(--surface)', borderRadius: 'var(--r-sm)' }}>
|
{!isMcpBridge && !isComingSoon && configuring === key ? (
|
||||||
|
<div style={{ marginTop: 12, padding: 16, background: 'var(--bg)', borderRadius: 'var(--r-sm)' }}>
|
||||||
{info.fields.map(field => (
|
{info.fields.map(field => (
|
||||||
<div key={field} style={{ marginBottom: 10 }}>
|
<div key={field} style={{ marginBottom: 10 }}>
|
||||||
<label style={{ fontSize: '0.8rem', fontWeight: 600, display: 'block', marginBottom: 4 }}>
|
<label style={{ fontSize: '0.8rem', fontWeight: 600, display: 'block', marginBottom: 4 }}>
|
||||||
|
|
@ -97,7 +193,7 @@ export default function SettingsIntegrations() {
|
||||||
onChange={e => setFormData({ ...formData, [field]: e.target.value })}
|
onChange={e => setFormData({ ...formData, [field]: e.target.value })}
|
||||||
style={{
|
style={{
|
||||||
width: '100%', padding: '8px 12px', borderRadius: 'var(--r-sm)',
|
width: '100%', padding: '8px 12px', borderRadius: 'var(--r-sm)',
|
||||||
border: '1px solid var(--border)', fontSize: '0.9rem', boxSizing: 'border-box',
|
border: '1px solid var(--card-border)', fontSize: '0.9rem', boxSizing: 'border-box',
|
||||||
}}
|
}}
|
||||||
placeholder={`Enter ${field.replace(/_/g, ' ')}`}
|
placeholder={`Enter ${field.replace(/_/g, ' ')}`}
|
||||||
/>
|
/>
|
||||||
|
|
@ -106,32 +202,36 @@ export default function SettingsIntegrations() {
|
||||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||||
<button onClick={() => handleSave(key)} disabled={saving}
|
<button onClick={() => handleSave(key)} disabled={saving}
|
||||||
style={{
|
style={{
|
||||||
background: 'var(--accent)', color: '#fff', border: 'none',
|
background: 'var(--accent)', color: 'var(--text-inv)', border: 'none',
|
||||||
padding: '8px 20px', borderRadius: 'var(--r-sm)', cursor: 'pointer', fontWeight: 600,
|
padding: '8px 20px', borderRadius: 'var(--r-sm)', cursor: 'pointer', fontWeight: 600,
|
||||||
}}>
|
}}>
|
||||||
{saving ? 'Saving...' : 'Save'}
|
{saving ? 'Saving...' : 'Save'}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setConfiguring(null)}
|
<button onClick={() => setConfiguring(null)}
|
||||||
style={{
|
style={{
|
||||||
background: 'transparent', color: 'var(--text-2)', border: '1px solid var(--border)',
|
background: 'transparent', color: 'var(--text-2)', border: '1px solid var(--card-border)',
|
||||||
padding: '8px 20px', borderRadius: 'var(--r-sm)', cursor: 'pointer',
|
padding: '8px 20px', borderRadius: 'var(--r-sm)', cursor: 'pointer',
|
||||||
}}>
|
}}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : !isMcpBridge && !isComingSoon ? (
|
||||||
<button onClick={() => startConfigure(key)}
|
<button onClick={() => startConfigure(key)}
|
||||||
style={{
|
style={{
|
||||||
marginTop: 8, background: 'transparent', color: 'var(--accent)',
|
marginTop: 8, background: 'transparent', color: 'var(--accent)',
|
||||||
border: '1px solid var(--accent)', padding: '6px 16px', borderRadius: 'var(--r-sm)',
|
border: '1px solid var(--accent)', padding: '6px 16px', borderRadius: 'var(--r-sm)',
|
||||||
cursor: 'pointer', fontSize: '0.85rem', fontWeight: 600,
|
cursor: 'pointer', fontSize: '0.85rem', fontWeight: 600,
|
||||||
}}>
|
}}>
|
||||||
{status[key] === 'not_configured' ? 'Configure' : 'Reconfigure'}
|
{status[key] === 'not_configured' || !status[key] ? 'Configure' : 'Reconfigure'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue