diff --git a/diligence/routers/integrations.py b/diligence/routers/integrations.py index 5b705a2..b25b904 100644 --- a/diligence/routers/integrations.py +++ b/diligence/routers/integrations.py @@ -235,12 +235,23 @@ async def list_providers(): """Return the provider registry with setup instructions.""" result = {} 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] = { "name": info["name"], "type": info["type"], + "category": info.get("category", "other"), "fields": info["fields"], "help_url": info.get("help_url", ""), "help_text": info.get("help_text", "").replace("{BASE_URL}", settings.base_url), + "warning": info.get("warning", ""), + "sync_status": sync_status, } return result diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8f2f68e..f940858 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -107,6 +107,18 @@ export default function App() { return ( <> {hasToken() && } + {hasToken() && ( + + ⚙️ + + )} } /> } /> diff --git a/frontend/src/pages/AgentConnect.jsx b/frontend/src/pages/AgentConnect.jsx index d6de371..83ef206 100644 --- a/frontend/src/pages/AgentConnect.jsx +++ b/frontend/src/pages/AgentConnect.jsx @@ -69,6 +69,12 @@ export default function AgentConnect() { return (
+ + ← Back to Integrations +

AI Coach

🤖
-

No AI provider connected

-

- Connect OpenAI, OpenRouter, Claude, Ollama, or any other provider to start chatting with your AI fitness coach. +

Set up your AI coach

+

+ Connect an AI provider to chat with your fitness coach right here. OpenRouter, Groq, and Hugging Face all have free tiers.

- - Configure AI Provider + Set up AI Provider +

+ Advanced: + connect Claude Desktop, Cursor, or Claude Code via MCP + +

) diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 6227b83..e3b3f63 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -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 (
+ {/* Getting started checklist */} + {showChecklist && !allDone && ( +
+
+
+ Getting Started +
+ +
+ {setupItems.map((item, i) => ( +
item.link && !item.done && navigate(item.link)} + style={{ + display: 'flex', alignItems: 'center', gap: '10px', + padding: '7px 0', + cursor: item.link && !item.done ? 'pointer' : 'default', + }} + > + + {item.done ? '✓' : ''} + + + {item.label} + + {item.link && !item.done && ( + + )} +
+ ))} +
+ {completedCount} of {setupItems.length} complete +
+
+ )} + {/* Program bar */} {status.program_name && (
+
navigate('/agent')} + style={{ + display: 'flex', alignItems: 'center', justifyContent: 'space-between', + padding: '14px 0', cursor: 'pointer', + }} + > +
+ 🤖 +
+
Connect AI Agent
+
Claude Desktop, Cursor, Claude Code setup
+
+
+ +
{/* 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 →
diff --git a/frontend/src/pages/SettingsIntegrations.jsx b/frontend/src/pages/SettingsIntegrations.jsx index 28b8f1d..fb10d66 100644 --- a/frontend/src/pages/SettingsIntegrations.jsx +++ b/frontend/src/pages/SettingsIntegrations.jsx @@ -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
Loading...
+ // 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 (
-

Integrations

+

Integrations

Configure your fitness devices and services. Credentials are encrypted and stored locally — never sent to any external server.

- {Object.entries(providers).map(([key, info]) => ( -
-
- {info.name} - { + 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 ( +
+
- {STATUS_LABELS[status[key] || 'not_configured']} - -
- -

- {info.help_text} -

- - {info.help_url && ( - - Developer portal → - - )} - - {configuring === key ? ( -
- {info.fields.map(field => ( -
- - 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, ' ')}`} - /> -
- ))} -
- - -
+ {catInfo.title}
- ) : ( - - )} -
- ))} +

+ {catInfo.description} + {cat === 'ai_provider' && ( + + Or connect Claude Desktop / Cursor via MCP → + + )} +

+ + {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 ( +
+
+
+ {info.name} + {badge && ( + + {badge.label} + + )} +
+ + {STATUS_LABELS[status[key] || 'not_configured']} + +
+ +

+ {info.help_text} +

+ + {info.warning && ( +

+ ⚠ {info.warning} +

+ )} + + {info.help_url && ( + + {isMcpBridge ? 'MCP setup docs →' : 'Developer portal →'} + + )} + + {/* Configuration form — skip for mcp_bridge and coming_soon */} + {!isMcpBridge && !isComingSoon && configuring === key ? ( +
+ {info.fields.map(field => ( +
+ + 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, ' ')}`} + /> +
+ ))} +
+ + +
+
+ ) : !isMcpBridge && !isComingSoon ? ( + + ) : null} +
+ ) + })} +
+ ) + })}
) }