diff --git a/frontends/nextjs/src/app/codegen/CodegenStudioClient.tsx b/frontends/nextjs/src/app/codegen/CodegenStudioClient.tsx index ee56df809..f67d800a4 100644 --- a/frontends/nextjs/src/app/codegen/CodegenStudioClient.tsx +++ b/frontends/nextjs/src/app/codegen/CodegenStudioClient.tsx @@ -1,6 +1,5 @@ 'use client' -import type { CodegenManifest } from '@/lib/codegen/codegen-types' import { useMemo, useState, type ChangeEvent } from 'react' import { @@ -16,6 +15,10 @@ import { Typography, } from '@mui/material' +import Header from './components/Header' +import Sidebar from './components/Sidebar' +import { useCodegenData, type CodegenRequest } from './hooks/useCodegenData' + const runtimeOptions = [ { value: 'web', label: 'Next.js web' }, { value: 'cli', label: 'Command line' }, @@ -24,7 +27,7 @@ const runtimeOptions = [ { value: 'server', label: 'Server service' }, ] -const initialFormState = { +const initialFormState: CodegenRequest = { projectName: 'nebula-launch', packageId: 'codegen_studio', runtime: 'web', @@ -32,51 +35,11 @@ const initialFormState = { brief: 'Modern web interface with CLI companions', } -type FormState = (typeof initialFormState) - -type FetchStatus = 'idle' | 'loading' | 'success' - -const createFilename = (header: string | null, fallback: string) => { - const match = header?.match(/filename="?([^"]+)"?/) ?? null - return match ? match[1] : fallback -} - -const downloadBlob = (blob: Blob, filename: string) => { - const url = URL.createObjectURL(blob) - const anchor = document.createElement('a') - anchor.href = url - anchor.download = filename - document.body.appendChild(anchor) - anchor.click() - anchor.remove() - URL.revokeObjectURL(url) -} - -const fetchZip = async (values: FormState) => { - const response = await fetch('/api/codegen/studio', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(values), - }) - if (!response.ok) { - throw new Error('Codegen Studio service returned an error') - } - const blob = await response.blob() - const filename = createFilename(response.headers.get('content-disposition'), `${values.projectName}.zip`) - downloadBlob(blob, filename) - const manifestHeader = response.headers.get('x-codegen-manifest') - const manifest = manifestHeader - ? (JSON.parse(decodeURIComponent(manifestHeader)) as CodegenManifest) - : null - return { filename, manifest } -} +type FormState = typeof initialFormState export default function CodegenStudioClient() { const [form, setForm] = useState(initialFormState) - const [status, setStatus] = useState('idle') - const [message, setMessage] = useState(null) - const [error, setError] = useState(null) - const [manifest, setManifest] = useState(null) + const { status, message, error, manifest, generate } = useCodegenData() const runtimeDescription = useMemo(() => { switch (form.runtime) { @@ -112,125 +75,62 @@ export default function CodegenStudioClient() { setForm((prev) => ({ ...prev, [key]: event.target.value })) } - const handleSubmit = async () => { - setStatus('loading') - setError(null) - setMessage(null) - try { - const { filename, manifest } = await fetchZip(form) - setMessage(`Zip ${filename} created successfully.`) - setManifest(manifest) - setStatus('success') - } catch (err) { - setError(err instanceof Error ? err.message : 'Unable to generate the zip') - setManifest(null) - setStatus('idle') - } - } + const handleSubmit = () => generate(form) return ( - - - - - Codegen Studio Export - - - Configure a starter bundle for MetaBuilder packages and download it instantly. - - - - - + + +
- - - {runtimeOptions.map((option) => ( - - {option.label} - - ))} - - - {runtimeDescription} - - - - - - - - {message && {message}} - {error && {error}} - {manifest && ( - - - Manifest preview + + + + + + {runtimeOptions.map((option) => ( + + {option.label} + + ))} + + + {runtimeDescription} - - - Project: {manifest.projectName} - - - Package: {manifest.packageId} - - - Runtime: {manifest.runtime} - - - Tone: {manifest.tone ?? 'adaptive'} - - - Generated at: {new Date(manifest.generatedAt).toLocaleString()} - - - - )} - - Bundle contents - {previewFiles.map((entry) => ( - - • {entry} - - ))} + + + + + + + {message && {message}} + {error && {error}} + + + + diff --git a/frontends/nextjs/src/app/codegen/components/Header.tsx b/frontends/nextjs/src/app/codegen/components/Header.tsx new file mode 100644 index 000000000..29dcd934f --- /dev/null +++ b/frontends/nextjs/src/app/codegen/components/Header.tsx @@ -0,0 +1,21 @@ +'use client' + +import { Stack, Typography } from '@mui/material' + +interface HeaderProps { + title: string + subtitle: string +} + +export default function Header({ title, subtitle }: HeaderProps) { + return ( + + + {title} + + + {subtitle} + + + ) +} diff --git a/frontends/nextjs/src/app/codegen/components/Sidebar.tsx b/frontends/nextjs/src/app/codegen/components/Sidebar.tsx new file mode 100644 index 000000000..27172ca38 --- /dev/null +++ b/frontends/nextjs/src/app/codegen/components/Sidebar.tsx @@ -0,0 +1,51 @@ +'use client' + +import type { CodegenManifest } from '@/lib/codegen/codegen-types' +import { Paper, Stack, Typography } from '@mui/material' + +interface SidebarProps { + manifest: CodegenManifest | null + previewFiles: string[] +} + +export default function Sidebar({ manifest, previewFiles }: SidebarProps) { + return ( + + {manifest && ( + + + Manifest preview + + + + Project: {manifest.projectName} + + + Package: {manifest.packageId} + + + Runtime: {manifest.runtime} + + + Tone: {manifest.tone ?? 'adaptive'} + + + Generated at: {new Date(manifest.generatedAt).toLocaleString()} + + + + )} + + Bundle contents + {previewFiles.map((entry) => ( + + • {entry} + + ))} + + + ) +} diff --git a/frontends/nextjs/src/app/codegen/hooks/useCodegenData.ts b/frontends/nextjs/src/app/codegen/hooks/useCodegenData.ts new file mode 100644 index 000000000..61d831c7c --- /dev/null +++ b/frontends/nextjs/src/app/codegen/hooks/useCodegenData.ts @@ -0,0 +1,74 @@ +'use client' + +import type { CodegenManifest } from '@/lib/codegen/codegen-types' +import { useCallback, useState } from 'react' + +export type CodegenRequest = { + projectName: string + packageId: string + runtime: string + tone: string + brief: string +} + +export type FetchStatus = 'idle' | 'loading' | 'success' + +const createFilename = (header: string | null, fallback: string) => { + const match = header?.match(/filename="?([^"]+)"?/) ?? null + return match ? match[1] : fallback +} + +const downloadBlob = (blob: Blob, filename: string) => { + const url = URL.createObjectURL(blob) + const anchor = document.createElement('a') + anchor.href = url + anchor.download = filename + document.body.appendChild(anchor) + anchor.click() + anchor.remove() + URL.revokeObjectURL(url) +} + +const fetchZip = async (values: CodegenRequest) => { + const response = await fetch('/api/codegen/studio', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(values), + }) + if (!response.ok) { + throw new Error('Codegen Studio service returned an error') + } + const blob = await response.blob() + const filename = createFilename(response.headers.get('content-disposition'), `${values.projectName}.zip`) + downloadBlob(blob, filename) + const manifestHeader = response.headers.get('x-codegen-manifest') + const manifest = manifestHeader + ? (JSON.parse(decodeURIComponent(manifestHeader)) as CodegenManifest) + : null + return { filename, manifest } +} + +export function useCodegenData() { + const [status, setStatus] = useState('idle') + const [message, setMessage] = useState(null) + const [error, setError] = useState(null) + const [manifest, setManifest] = useState(null) + + const generate = useCallback(async (values: CodegenRequest) => { + setStatus('loading') + setError(null) + setMessage(null) + try { + const { filename, manifest: manifestResult } = await fetchZip(values) + setMessage(`Zip ${filename} created successfully.`) + setManifest(manifestResult) + setStatus('success') + } catch (err) { + setError(err instanceof Error ? err.message : 'Unable to generate the zip') + setManifest(null) + setStatus('idle') + } + }, []) + + return { status, message, error, manifest, generate } +}