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" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<title>Fitness Rewards</title>
|
||||
<script type="module" crossorigin src="/assets/index-DIoo6DtB.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CRHpiAro.css">
|
||||
<script type="module" crossorigin src="/assets/index-De1TwOuS.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-ciTnvXdZ.css">
|
||||
</head>
|
||||
<body>
|
||||
<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.meal_plans import router as meal_plans_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(onboarding_router)
|
||||
|
|
@ -112,6 +113,7 @@ app.include_router(programs_router)
|
|||
app.include_router(nutrition_router)
|
||||
app.include_router(meal_plans_router)
|
||||
app.include_router(ai_chat_router)
|
||||
app.include_router(agent_router)
|
||||
|
||||
|
||||
@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 SupportAdmin from './pages/SupportAdmin'
|
||||
import ChatCoach from './pages/ChatCoach'
|
||||
import AgentConnect from './pages/AgentConnect'
|
||||
|
||||
function ProtectedRoute({ children }) {
|
||||
if (!hasToken()) return <Navigate to="/login" />
|
||||
|
|
@ -95,6 +96,9 @@ function NavBar() {
|
|||
<NavLink to="/chat" className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}>
|
||||
<span className="nav-icon">🧠</span> Coach
|
||||
</NavLink>
|
||||
<NavLink to="/agent" className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}>
|
||||
<span className="nav-icon">🔌</span> Agent
|
||||
</NavLink>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
|
@ -123,6 +127,7 @@ export default function App() {
|
|||
<Route path="/settings/integrations" element={<ProtectedRoute><SettingsIntegrations /></ProtectedRoute>} />
|
||||
<Route path="/meal-plan" element={<ProtectedRoute><MealPlan /></ProtectedRoute>} />
|
||||
<Route path="/chat" element={<ProtectedRoute><ChatCoach /></ProtectedRoute>} />
|
||||
<Route path="/agent" element={<ProtectedRoute><AgentConnect /></ProtectedRoute>} />
|
||||
</Routes>
|
||||
{hasToken() && <NavBar />}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -145,6 +145,9 @@ export const api = {
|
|||
|
||||
// Resources
|
||||
getResourceRecommendations: () => request('/onboarding/recommendations'),
|
||||
|
||||
// Agent
|
||||
agentConfig: () => request('/agent/config'),
|
||||
};
|
||||
|
||||
export function setToken(token) {
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ input::placeholder, textarea::placeholder { color: var(--text-3); font-weight: 4
|
|||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
padding: 6px 14px;
|
||||
padding: 6px 10px;
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
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