-
-
Notifications
You must be signed in to change notification settings - Fork 4
design landing,features , signin, and signup pages #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c30071b
a91b602
c578d06
d7f92d9
2db4631
a38bfcd
00305f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| "use client"; | ||
|
|
||
| import { useEffect, useState } from "react"; | ||
| import { useRouter } from "next/navigation"; | ||
| import { useSession } from "next-auth/react"; | ||
| import { Sidebar } from "@/components/sidebar"; | ||
| import { Card } from "@/components/ui/card"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { Input } from "@/components/ui/input"; | ||
| import { Label } from "@/components/ui/label"; | ||
| import { Bot, ServerCog, Loader2, CheckCircle2, XCircle, CircleDot } from "lucide-react"; | ||
|
|
||
| interface Agent { | ||
| id: string; | ||
| name: string; | ||
| status: "connected" | "disconnected" | "warning"; | ||
| } | ||
|
|
||
| interface McpConfig { | ||
| url: string; | ||
| apiKey: string; | ||
| } | ||
|
|
||
| export default function AiAgentsPage() { | ||
| const router = useRouter(); | ||
| const { data: session, status } = useSession(); | ||
| const [mounted, setMounted] = useState(false); | ||
|
|
||
| const [agents, setAgents] = useState<Agent[]>([]); | ||
| const [loadingAgents, setLoadingAgents] = useState(true); | ||
| const [savingAgentId, setSavingAgentId] = useState<string | null>(null); | ||
|
|
||
| const [config, setConfig] = useState<McpConfig>({ url: "", apiKey: "" }); | ||
| const [savingConfig, setSavingConfig] = useState(false); | ||
|
|
||
| const [recent, setRecent] = useState<string[]>([]); | ||
|
|
||
| useEffect(() => setMounted(true), []); | ||
|
|
||
| useEffect(() => { | ||
| if (status === "loading") return; | ||
| if (!session) router.push("/signin"); | ||
| }, [status, session, router]); | ||
|
|
||
| // Fetch dynamic data | ||
| useEffect(() => { | ||
| async function fetchData() { | ||
| try { | ||
| setLoadingAgents(true); | ||
| const [a, c] = await Promise.all([ | ||
| fetch("/api/ai/agents").then((r) => r.json()), | ||
| fetch("/api/ai/mcp-config").then((r) => r.json()), | ||
| ]); | ||
| setAgents(a.agents ?? []); | ||
| setConfig({ url: c.url ?? "", apiKey: c.apiKey ?? "" }); | ||
| setRecent(c.recent ?? []); | ||
| } catch (e) { | ||
| console.error(e); | ||
| } finally { | ||
| setLoadingAgents(false); | ||
| } | ||
| } | ||
| if (mounted) fetchData(); | ||
| }, [mounted]); | ||
|
|
||
| async function toggleAgent(agent: Agent) { | ||
| setSavingAgentId(agent.id); | ||
| try { | ||
| const res = await fetch("/api/ai/agents", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ id: agent.id, action: agent.status === "connected" ? "disconnect" : "connect" }), | ||
| }); | ||
| const data = await res.json(); | ||
| setAgents(data.agents); | ||
| } catch (e) { | ||
| console.error(e); | ||
| } finally { | ||
| setSavingAgentId(null); | ||
| } | ||
| } | ||
|
|
||
| async function saveConfig() { | ||
| setSavingConfig(true); | ||
| try { | ||
| const res = await fetch("/api/ai/mcp-config", { | ||
| method: "PUT", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify(config), | ||
| }); | ||
| const data = await res.json(); | ||
| setRecent(data.recent ?? []); | ||
| } catch (e) { | ||
| console.error(e); | ||
| } finally { | ||
| setSavingConfig(false); | ||
| } | ||
| } | ||
|
Comment on lines
+66
to
+98
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add response validation, optimistic updates, and error feedback. Both action handlers (
Suggested improvements for async function toggleAgent(agent: Agent) {
setSavingAgentId(agent.id);
+
+ // Optimistic update
+ const previousAgents = agents;
+ const newStatus = agent.status === "connected" ? "disconnected" : "connected";
+ setAgents(agents.map(a => a.id === agent.id ? { ...a, status: newStatus } : a));
+
try {
const res = await fetch("/api/ai/agents", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: agent.id, action: agent.status === "connected" ? "disconnect" : "connect" }),
});
+
+ if (!res.ok) {
+ throw new Error(`Failed to toggle agent: ${res.status}`);
+ }
+
const data = await res.json();
setAgents(data.agents);
} catch (e) {
console.error(e);
+ // Rollback optimistic update
+ setAgents(previousAgents);
+ // Show error to user
+ alert("Failed to toggle agent. Please try again.");
} finally {
setSavingAgentId(null);
}
}Similar improvements should be applied to |
||
|
|
||
| function StatusDot({ s }: { s: Agent["status"] }) { | ||
| const color = s === "connected" ? "bg-emerald-500" : s === "warning" ? "bg-amber-500" : "bg-rose-500"; | ||
| return <span className={`inline-block h-2.5 w-2.5 rounded-full ${color}`} />; | ||
| } | ||
|
|
||
| if (!mounted || status === "loading") { | ||
| return ( | ||
| <div className="min-h-screen flex items-center justify-center bg-background"> | ||
| <div className="flex flex-col items-center gap-4"> | ||
| <Loader2 className="h-8 w-8 animate-spin text-primary" /> | ||
| <div className="text-lg text-muted-foreground">Loading AI Agents...</div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (!session) return null; | ||
|
|
||
| return ( | ||
| <div className="min-h-screen bg-background relative overflow-hidden"> | ||
| <div className="fixed inset-0 -z-10 grid-background opacity-20" /> | ||
| <div className="fixed inset-0 -z-10"> | ||
| <div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/10 rounded-full blur-3xl pulse-glow" /> | ||
| <div className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-secondary/10 rounded-full blur-3xl pulse-glow" style={{ animationDelay: "1s" }} /> | ||
| </div> | ||
|
|
||
| <Sidebar /> | ||
|
|
||
| <main className="ml-64 min-h-screen transition-all duration-300"> | ||
| <div className="container mx-auto px-8 py-8"> | ||
| {/* Header area to mirror dashboard top spacing */} | ||
| <div className="flex items-center justify-between mb-6"> | ||
| <h1 className="text-xl font-semibold">AI Agents</h1> | ||
| <div className="flex items-center gap-3"> | ||
| <button className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground">π</button> | ||
| <div className="h-9 w-9 rounded-full bg-card border border-border flex items-center justify-center text-muted-foreground">R</div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> | ||
| {/* Left: Agents list (2 cols) */} | ||
| <Card className="lg:col-span-2 border-border bg-card/50"> | ||
| <div className="p-4 md:p-6"> | ||
| <div className="flex items-center gap-2 mb-4"> | ||
| <Bot className="h-5 w-5 text-primary" /> | ||
| <h2 className="text-sm font-medium">AI Coding Agents</h2> | ||
| </div> | ||
|
|
||
| <div className="space-y-4"> | ||
| {(loadingAgents ? [1,2,3].map(n => ({ id: String(n), name: "", status: "disconnected" as const })) : agents).map((agent, idx) => ( | ||
| <div key={agent.id || idx} className="flex items-center justify-between rounded-md border border-border bg-card px-4 py-3"> | ||
| <div className="flex items-center gap-3"> | ||
| <div className="h-9 w-9 rounded-md bg-primary/10 flex items-center justify-center text-primary"> | ||
| <Bot className="h-5 w-5" /> | ||
| </div> | ||
| <div> | ||
| <div className="font-medium text-foreground">{agent.name || "Loading..."}</div> | ||
| </div> | ||
| </div> | ||
| <div className="flex items-center gap-3"> | ||
| <StatusDot s={agent.status} /> | ||
| <Button | ||
| size="sm" | ||
| variant={agent.status === "connected" ? "secondary" : "default"} | ||
| className={agent.status === "connected" ? "" : "bg-primary"} | ||
| onClick={() => toggleAgent(agent)} | ||
| disabled={savingAgentId === agent.id || loadingAgents} | ||
| > | ||
| {savingAgentId === agent.id ? ( | ||
| <Loader2 className="h-4 w-4 animate-spin" /> | ||
| ) : agent.status === "connected" ? ( | ||
| "Disconnect" | ||
| ) : ( | ||
| "Connect" | ||
| )} | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </Card> | ||
|
|
||
| {/* Right: MCP server config */} | ||
| <Card className="border-border bg-card/50"> | ||
| <div className="p-4 md:p-6 space-y-4"> | ||
| <div className="flex items-center gap-2"> | ||
| <ServerCog className="h-5 w-5 text-primary" /> | ||
| <h2 className="text-sm font-medium">MCP Server Configuration</h2> | ||
| </div> | ||
| <div className="space-y-2"> | ||
| <Label htmlFor="mcpUrl">MCP Server URL</Label> | ||
| <Input id="mcpUrl" placeholder="https://..." value={config.url} onChange={(e) => setConfig({ ...config, url: e.target.value })} /> | ||
| </div> | ||
| <div className="space-y-2"> | ||
| <Label htmlFor="mcpKey">API Key</Label> | ||
| <Input id="mcpKey" type="password" placeholder="β’β’β’β’β’β’β’β’" value={config.apiKey} onChange={(e) => setConfig({ ...config, apiKey: e.target.value })} /> | ||
| </div> | ||
| <div> | ||
| <Button onClick={saveConfig} disabled={savingConfig} className="bg-primary"> | ||
| {savingConfig ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null} | ||
| Save Configuration | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| </Card> | ||
| </div> | ||
|
|
||
| {/* Recent configs */} | ||
| <Card className="mt-6 border-border bg-card/50"> | ||
| <div className="p-4 md:p-6"> | ||
| <h3 className="text-sm font-medium mb-3">Recent MCP Server Configurations</h3> | ||
| <ul className="list-disc pl-5 space-y-1 text-sm text-muted-foreground"> | ||
| {recent.length === 0 ? ( | ||
| <li>No recent configurations.</li> | ||
| ) : ( | ||
| recent.map((r, i) => <li key={i}>{r}</li>) | ||
| )} | ||
| </ul> | ||
| </div> | ||
| </Card> | ||
| </div> | ||
| </main> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // Shared in-memory workspace state for dev/demo. Not for production use. | ||
|
|
||
| export type WorkspaceState = { | ||
| id: string; | ||
| name: string; | ||
| provider: string; // aws | gcp | azure | local | ||
| size: string; // small | medium | large | ||
| region: string; // us-east | etc | ||
| status: "running" | "stopped"; | ||
| metrics: { | ||
| cpu: number; // percent | ||
| memory: { usedGb: number; totalGb: number }; | ||
| disk: { usedGb: number; totalGb: number }; | ||
| network: { inMb: number; outMb: number }; | ||
| }; | ||
| terminal: string[]; | ||
| snapshots: Array<{ id: string; createdAt: number; location: string }>; | ||
| assistant: { tips: string[]; note: string }; | ||
| lastUpdate: number; | ||
| }; | ||
|
|
||
| const store = new Map<string, WorkspaceState>(); | ||
|
|
||
| let seed = Date.now() % 100000; | ||
| function rnd() { | ||
| seed = (seed * 1664525 + 1013904223) % 4294967296; | ||
| return seed / 4294967296; | ||
| } | ||
|
|
||
| export function jitter(n: number, pct = 0.15, min = 0, max = Number.POSITIVE_INFINITY) { | ||
| const j = 1 + (rnd() * 2 - 1) * pct; | ||
| const v = Math.max(min, Math.min(max, n * j)); | ||
| return Math.round(v * 100) / 100; | ||
| } | ||
|
|
||
| function defaultTips(name: string) { | ||
| return [ | ||
| "You can improve startup time by updating packages.", | ||
| "Enable hot-reload caching for faster builds.", | ||
| `Run tests in watch mode inside ${name} for quicker feedback.`, | ||
| ]; | ||
| } | ||
|
|
||
| export function ensureWorkspace(id: string): WorkspaceState { | ||
| const key = String(id); | ||
| if (store.has(key)) return store.get(key)!; | ||
| const sizes = { small: { cpu: 2, ram: 4 }, medium: { cpu: 4, ram: 8 }, large: { cpu: 8, ram: 16 } } as const; | ||
| const keys = Object.keys(sizes) as Array<keyof typeof sizes>; | ||
| const pickSize = keys[Math.floor(rnd() * keys.length)] ?? "small"; | ||
| const ws: WorkspaceState = { | ||
| id: key, | ||
| name: `my-nextjs-app-${key}`, | ||
| provider: "aws", | ||
| size: String(pickSize), | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| region: "us-east-1", | ||
| status: "running", | ||
| metrics: { | ||
| cpu: Math.round(25 + rnd() * 40), | ||
| memory: { usedGb: Math.round(2 + rnd() * 6), totalGb: sizes[pickSize].ram }, | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| disk: { usedGb: Math.round(20 + rnd() * 40), totalGb: 100 }, | ||
| network: { inMb: Math.round(80 + rnd() * 80), outMb: Math.round(120 + rnd() * 120) }, | ||
| }, | ||
| terminal: [ | ||
| `ritesh@cloudidex:~$ npm run dev`, | ||
| `> web@ dev`, | ||
| `Server ready on http://localhost:3000 π`, | ||
| ], | ||
| snapshots: [], | ||
| assistant: { tips: defaultTips(`app-${key}`), note: "Predicted CPU load ~60% in next 10 mins" }, | ||
| lastUpdate: Date.now(), | ||
| }; | ||
| store.set(key, ws); | ||
| return ws; | ||
| } | ||
|
|
||
| export function getStore() { | ||
| return store; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { NextResponse } from "next/server"; | ||
| import { prisma } from "@/lib/prisma"; | ||
| import { getServerSession } from "next-auth"; | ||
| import { createAuthConfig } from "@/lib/auth-config"; | ||
|
|
||
| type Conn = { provider: string; connected: boolean; available: boolean }; | ||
|
|
||
| export async function GET() { | ||
| try { | ||
| // Determine provider availability from env | ||
| const googleAvailable = Boolean(process.env.AUTH_GOOGLE_ID && process.env.AUTH_GOOGLE_SECRET); | ||
| const githubAvailable = Boolean(process.env.AUTH_GITHUB_ID && process.env.AUTH_GITHUB_SECRET); | ||
|
|
||
| // Derive connections for the signed-in user from the Accounts table (if signed in) | ||
| const session = await getServerSession(createAuthConfig()); | ||
|
|
||
| let connected = new Set<string>(); | ||
| if (session?.user?.id) { | ||
| try { | ||
| const accounts = await prisma.account.findMany({ | ||
| where: { userId: session.user.id }, | ||
| select: { provider: true }, | ||
| }); | ||
| connected = new Set(accounts.map((a) => a.provider.toLowerCase())); | ||
| } catch (e) { | ||
| console.error("/api/account/connections prisma error", e); | ||
| } | ||
| } | ||
|
|
||
| const providers: Conn[] = [ | ||
| { provider: "Google", connected: connected.has("google"), available: googleAvailable }, | ||
| { provider: "GitHub", connected: connected.has("github"), available: githubAvailable }, | ||
| ]; | ||
|
|
||
| return NextResponse.json({ connections: providers, updatedAt: new Date().toISOString() }); | ||
| } catch (e) { | ||
| console.error("/api/account/connections route error", e); | ||
| // Always return JSON to avoid client JSON parsing errors | ||
| return NextResponse.json( | ||
| { connections: [], error: "failed_to_load_connections" }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| export async function POST() { | ||
| // TODO: hook into your real user deletion logic | ||
| // For now, just simulate success so the button works end-to-end | ||
| return NextResponse.json({ ok: true }, { status: 200 }); | ||
| } | ||
|
Comment on lines
+3
to
+7
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Missing authentication check exposes deletion endpoint. This endpoint can be invoked by anyone without authentication, creating a critical security vulnerability. Even though the deletion logic is a placeholder, the endpoint is publicly accessible. Apply this diff to add authentication: import { NextResponse } from "next/server";
+import { auth } from "@/lib/auth";
-export async function POST() {
+export async function POST(req: Request) {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
+ }
+
// TODO: hook into your real user deletion logic
// For now, just simulate success so the button works end-to-end
return NextResponse.json({ ok: true }, { status: 200 });
} |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| export async function POST(req: Request) { | ||
| // This is a placeholder implementation; validate and change password in your auth system here | ||
| const { current, next } = await req.json(); | ||
| if (!next || next.length < 8) { | ||
| return NextResponse.json({ ok: false, error: "Password too short" }, { status: 400 }); | ||
| } | ||
| return NextResponse.json({ ok: true }); | ||
| } | ||
|
Comment on lines
+3
to
+10
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Missing authentication and current password validation. This endpoint has multiple security issues:
Apply this diff to add authentication and validation: import { NextResponse } from "next/server";
+import { auth } from "@/lib/auth";
export async function POST(req: Request) {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
+ }
+
// This is a placeholder implementation; validate and change password in your auth system here
const { current, next } = await req.json();
+
+ // TODO: Validate current password against stored hash
+ // if (!await validatePassword(session.user.id, current)) {
+ // return NextResponse.json({ ok: false, error: "Current password is incorrect" }, { status: 400 });
+ // }
+
if (!next || next.length < 8) {
return NextResponse.json({ ok: false, error: "Password too short" }, { status: 400 });
}
+
+ // TODO: Hash and update the password in the database
+
return NextResponse.json({ ok: true });
}
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| type Agent = { id: string; name: string; status: "connected" | "disconnected" | "warning" }; | ||
|
|
||
| // In-memory store for demo; replace with DB/service | ||
| let agents: Agent[] = [ | ||
| { id: "code-expo-pilot", name: "Code Expo Pilot", status: "connected" }, | ||
| { id: "cloud-code-copilot", name: "Cloud Code Copilot", status: "disconnected" }, | ||
| { id: "custom-agents", name: "Custom Agents", status: "warning" }, | ||
| ]; | ||
|
Comment on lines
+5
to
+10
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§© Analysis chainReplace in-memory store before production deployment. While the comment acknowledges this is demo code, the in-memory store has the same critical issues as the MCP config route: state will not persist across serverless function invocations in Next.js 15 deployments. Ensure this is replaced with a database or persistent service before merging to production branches. Run this script to check if similar in-memory patterns exist elsewhere: π Script executed: #!/bin/bash
# Description: Find other API routes using in-memory state patterns
# Search for module-level let/var declarations in API routes
rg -n --type=ts -g 'app/api/**/*.ts' '^(let|var)\s+\w+.*=.*\[|\{' -C 2Length of output: 148 Let me gather information about the repository structure and search for in-memory patterns more effectively. π Script executed: #!/bin/bash
# Find TypeScript files in api-related directories
echo "=== Looking for API route files ==="
fd -e ts -e tsx 'api' --type f | head -20
echo ""
echo "=== Checking general structure ==="
fd -type d -name 'api' | head -10
echo ""
echo "=== Search for module-level let/var assignments with arrays/objects in TS files ==="
rg -n --type=ts '^\s*(let|var|const)\s+\w+\s*[:=].*[\[\{]' -A 3 | head -40Length of output: 2413 Let me search more specifically for API routes and mutable state patterns. π Script executed: #!/bin/bash
# Find files in API directory
echo "=== API route files in apps/web ==="
fd 'route\.(ts|tsx)$' apps/web/app/api 2>/dev/null || echo "No route files found"
echo ""
echo "=== Search for mutable state (let/var) in API routes ==="
fd -e ts apps/web/app/api 2>/dev/null | xargs grep -n '^\s*\(let\|var\)\s' 2>/dev/null | head -30
echo ""
echo "=== Specific check for agents route ==="
cat apps/web/app/api/ai/agents/route.ts 2>/dev/null || echo "File not found"
echo ""
echo "=== Check for other mutable module-level state in any API routes ==="
rg -n '^\s*(let|var)\s+\w+.*=' apps/web/app/api/ 2>/dev/null | grep -v node_modulesLength of output: 2405 Replace mutable module-level state stores across multiple API routes before production deployment. Verification confirms the concern is critical and systemic. The agents route contains
In Next.js 15 serverless deployments, state stored at module level is lost on each function invocation. Replace all of these with database or persistent service calls before merging to production branches. π€ Prompt for AI Agents |
||
|
|
||
| export async function GET() { | ||
| return NextResponse.json({ agents }); | ||
| } | ||
|
Comment on lines
+12
to
+14
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add authentication to the GET endpoint. The GET handler allows unauthenticated access to agent data. Consider adding session validation to ensure only authenticated users can view agents. Apply this diff to add authentication: +import { getServerSession } from "next-auth/next";
+import { createAuthConfig } from "@/lib/auth-config";
+
export async function GET() {
+ const session = await getServerSession(createAuthConfig());
+ if (!session) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
return NextResponse.json({ agents });
}
π€ Prompt for AI Agents |
||
|
|
||
| export async function POST(req: Request) { | ||
| const { id, action } = await req.json(); | ||
| agents = agents.map((a) => | ||
| a.id === id | ||
| ? { | ||
| ...a, | ||
| status: action === "connect" ? "connected" : action === "disconnect" ? "disconnected" : a.status, | ||
| } | ||
| : a | ||
| ); | ||
| return NextResponse.json({ ok: true, agents }); | ||
| } | ||
|
Comment on lines
+16
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add input validation and improve status update logic. The POST handler has several issues:
+import { z } from "zod";
+
+const agentActionSchema = z.object({
+ id: z.string().min(1),
+ action: z.enum(["connect", "disconnect"]),
+});
+
export async function POST(req: Request) {
+ try {
- const { id, action } = await req.json();
+ const body = await req.json();
+ const { id, action } = agentActionSchema.parse(body);
+
+ const agent = agents.find(a => a.id === id);
+ if (!agent) {
+ return NextResponse.json({ error: "Agent not found" }, { status: 404 });
+ }
+
agents = agents.map((a) =>
a.id === id
? {
...a,
- status: action === "connect" ? "connected" : action === "disconnect" ? "disconnected" : a.status,
+ status: action === "connect" ? "connected" : "disconnected",
}
: a
);
return NextResponse.json({ ok: true, agents });
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json({ error: "Invalid input" }, { status: 400 });
+ }
+ return NextResponse.json({ error: "Internal error" }, { status: 500 });
+ }
}π€ Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Improve error handling and user feedback for data fetching.
The data fetching logic has minimal error handling:
response.okbefore parsing JSONuseEffect(() => { async function fetchData() { try { setLoadingAgents(true); const [a, c] = await Promise.all([ - fetch("/api/ai/agents").then((r) => r.json()), - fetch("/api/ai/mcp-config").then((r) => r.json()), + fetch("/api/ai/agents").then(async (r) => { + if (!r.ok) throw new Error(`Failed to fetch agents: ${r.status}`); + return r.json(); + }), + fetch("/api/ai/mcp-config").then(async (r) => { + if (!r.ok) throw new Error(`Failed to fetch config: ${r.status}`); + return r.json(); + }), ]); setAgents(a.agents ?? []); setConfig({ url: c.url ?? "", apiKey: c.apiKey ?? "" }); setRecent(c.recent ?? []); } catch (e) { console.error(e); + // Add user-visible error state + // setError("Failed to load AI agents. Please refresh the page."); } finally { setLoadingAgents(false); } } if (mounted) fetchData(); }, [mounted]);Consider adding an error state and displaying it to users in the UI.
π Committable suggestion