mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-05-05 11:09:39 +00:00
Merge pull request #240 from johndoe6345789/codex/create-header-and-sidebar-components
Refactor Codegen Studio layout
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user