From 0b6a227dc328f93b4af3a93d1690a200f1eb9116 Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 9 Feb 2026 23:15:40 -0800 Subject: [PATCH] Wire studio page to persistent session data --- data/studio-sessions.json | 146 ++++ src/app/api/studio/[id]/route.ts | 103 +++ src/app/api/studio/route.ts | 82 +++ src/app/studio/page.tsx | 1064 ++++++++++-------------------- src/lib/studioStore.ts | 98 +++ 5 files changed, 793 insertions(+), 700 deletions(-) create mode 100644 data/studio-sessions.json create mode 100644 src/app/api/studio/[id]/route.ts create mode 100644 src/app/api/studio/route.ts create mode 100644 src/lib/studioStore.ts diff --git a/data/studio-sessions.json b/data/studio-sessions.json new file mode 100644 index 00000000..3ff42382 --- /dev/null +++ b/data/studio-sessions.json @@ -0,0 +1,146 @@ +[ + { + "id": "studio_1001", + "skillSlug": "content-summarizer", + "skillName": "Content Summarizer", + "status": "completed", + "inputs": { + "source": "https://foragents.dev/docs/bootstrap", + "tone": "technical", + "maxBullets": 5 + }, + "outputs": { + "summary": "Bootstrap docs define setup flow, required files, and validation checks.", + "bullets": [ + "Load bootstrap files first", + "Validate environment assumptions", + "Track run metadata" + ] + }, + "logs": [ + { + "timestamp": "2026-02-09T19:00:12.000Z", + "message": "Session queued" + }, + { + "timestamp": "2026-02-09T19:00:14.000Z", + "message": "Fetched source content" + }, + { + "timestamp": "2026-02-09T19:00:20.000Z", + "message": "Generated summary output" + } + ], + "startedAt": "2026-02-09T19:00:12.000Z", + "completedAt": "2026-02-09T19:00:21.000Z" + }, + { + "id": "studio_1002", + "skillSlug": "dependency-scanner", + "skillName": "Dependency Scanner", + "status": "active", + "inputs": { + "repo": "reflectt/foragents.dev", + "depth": "full" + }, + "outputs": { + "checkedPackages": 112, + "vulnerabilities": 0 + }, + "logs": [ + { + "timestamp": "2026-02-09T21:10:02.000Z", + "message": "Scanning lockfile" + }, + { + "timestamp": "2026-02-09T21:10:18.000Z", + "message": "Resolving transitive dependency graph" + } + ], + "startedAt": "2026-02-09T21:10:02.000Z", + "completedAt": null + }, + { + "id": "studio_1003", + "skillSlug": "release-note-writer", + "skillName": "Release Note Writer", + "status": "error", + "inputs": { + "range": "v0.9.0..v1.0.0", + "includeBreaking": true + }, + "outputs": { + "error": "No git tag found for v0.9.0" + }, + "logs": [ + { + "timestamp": "2026-02-08T15:42:00.000Z", + "message": "Collecting commits" + }, + { + "timestamp": "2026-02-08T15:42:03.000Z", + "message": "Failed to resolve start tag v0.9.0" + } + ], + "startedAt": "2026-02-08T15:42:00.000Z", + "completedAt": "2026-02-08T15:42:03.000Z" + }, + { + "id": "studio_1004", + "skillSlug": "api-schema-checker", + "skillName": "API Schema Checker", + "status": "completed", + "inputs": { + "endpoint": "/api/skill/[slug]", + "strict": false + }, + "outputs": { + "warnings": [ + "Missing example for 404 response" + ], + "status": "pass" + }, + "logs": [ + { + "timestamp": "2026-02-07T12:07:10.000Z", + "message": "Loaded route schema" + }, + { + "timestamp": "2026-02-07T12:07:13.000Z", + "message": "Validated request and response shapes" + } + ], + "startedAt": "2026-02-07T12:07:10.000Z", + "completedAt": "2026-02-07T12:07:14.000Z" + }, + { + "id": "studio_1005", + "skillSlug": "log-triage", + "skillName": "Log Triage", + "status": "completed", + "inputs": { + "window": "24h", + "severity": "warn" + }, + "outputs": { + "clusters": 3, + "topIssue": "Missing auth header on /api/verify/start" + }, + "logs": [ + { + "timestamp": "2026-02-06T08:33:44.000Z", + "message": "Indexed 1,932 log lines" + }, + { + "timestamp": "2026-02-06T08:34:01.000Z", + "message": "Ranked anomaly clusters" + }, + { + "timestamp": "2026-02-06T08:34:09.000Z", + "message": "Exported triage report" + } + ], + "startedAt": "2026-02-06T08:33:44.000Z", + "completedAt": "2026-02-06T08:34:09.000Z" + } +] diff --git a/src/app/api/studio/[id]/route.ts b/src/app/api/studio/[id]/route.ts new file mode 100644 index 00000000..c2503788 --- /dev/null +++ b/src/app/api/studio/[id]/route.ts @@ -0,0 +1,103 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + isStudioSessionStatus, + readStudioSessions, + sanitizeOutputs, + StudioSession, + writeStudioSessions, +} from "@/lib/studioStore"; + +type UpdateStudioSessionRequest = { + status?: unknown; + outputs?: unknown; + logMessage?: unknown; +}; + +export async function GET( + _request: NextRequest, + context: { params: Promise<{ id: string }> } +) { + const { id } = await context.params; + const sessions = await readStudioSessions(); + const session = sessions.find((item) => item.id === id); + + if (!session) { + return NextResponse.json({ error: "Session not found" }, { status: 404 }); + } + + return NextResponse.json( + { + session, + updatedAt: new Date().toISOString(), + }, + { + headers: { + "Cache-Control": "no-store", + }, + } + ); +} + +export async function PATCH( + request: NextRequest, + context: { params: Promise<{ id: string }> } +) { + const { id } = await context.params; + + let payload: UpdateStudioSessionRequest; + try { + payload = (await request.json()) as UpdateStudioSessionRequest; + } catch { + return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); + } + + const sessions = await readStudioSessions(); + const targetIndex = sessions.findIndex((session) => session.id === id); + + if (targetIndex === -1) { + return NextResponse.json({ error: "Session not found" }, { status: 404 }); + } + + const current = sessions[targetIndex] as StudioSession; + const now = new Date().toISOString(); + + const nextStatus = payload.status; + if (typeof nextStatus !== "undefined" && !isStudioSessionStatus(nextStatus)) { + return NextResponse.json({ error: "status must be active, completed, or error" }, { status: 400 }); + } + + const logMessage = typeof payload.logMessage === "string" ? payload.logMessage.trim() : ""; + + const updatedSession: StudioSession = { + ...current, + status: isStudioSessionStatus(nextStatus) ? nextStatus : current.status, + outputs: + typeof payload.outputs === "undefined" + ? current.outputs + : { + ...current.outputs, + ...sanitizeOutputs(payload.outputs), + }, + logs: logMessage + ? [ + ...current.logs, + { + timestamp: now, + message: logMessage, + }, + ] + : current.logs, + completedAt: + isStudioSessionStatus(nextStatus) && nextStatus !== "active" + ? now + : isStudioSessionStatus(nextStatus) && nextStatus === "active" + ? null + : current.completedAt, + }; + + const nextSessions = [...sessions]; + nextSessions[targetIndex] = updatedSession; + await writeStudioSessions(nextSessions); + + return NextResponse.json({ session: updatedSession }); +} diff --git a/src/app/api/studio/route.ts b/src/app/api/studio/route.ts new file mode 100644 index 00000000..e598f853 --- /dev/null +++ b/src/app/api/studio/route.ts @@ -0,0 +1,82 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + formatSkillNameFromSlug, + readStudioSessions, + sanitizeInputs, + StudioSession, + writeStudioSessions, +} from "@/lib/studioStore"; + +type StartStudioSessionRequest = { + skillSlug?: unknown; + skillName?: unknown; + inputs?: unknown; +}; + +export async function GET() { + const sessions = await readStudioSessions(); + + return NextResponse.json( + { + sessions, + count: sessions.length, + updatedAt: new Date().toISOString(), + }, + { + headers: { + "Cache-Control": "no-store", + }, + } + ); +} + +export async function POST(request: NextRequest) { + let payload: StartStudioSessionRequest; + + try { + payload = (await request.json()) as StartStudioSessionRequest; + } catch { + return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); + } + + const skillSlug = typeof payload.skillSlug === "string" ? payload.skillSlug.trim().toLowerCase() : ""; + + if (!skillSlug) { + return NextResponse.json({ error: "skillSlug is required" }, { status: 400 }); + } + + const skillName = + typeof payload.skillName === "string" && payload.skillName.trim() + ? payload.skillName.trim() + : formatSkillNameFromSlug(skillSlug); + + const inputs = sanitizeInputs(payload.inputs); + const sessions = await readStudioSessions(); + const now = new Date().toISOString(); + + const nextSession: StudioSession = { + id: `studio_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + skillSlug, + skillName, + status: "active", + inputs, + outputs: {}, + logs: [ + { + timestamp: now, + message: `Started ${skillName}`, + }, + ], + startedAt: now, + completedAt: null, + }; + + await writeStudioSessions([nextSession, ...sessions]); + + return NextResponse.json( + { + session: nextSession, + }, + { status: 201 } + ); +} diff --git a/src/app/studio/page.tsx b/src/app/studio/page.tsx index 86f098ad..8b884658 100644 --- a/src/app/studio/page.tsx +++ b/src/app/studio/page.tsx @@ -1,759 +1,423 @@ +/* eslint-disable react/no-unescaped-entities */ "use client"; -import { useState, useEffect } from "react"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; -import { Checkbox } from "@/components/ui/checkbox"; -import templates from "@/data/studio-templates.json"; - -type Parameter = { - name: string; - type: string; - description: string; - default: string | number; -}; +import { Textarea } from "@/components/ui/textarea"; -type EnvVar = { - name: string; - description: string; - required: boolean; +type StudioSessionStatus = "active" | "completed" | "error"; + +type StudioLogEntry = { + timestamp: string; + message: string; }; -type Template = { +type StudioSession = { id: string; - name: string; - description: string; - category: string; - tags: string[]; - parameters: Parameter[]; - dependencies: string[]; - envVars: EnvVar[]; - sampleInput: string; - sampleOutput: string; - validation: string; - skillTemplate: string; + skillSlug: string; + skillName: string; + status: StudioSessionStatus; + inputs: Record; + outputs: Record; + logs: StudioLogEntry[]; + startedAt: string; + completedAt: string | null; }; -type SkillData = { - name: string; - description: string; - category: string; - tags: string[]; - author: string; - install_cmd: string; - repo_url: string; - parameters: Parameter[]; - dependencies: string[]; - envVars: EnvVar[]; - sampleInput: string; - sampleOutput: string; - validation: string; +const SKILL_OPTIONS = [ + { slug: "content-summarizer", name: "Content Summarizer" }, + { slug: "dependency-scanner", name: "Dependency Scanner" }, + { slug: "release-note-writer", name: "Release Note Writer" }, + { slug: "api-schema-checker", name: "API Schema Checker" }, + { slug: "log-triage", name: "Log Triage" }, +]; + +const STATUS_BADGE_VARIANT: Record = { + active: "default", + completed: "secondary", + error: "destructive", }; -const CATEGORIES = [ - "Core Systems", - "Integration", - "Utilities", - "Communication", - "Automation", - "Observability", - "Security", - "Data", -]; +function safeJsonParse(value: string): Record { + if (!value.trim()) return {}; + + try { + const parsed = JSON.parse(value) as unknown; + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}; + return parsed as Record; + } catch { + return {}; + } +} + +function prettyJson(value: unknown): string { + return JSON.stringify(value, null, 2); +} export default function StudioPage() { - const [step, setStep] = useState(0); // 0 = template picker, 1-4 = wizard steps - const [, setSelectedTemplate] = useState