feat: Agent Connect page + /api/agent/config endpoint
- New AgentConnect.jsx page with MCP URL, token, setup instructions - Claude Desktop config snippet with copy button - Claude Code CLI command with copy button - Agent capabilities overview - Added to bottom nav bar (6th item, plug icon) - Backend /api/agent/config endpoint returns deployment-aware MCP details - Nav padding adjusted for 6 items - Frontend rebuilt with new page
This commit is contained in:
parent
a6e6e2f54f
commit
df39a8dc08
10 changed files with 325 additions and 72 deletions
File diff suppressed because one or more lines are too long
68
diligence/frontend/assets/index-De1TwOuS.js
Normal file
68
diligence/frontend/assets/index-De1TwOuS.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -7,8 +7,8 @@
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<title>Fitness Rewards</title>
|
<title>Fitness Rewards</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DIoo6DtB.js"></script>
|
<script type="module" crossorigin src="/assets/index-De1TwOuS.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CRHpiAro.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-ciTnvXdZ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ from diligence.routers.support import router as support_router
|
||||||
from diligence.routers.nutrition import router as nutrition_router
|
from diligence.routers.nutrition import router as nutrition_router
|
||||||
from diligence.routers.meal_plans import router as meal_plans_router
|
from diligence.routers.meal_plans import router as meal_plans_router
|
||||||
from diligence.routers.ai_chat import router as ai_chat_router
|
from diligence.routers.ai_chat import router as ai_chat_router
|
||||||
|
from diligence.routers.agent import router as agent_router
|
||||||
|
|
||||||
app.include_router(auth_router)
|
app.include_router(auth_router)
|
||||||
app.include_router(onboarding_router)
|
app.include_router(onboarding_router)
|
||||||
|
|
@ -112,6 +113,7 @@ app.include_router(programs_router)
|
||||||
app.include_router(nutrition_router)
|
app.include_router(nutrition_router)
|
||||||
app.include_router(meal_plans_router)
|
app.include_router(meal_plans_router)
|
||||||
app.include_router(ai_chat_router)
|
app.include_router(ai_chat_router)
|
||||||
|
app.include_router(agent_router)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/health")
|
@app.get("/api/health")
|
||||||
|
|
|
||||||
35
diligence/routers/agent.py
Normal file
35
diligence/routers/agent.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""Agent connection endpoint — returns MCP config for AI agent setup."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from diligence.config import get_settings
|
||||||
|
from diligence.utils.auth import get_current_user
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/agent", tags=["agent"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/config")
|
||||||
|
async def agent_config(user=Depends(get_current_user)):
|
||||||
|
"""Return MCP connection details for the authenticated user."""
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
# Determine MCP URL based on deployment mode
|
||||||
|
base = settings.base_url.rstrip("/")
|
||||||
|
if settings.is_sqlite:
|
||||||
|
# pip install path — MCP served from same process
|
||||||
|
mcp_url = f"{base}/mcp/sse"
|
||||||
|
else:
|
||||||
|
# Docker path — MCP on separate container port 3001
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
parsed = urlparse(base)
|
||||||
|
docker_host = parsed.hostname or "localhost"
|
||||||
|
mcp_url = f"http://{docker_host}:3001/sse"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"mcp_url": mcp_url,
|
||||||
|
"api_token": settings.api_token if getattr(user, "is_admin", False) else None,
|
||||||
|
"api_token_set": bool(settings.api_token),
|
||||||
|
"deployment": "local" if settings.is_sqlite else "docker",
|
||||||
|
"base_url": base,
|
||||||
|
"tools_count": 14,
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ import CatalogDetail from './pages/CatalogDetail'
|
||||||
import Support from './pages/Support'
|
import Support from './pages/Support'
|
||||||
import SupportAdmin from './pages/SupportAdmin'
|
import SupportAdmin from './pages/SupportAdmin'
|
||||||
import ChatCoach from './pages/ChatCoach'
|
import ChatCoach from './pages/ChatCoach'
|
||||||
|
import AgentConnect from './pages/AgentConnect'
|
||||||
|
|
||||||
function ProtectedRoute({ children }) {
|
function ProtectedRoute({ children }) {
|
||||||
if (!hasToken()) return <Navigate to="/login" />
|
if (!hasToken()) return <Navigate to="/login" />
|
||||||
|
|
@ -95,6 +96,9 @@ function NavBar() {
|
||||||
<NavLink to="/chat" className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}>
|
<NavLink to="/chat" className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}>
|
||||||
<span className="nav-icon">🧠</span> Coach
|
<span className="nav-icon">🧠</span> Coach
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink to="/agent" className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}>
|
||||||
|
<span className="nav-icon">🔌</span> Agent
|
||||||
|
</NavLink>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -123,6 +127,7 @@ export default function App() {
|
||||||
<Route path="/settings/integrations" element={<ProtectedRoute><SettingsIntegrations /></ProtectedRoute>} />
|
<Route path="/settings/integrations" element={<ProtectedRoute><SettingsIntegrations /></ProtectedRoute>} />
|
||||||
<Route path="/meal-plan" element={<ProtectedRoute><MealPlan /></ProtectedRoute>} />
|
<Route path="/meal-plan" element={<ProtectedRoute><MealPlan /></ProtectedRoute>} />
|
||||||
<Route path="/chat" element={<ProtectedRoute><ChatCoach /></ProtectedRoute>} />
|
<Route path="/chat" element={<ProtectedRoute><ChatCoach /></ProtectedRoute>} />
|
||||||
|
<Route path="/agent" element={<ProtectedRoute><AgentConnect /></ProtectedRoute>} />
|
||||||
</Routes>
|
</Routes>
|
||||||
{hasToken() && <NavBar />}
|
{hasToken() && <NavBar />}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,9 @@ export const api = {
|
||||||
|
|
||||||
// Resources
|
// Resources
|
||||||
getResourceRecommendations: () => request('/onboarding/recommendations'),
|
getResourceRecommendations: () => request('/onboarding/recommendations'),
|
||||||
|
|
||||||
|
// Agent
|
||||||
|
agentConfig: () => request('/agent/config'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setToken(token) {
|
export function setToken(token) {
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ input::placeholder, textarea::placeholder { color: var(--text-3); font-weight: 4
|
||||||
font-size: 0.62rem;
|
font-size: 0.62rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
padding: 6px 14px;
|
padding: 6px 10px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.15s;
|
transition: color 0.15s;
|
||||||
border-radius: var(--r-sm);
|
border-radius: var(--r-sm);
|
||||||
|
|
|
||||||
208
frontend/src/pages/AgentConnect.jsx
Normal file
208
frontend/src/pages/AgentConnect.jsx
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { api } from '../api'
|
||||||
|
|
||||||
|
function CopyButton({ text, label }) {
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
} catch { /* fallback for non-HTTPS */ }
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="btn-outline btn-sm"
|
||||||
|
style={{ fontSize: '0.75rem', padding: '6px 12px', whiteSpace: 'nowrap' }}
|
||||||
|
>
|
||||||
|
{copied ? 'Copied' : (label || 'Copy')}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CodeBlock({ children, copyText }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
background: 'var(--bg)', border: '1px solid var(--divider)',
|
||||||
|
borderRadius: 'var(--r-sm)', padding: '14px 16px',
|
||||||
|
fontFamily: 'var(--font-mono)', fontSize: '0.78rem', lineHeight: 1.6,
|
||||||
|
overflowX: 'auto', position: 'relative', whiteSpace: 'pre',
|
||||||
|
}}>
|
||||||
|
{copyText && (
|
||||||
|
<div style={{ position: 'absolute', top: '8px', right: '8px' }}>
|
||||||
|
<CopyButton text={copyText} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AgentConnect() {
|
||||||
|
const [config, setConfig] = useState(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [showToken, setShowToken] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.agentConfig()
|
||||||
|
.then(setConfig)
|
||||||
|
.catch(console.error)
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading) return <div className="page"><div className="loading">Loading...</div></div>
|
||||||
|
if (!config) return <div className="page"><div className="error-msg">Failed to load agent config</div></div>
|
||||||
|
|
||||||
|
const maskedToken = config.api_token
|
||||||
|
? config.api_token.slice(0, 8) + '\u2026' + config.api_token.slice(-4)
|
||||||
|
: null
|
||||||
|
|
||||||
|
const claudeDesktopConfig = JSON.stringify({
|
||||||
|
"mcpServers": {
|
||||||
|
"diligence": {
|
||||||
|
"url": config.mcp_url,
|
||||||
|
...(config.api_token ? { "headers": { "Authorization": `Bearer ${config.api_token}` } } : {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, null, 2)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<h1 style={{
|
||||||
|
fontFamily: 'var(--font-display)', fontWeight: 800,
|
||||||
|
fontSize: '1.3rem', marginBottom: '4px',
|
||||||
|
}}>
|
||||||
|
Connect Your AI Agent
|
||||||
|
</h1>
|
||||||
|
<p style={{ color: 'var(--text-3)', fontSize: '0.85rem' }}>
|
||||||
|
Use any MCP-compatible AI to log workouts, track food, and manage your fitness.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Connection Details */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="section-label">Connection</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '14px' }}>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: 'var(--text-3)', fontWeight: 600, marginBottom: '4px' }}>
|
||||||
|
MCP Endpoint
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: '8px',
|
||||||
|
background: 'var(--bg)', borderRadius: 'var(--r-sm)',
|
||||||
|
padding: '10px 12px', border: '1px solid var(--divider)',
|
||||||
|
}}>
|
||||||
|
<code style={{
|
||||||
|
flex: 1, fontFamily: 'var(--font-mono)', fontSize: '0.82rem',
|
||||||
|
color: 'var(--accent)', wordBreak: 'break-all',
|
||||||
|
}}>
|
||||||
|
{config.mcp_url}
|
||||||
|
</code>
|
||||||
|
<CopyButton text={config.mcp_url} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{config.api_token && (
|
||||||
|
<div style={{ marginBottom: '14px' }}>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: 'var(--text-3)', fontWeight: 600, marginBottom: '4px' }}>
|
||||||
|
API Token
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: '8px',
|
||||||
|
background: 'var(--bg)', borderRadius: 'var(--r-sm)',
|
||||||
|
padding: '10px 12px', border: '1px solid var(--divider)',
|
||||||
|
}}>
|
||||||
|
<code
|
||||||
|
onClick={() => setShowToken(!showToken)}
|
||||||
|
style={{
|
||||||
|
flex: 1, fontFamily: 'var(--font-mono)', fontSize: '0.82rem',
|
||||||
|
cursor: 'pointer', wordBreak: 'break-all',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showToken ? config.api_token : maskedToken}
|
||||||
|
</code>
|
||||||
|
<CopyButton text={config.api_token} />
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.7rem', color: 'var(--text-3)', marginTop: '4px' }}>
|
||||||
|
Tap token to reveal. Only visible to admins.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!config.api_token && config.api_token_set && (
|
||||||
|
<div style={{
|
||||||
|
fontSize: '0.8rem', color: 'var(--text-3)', fontStyle: 'italic',
|
||||||
|
padding: '8px 0',
|
||||||
|
}}>
|
||||||
|
API token is set but only visible to admin users.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', gap: '12px', flexWrap: 'wrap',
|
||||||
|
fontSize: '0.78rem', color: 'var(--text-3)',
|
||||||
|
}}>
|
||||||
|
<span>{config.tools_count} tools available</span>
|
||||||
|
<span style={{ color: 'var(--divider)' }}>|</span>
|
||||||
|
<span>{config.deployment === 'local' ? 'Local (SQLite)' : 'Docker (PostgreSQL)'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Claude Desktop Setup */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="section-label">Claude Desktop</div>
|
||||||
|
<p style={{ fontSize: '0.82rem', color: 'var(--text-2)', marginBottom: '12px' }}>
|
||||||
|
Add this to your Claude Desktop MCP config:
|
||||||
|
</p>
|
||||||
|
<CodeBlock copyText={claudeDesktopConfig}>
|
||||||
|
{claudeDesktopConfig}
|
||||||
|
</CodeBlock>
|
||||||
|
<p style={{ fontSize: '0.75rem', color: 'var(--text-3)', marginTop: '10px' }}>
|
||||||
|
On macOS: <code style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem' }}>
|
||||||
|
~/Library/Application Support/Claude/claude_desktop_config.json
|
||||||
|
</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Claude Code Setup */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="section-label">Claude Code</div>
|
||||||
|
<p style={{ fontSize: '0.82rem', color: 'var(--text-2)', marginBottom: '12px' }}>
|
||||||
|
Add the MCP server from your terminal:
|
||||||
|
</p>
|
||||||
|
<CodeBlock copyText={`claude mcp add diligence --transport sse ${config.mcp_url}`}>
|
||||||
|
{`claude mcp add diligence --transport sse ${config.mcp_url}`}
|
||||||
|
</CodeBlock>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* What your agent can do */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="section-label">What Your Agent Can Do</div>
|
||||||
|
<div style={{ display: 'grid', gap: '8px' }}>
|
||||||
|
{[
|
||||||
|
['Log workouts', 'Track any activity and earn points automatically'],
|
||||||
|
['Track food', 'Search 400K+ foods, log meals with full macros'],
|
||||||
|
['Manage meal plans', 'Create plans, track compliance, adjust portions'],
|
||||||
|
['Check progress', 'Daily points, weekly summary, program status'],
|
||||||
|
['Redeem rewards', 'Spend earned points on rewards you set up'],
|
||||||
|
].map(([title, desc]) => (
|
||||||
|
<div key={title} style={{
|
||||||
|
display: 'flex', gap: '10px', alignItems: 'flex-start',
|
||||||
|
padding: '8px 0', borderBottom: '1px solid var(--divider)',
|
||||||
|
}}>
|
||||||
|
<span style={{ color: 'var(--green)', fontSize: '0.9rem', marginTop: '1px' }}>
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 700, fontSize: '0.85rem' }}>{title}</div>
|
||||||
|
<div style={{ fontSize: '0.78rem', color: 'var(--text-3)' }}>{desc}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue