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
|
|
@ -107,6 +107,18 @@ export default function App() {
|
|||
return (
|
||||
<>
|
||||
{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>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/onboarding" element={<ProtectedRoute><Onboarding /></ProtectedRoute>} />
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@ export default function AgentConnect() {
|
|||
|
||||
return (
|
||||
<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' }}>
|
||||
<h1 style={{
|
||||
fontFamily: 'var(--font-display)', fontWeight: 800,
|
||||
|
|
|
|||
|
|
@ -94,17 +94,22 @@ export default function ChatCoach() {
|
|||
<h1 className="page-title">AI Coach</h1>
|
||||
<div className="card" style={{ textAlign: 'center', padding: '32px 20px' }}>
|
||||
<div style={{ fontSize: '2rem', marginBottom: '12px' }}>🤖</div>
|
||||
<h3 style={{ marginBottom: '8px' }}>No AI provider connected</h3>
|
||||
<p style={{ color: 'var(--text-2)', fontSize: '0.9rem', marginBottom: '16px' }}>
|
||||
Connect OpenAI, OpenRouter, Claude, Ollama, or any other provider to start chatting with your AI fitness coach.
|
||||
<h3 style={{ marginBottom: '8px' }}>Set up your AI coach</h3>
|
||||
<p style={{ color: 'var(--text-2)', fontSize: '0.9rem', marginBottom: '20px' }}>
|
||||
Connect an AI provider to chat with your fitness coach right here. OpenRouter, Groq, and Hugging Face all have free tiers.
|
||||
</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)',
|
||||
background: 'var(--accent)', color: 'var(--text-inv)', textDecoration: 'none',
|
||||
fontWeight: 600, fontSize: '0.88rem',
|
||||
}}>
|
||||
Configure AI Provider
|
||||
Set up AI Provider
|
||||
</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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ function Tip({ text, children }) {
|
|||
export default function Dashboard() {
|
||||
const [status, setStatus] = useState(null)
|
||||
const [integrations, setIntegrations] = useState(null)
|
||||
const [aiStatus, setAiStatus] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [syncing, setSyncing] = useState(null)
|
||||
const navigate = useNavigate()
|
||||
|
|
@ -39,9 +40,10 @@ export default function Dashboard() {
|
|||
useEffect(() => { loadAll() }, [])
|
||||
async function loadAll() {
|
||||
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)
|
||||
setIntegrations(intg)
|
||||
setAiStatus(ai)
|
||||
} catch (err) { console.error(err) }
|
||||
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.' },
|
||||
]
|
||||
|
||||
// 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 (
|
||||
<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 */}
|
||||
{status.program_name && (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -127,6 +127,22 @@ export default function Settings() {
|
|||
</div>
|
||||
<span style={{ color: 'var(--text-3)', fontSize: '1.1rem' }}>›</span>
|
||||
</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>
|
||||
|
||||
{/* Point Rules */}
|
||||
|
|
@ -192,7 +208,7 @@ export default function Settings() {
|
|||
onClick={() => navigate('/settings/integrations')}
|
||||
style={{ padding: '12px 0', textAlign: 'center', cursor: 'pointer', color: 'var(--accent)', fontSize: '0.88rem', fontWeight: 600 }}
|
||||
>
|
||||
Configure all 11 providers →
|
||||
All integrations →
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { api } from '../api'
|
||||
|
||||
const STATUS_COLORS = {
|
||||
|
|
@ -13,16 +14,51 @@ const STATUS_LABELS = {
|
|||
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() {
|
||||
const [providers, setProviders] = useState({})
|
||||
const [status, setStatus] = useState({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [configuring, setConfiguring] = useState(null) // provider key being configured
|
||||
const [configuring, setConfiguring] = useState(null)
|
||||
const [formData, setFormData] = useState({})
|
||||
const [saving, setSaving] = useState(false)
|
||||
const location = useLocation()
|
||||
|
||||
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() {
|
||||
try {
|
||||
const [p, s] = await Promise.all([api.listProviders(), api.fullIntegrationStatus()])
|
||||
|
|
@ -50,88 +86,152 @@ export default function SettingsIntegrations() {
|
|||
|
||||
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 (
|
||||
<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' }}>
|
||||
Configure your fitness devices and services. Credentials are encrypted and stored locally — never sent to any external server.
|
||||
</p>
|
||||
|
||||
{Object.entries(providers).map(([key, info]) => (
|
||||
<div key={key} style={{
|
||||
background: 'var(--surface-2)', borderRadius: 'var(--r-md)', padding: 20, marginBottom: 12,
|
||||
border: status[key] === 'connected' ? '2px solid var(--accent)' : '1px solid var(--border)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<strong style={{ fontSize: '1rem' }}>{info.name}</strong>
|
||||
<span style={{
|
||||
fontSize: '0.75rem', padding: '3px 10px', borderRadius: 20, fontWeight: 600,
|
||||
background: STATUS_COLORS[status[key] || 'not_configured'] + '22',
|
||||
color: STATUS_COLORS[status[key] || 'not_configured'],
|
||||
{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,
|
||||
}}>
|
||||
{STATUS_LABELS[status[key] || 'not_configured']}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p style={{ color: 'var(--text-2)', fontSize: '0.85rem', margin: '0 0 12px' }}>
|
||||
{info.help_text}
|
||||
</p>
|
||||
|
||||
{info.help_url && (
|
||||
<a href={info.help_url} target="_blank" rel="noopener noreferrer"
|
||||
style={{ fontSize: '0.8rem', color: 'var(--accent)' }}>
|
||||
Developer portal →
|
||||
</a>
|
||||
)}
|
||||
|
||||
{configuring === key ? (
|
||||
<div style={{ marginTop: 12, padding: 16, background: 'var(--surface)', borderRadius: 'var(--r-sm)' }}>
|
||||
{info.fields.map(field => (
|
||||
<div key={field} style={{ marginBottom: 10 }}>
|
||||
<label style={{ fontSize: '0.8rem', fontWeight: 600, display: 'block', marginBottom: 4 }}>
|
||||
{field.replace(/_/g, ' ')}
|
||||
</label>
|
||||
<input
|
||||
type={field.includes('secret') || field.includes('token') || field.includes('key') ? 'password' : 'text'}
|
||||
value={formData[field] || ''}
|
||||
onChange={e => setFormData({ ...formData, [field]: e.target.value })}
|
||||
style={{
|
||||
width: '100%', padding: '8px 12px', borderRadius: 'var(--r-sm)',
|
||||
border: '1px solid var(--border)', fontSize: '0.9rem', boxSizing: 'border-box',
|
||||
}}
|
||||
placeholder={`Enter ${field.replace(/_/g, ' ')}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||
<button onClick={() => handleSave(key)} disabled={saving}
|
||||
style={{
|
||||
background: 'var(--accent)', color: '#fff', border: 'none',
|
||||
padding: '8px 20px', borderRadius: 'var(--r-sm)', cursor: 'pointer', fontWeight: 600,
|
||||
}}>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
<button onClick={() => setConfiguring(null)}
|
||||
style={{
|
||||
background: 'transparent', color: 'var(--text-2)', border: '1px solid var(--border)',
|
||||
padding: '8px 20px', borderRadius: 'var(--r-sm)', cursor: 'pointer',
|
||||
}}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
{catInfo.title}
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={() => startConfigure(key)}
|
||||
style={{
|
||||
marginTop: 8, background: 'transparent', color: 'var(--accent)',
|
||||
border: '1px solid var(--accent)', padding: '6px 16px', borderRadius: 'var(--r-sm)',
|
||||
cursor: 'pointer', fontSize: '0.85rem', fontWeight: 600,
|
||||
}}>
|
||||
{status[key] === 'not_configured' ? 'Configure' : 'Reconfigure'}
|
||||
</button>
|
||||
)}
|
||||
</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={{
|
||||
background: 'var(--card)', borderRadius: 'var(--r)', padding: 20, marginBottom: 10,
|
||||
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', alignItems: 'center', gap: 8 }}>
|
||||
<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={{
|
||||
fontSize: '0.75rem', padding: '3px 10px', borderRadius: 20, fontWeight: 600,
|
||||
background: STATUS_COLORS[status[key] || 'not_configured'] + '22',
|
||||
color: STATUS_COLORS[status[key] || 'not_configured'],
|
||||
}}>
|
||||
{STATUS_LABELS[status[key] || 'not_configured']}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p style={{ color: 'var(--text-2)', fontSize: '0.85rem', margin: '0 0 8px' }}>
|
||||
{info.help_text}
|
||||
</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 && (
|
||||
<a href={info.help_url} target="_blank" rel="noopener noreferrer"
|
||||
style={{ fontSize: '0.8rem', color: 'var(--accent)' }}>
|
||||
{isMcpBridge ? 'MCP setup docs →' : 'Developer portal →'}
|
||||
</a>
|
||||
)}
|
||||
|
||||
{/* Configuration form — skip for mcp_bridge and coming_soon */}
|
||||
{!isMcpBridge && !isComingSoon && configuring === key ? (
|
||||
<div style={{ marginTop: 12, padding: 16, background: 'var(--bg)', borderRadius: 'var(--r-sm)' }}>
|
||||
{info.fields.map(field => (
|
||||
<div key={field} style={{ marginBottom: 10 }}>
|
||||
<label style={{ fontSize: '0.8rem', fontWeight: 600, display: 'block', marginBottom: 4 }}>
|
||||
{field.replace(/_/g, ' ')}
|
||||
</label>
|
||||
<input
|
||||
type={field.includes('secret') || field.includes('token') || field.includes('key') ? 'password' : 'text'}
|
||||
value={formData[field] || ''}
|
||||
onChange={e => setFormData({ ...formData, [field]: e.target.value })}
|
||||
style={{
|
||||
width: '100%', padding: '8px 12px', borderRadius: 'var(--r-sm)',
|
||||
border: '1px solid var(--card-border)', fontSize: '0.9rem', boxSizing: 'border-box',
|
||||
}}
|
||||
placeholder={`Enter ${field.replace(/_/g, ' ')}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||
<button onClick={() => handleSave(key)} disabled={saving}
|
||||
style={{
|
||||
background: 'var(--accent)', color: 'var(--text-inv)', border: 'none',
|
||||
padding: '8px 20px', borderRadius: 'var(--r-sm)', cursor: 'pointer', fontWeight: 600,
|
||||
}}>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
<button onClick={() => setConfiguring(null)}
|
||||
style={{
|
||||
background: 'transparent', color: 'var(--text-2)', border: '1px solid var(--card-border)',
|
||||
padding: '8px 20px', borderRadius: 'var(--r-sm)', cursor: 'pointer',
|
||||
}}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : !isMcpBridge && !isComingSoon ? (
|
||||
<button onClick={() => startConfigure(key)}
|
||||
style={{
|
||||
marginTop: 8, background: 'transparent', color: 'var(--accent)',
|
||||
border: '1px solid var(--accent)', padding: '6px 16px', borderRadius: 'var(--r-sm)',
|
||||
cursor: 'pointer', fontSize: '0.85rem', fontWeight: 600,
|
||||
}}>
|
||||
{status[key] === 'not_configured' || !status[key] ? 'Configure' : 'Reconfigure'}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue