feat: add React frontend with auth, project list, import, and project detail pages

Converts packages/web from vanilla TypeScript Vite scaffold to React with:
- React 19, react-router-dom v7, @tanstack/react-query v5, Tailwind CSS v4
- JWT auth context with auto-refresh token support
- Login/Register pages, protected Layout with auth guard
- Projects list with grid cards and delete action
- ImportDialog supporting URL or file upload with API key display
- ProjectDetail with 4 tabs: Documentation, Modules, MCP Integration, Settings
- All TypeScript compiles cleanly (noEmit check passes)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 13:36:45 +08:00
parent ac60f0bb49
commit c3f8b598af
26 changed files with 1143 additions and 389 deletions

View File

@@ -0,0 +1,74 @@
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { apiFetch } from '../../lib/api';
type Project = { id: string; name: string };
export default function McpIntegration({ project }: { project: Project }) {
const [apiKey, setApiKey] = useState<string | null>(null);
const mcpBaseUrl = window.location.origin;
const mcpUrl = `${mcpBaseUrl}/mcp/${project.id}`;
const rotateMutation = useMutation({
mutationFn: () => apiFetch<{ apiKey: string }>(`/projects/${project.id}/api-key/rotate`, { method: 'POST' }),
onSuccess: (data) => setApiKey(data.apiKey),
});
const configSnippet = JSON.stringify({
mcpServers: {
[project.name.toLowerCase().replace(/\s+/g, '-')]: {
url: mcpUrl,
headers: { Authorization: `Bearer ${apiKey || '<your-api-key>'}` },
},
},
}, null, 2);
return (
<div className="space-y-6 max-w-2xl">
<div>
<h3 className="font-medium mb-2">MCP Service URL</h3>
<div className="flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-gray-100 rounded text-sm font-mono">{mcpUrl}</code>
<button onClick={() => navigator.clipboard.writeText(mcpUrl)} className="px-3 py-2 text-sm bg-gray-200 rounded hover:bg-gray-300">Copy</button>
</div>
</div>
<div>
<h3 className="font-medium mb-2">API Key</h3>
{apiKey ? (
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded">
<p className="text-xs text-yellow-700 mb-1">Save this key it won't be shown again.</p>
<code className="text-sm break-all">{apiKey}</code>
</div>
) : (
<p className="text-sm text-gray-500">API key is hidden. Rotate to generate a new one.</p>
)}
<button onClick={() => { if (confirm('This will invalidate the current API key. Continue?')) rotateMutation.mutate(); }}
className="mt-2 px-3 py-1 text-sm bg-orange-100 text-orange-700 rounded hover:bg-orange-200">Rotate API Key</button>
</div>
<div>
<h3 className="font-medium mb-2">Configuration for Claude Code / Cursor</h3>
<div className="relative">
<pre className="bg-gray-900 text-green-400 p-4 rounded text-sm overflow-auto">{configSnippet}</pre>
<button onClick={() => navigator.clipboard.writeText(configSnippet)} className="absolute top-2 right-2 px-2 py-1 text-xs bg-gray-700 text-white rounded hover:bg-gray-600">Copy</button>
</div>
</div>
<div>
<h3 className="font-medium mb-2">Available Tools</h3>
<div className="space-y-2 text-sm">
{[
{ name: 'get_project_overview', desc: 'Get project name, version, base URL, and module summary. Call this first.' },
{ name: 'list_modules', desc: 'List all modules with descriptions and endpoint counts.' },
{ name: 'list_endpoints', desc: 'List endpoints in a module. Provide moduleId.' },
{ name: 'get_endpoint_detail', desc: 'Get full endpoint details: parameters, request body, responses.' },
{ name: 'search_endpoints', desc: 'Search by keyword across all endpoints. Optional moduleId filter.' },
].map((t) => (
<div key={t.name} className="p-3 bg-gray-50 rounded">
<code className="font-medium">{t.name}</code>
<p className="text-gray-500 mt-1">{t.desc}</p>
</div>
))}
</div>
</div>
</div>
);
}