Merge pull request #240 from johndoe6345789/codex/create-header-and-sidebar-components

Refactor Codegen Studio layout
This commit is contained in:
2025-12-27 18:40:23 +00:00
committed by GitHub
4 changed files with 205 additions and 159 deletions
@@ -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<FormState>(initialFormState)
const [status, setStatus] = useState<FetchStatus>('idle')
const [message, setMessage] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [manifest, setManifest] = useState<CodegenManifest | null>(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 (
<Container maxWidth="md" className="py-16">
<Paper elevation={8} className="p-8 space-y-6">
<Stack spacing={2}>
<Typography variant="h3" component="h1">
Codegen Studio Export
</Typography>
<Typography variant="body1" color="text.secondary">
Configure a starter bundle for MetaBuilder packages and download it instantly.
</Typography>
</Stack>
<Stack spacing={3}>
<TextField
label="Project name"
value={form.projectName}
onChange={handleChange('projectName')}
fullWidth
<Container maxWidth="lg" className="py-16">
<Paper elevation={8} className="p-8">
<Stack spacing={5}>
<Header
title="Codegen Studio Export"
subtitle="Configure a starter bundle for MetaBuilder packages and download it instantly."
/>
<TextField
label="Package id"
value={form.packageId}
onChange={handleChange('packageId')}
fullWidth
/>
<TextField
select
label="Runtime"
value={form.runtime}
onChange={handleChange('runtime')}
fullWidth
>
{runtimeOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
<Typography variant="body2" color="text.secondary">
{runtimeDescription}
</Typography>
<TextField
label="Tone"
value={form.tone}
onChange={handleChange('tone')}
fullWidth
/>
<TextField
label="Creative brief"
value={form.brief}
onChange={handleChange('brief')}
fullWidth
multiline
minRows={3}
/>
<Box>
<Button
variant="contained"
color="primary"
onClick={handleSubmit}
disabled={status === 'loading'}
startIcon={status === 'loading' ? <CircularProgress size={16} /> : null}
>
{status === 'loading' ? 'Generating...' : 'Generate ZIP'}
</Button>
</Box>
{message && <Alert severity="success">{message}</Alert>}
{error && <Alert severity="error">{error}</Alert>}
{manifest && (
<Paper
elevation={1}
sx={{ border: '1px dashed', borderColor: 'divider', p: 2, backgroundColor: 'background.default' }}
>
<Typography variant="subtitle1" gutterBottom>
Manifest preview
<Stack direction={{ xs: 'column', md: 'row' }} spacing={5} alignItems="flex-start">
<Stack spacing={3} flex={1} width="100%">
<TextField
label="Project name"
value={form.projectName}
onChange={handleChange('projectName')}
fullWidth
/>
<TextField label="Package id" value={form.packageId} onChange={handleChange('packageId')} fullWidth />
<TextField select label="Runtime" value={form.runtime} onChange={handleChange('runtime')} fullWidth>
{runtimeOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
<Typography variant="body2" color="text.secondary">
{runtimeDescription}
</Typography>
<Stack spacing={0.5}>
<Typography variant="body2" color="text.secondary">
Project: {manifest.projectName}
</Typography>
<Typography variant="body2" color="text.secondary">
Package: {manifest.packageId}
</Typography>
<Typography variant="body2" color="text.secondary">
Runtime: {manifest.runtime}
</Typography>
<Typography variant="body2" color="text.secondary">
Tone: {manifest.tone ?? 'adaptive'}
</Typography>
<Typography variant="body2" color="text.secondary">
Generated at: {new Date(manifest.generatedAt).toLocaleString()}
</Typography>
</Stack>
</Paper>
)}
<Stack spacing={0.5}>
<Typography variant="subtitle2">Bundle contents</Typography>
{previewFiles.map((entry) => (
<Typography key={entry} variant="body2" color="text.secondary">
{entry}
</Typography>
))}
<TextField label="Tone" value={form.tone} onChange={handleChange('tone')} fullWidth />
<TextField
label="Creative brief"
value={form.brief}
onChange={handleChange('brief')}
fullWidth
multiline
minRows={3}
/>
<Box>
<Button
variant="contained"
color="primary"
onClick={handleSubmit}
disabled={status === 'loading'}
startIcon={status === 'loading' ? <CircularProgress size={16} /> : null}
>
{status === 'loading' ? 'Generating...' : 'Generate ZIP'}
</Button>
</Box>
{message && <Alert severity="success">{message}</Alert>}
{error && <Alert severity="error">{error}</Alert>}
</Stack>
<Box width={{ xs: '100%', md: 320 }} flexShrink={0}>
<Sidebar manifest={manifest} previewFiles={previewFiles} />
</Box>
</Stack>
</Stack>
</Paper>
@@ -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 (
<Stack spacing={2}>
<Typography variant="h3" component="h1">
{title}
</Typography>
<Typography variant="body1" color="text.secondary">
{subtitle}
</Typography>
</Stack>
)
}
@@ -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 (
<Stack spacing={3}>
{manifest && (
<Paper
elevation={1}
sx={{ border: '1px dashed', borderColor: 'divider', p: 2, backgroundColor: 'background.default' }}
>
<Typography variant="subtitle1" gutterBottom>
Manifest preview
</Typography>
<Stack spacing={0.5}>
<Typography variant="body2" color="text.secondary">
Project: {manifest.projectName}
</Typography>
<Typography variant="body2" color="text.secondary">
Package: {manifest.packageId}
</Typography>
<Typography variant="body2" color="text.secondary">
Runtime: {manifest.runtime}
</Typography>
<Typography variant="body2" color="text.secondary">
Tone: {manifest.tone ?? 'adaptive'}
</Typography>
<Typography variant="body2" color="text.secondary">
Generated at: {new Date(manifest.generatedAt).toLocaleString()}
</Typography>
</Stack>
</Paper>
)}
<Stack spacing={1}>
<Typography variant="subtitle2">Bundle contents</Typography>
{previewFiles.map((entry) => (
<Typography key={entry} variant="body2" color="text.secondary">
{entry}
</Typography>
))}
</Stack>
</Stack>
)
}
@@ -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<FetchStatus>('idle')
const [message, setMessage] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [manifest, setManifest] = useState<CodegenManifest | null>(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 }
}