Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
82d6764582 ai fleury 2026-03-26 07:42:33 +00:00
7 changed files with 1077 additions and 153 deletions

View File

@ -3,10 +3,8 @@ import { mdiLogout, mdiClose } from '@mdi/js'
import BaseIcon from './BaseIcon'
import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces'
import { useAppSelector } from '../stores/hooks'
import { useAppDispatch, useAppSelector } from '../stores/hooks'
import Link from 'next/link';
import { useAppDispatch } from '../stores/hooks';
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

View File

@ -1,6 +1,5 @@
import React, {useEffect, useRef} from 'react'
import React, { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'

View File

@ -1,5 +1,4 @@
import React, { ReactNode, useEffect } from 'react'
import { useState } from 'react'
import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'

View File

@ -7,6 +7,14 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
},
{
href: '/generator-studio',
label: 'Generator Studio',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiRocketLaunch' in icon ? icon['mdiRocketLaunch' as keyof typeof icon] : icon.mdiTable,
permissions: 'READ_PROJECTS'
},
{
href: '/users/users-list',

View File

@ -0,0 +1,925 @@
import Head from 'next/head';
import axios from 'axios';
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
import CardBox from '../components/CardBox';
import BaseButton from '../components/BaseButton';
import LayoutAuthenticated from '../layouts/Authenticated';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import FormField from '../components/FormField';
import { getPageTitle } from '../config';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { aiResponse } from '../stores/openAiSlice';
import { hasPermission } from '../helpers/userPermissions';
import { mdiChartTimelineVariant } from '@mdi/js';
type StudioOverview = {
stats: {
totalProjects: number;
activeRuns: number;
successfulDeployments: number;
};
recentProjects: any[];
recommendedTemplates: any[];
vmPlans: any[];
};
const TEMPLATE_CHOICES = [
{
key: 'saas_engine',
name: 'SAAS Engine',
badge: 'Recommended',
accent: 'from-cyan-500/20 via-slate-900 to-slate-950',
meta: '$399 license · Next.js full-stack · Auth/RBAC/CRUD',
pitch: 'Best for data-heavy SaaS products with dashboard workflows and production-ready admin tooling.',
},
{
key: 'python_runtime',
name: 'Python Instant Runtime',
badge: 'Free alternative',
accent: 'from-violet-500/20 via-slate-900 to-slate-950',
meta: 'Free · Django admin-first · Fast data modeling',
pitch: 'A leaner path when you want lower VM cost and strong admin primitives before custom UI polish.',
},
];
const ENVIRONMENT_CHOICES = [
{
key: 'sandbox',
name: 'Dedicated Sandbox',
meta: 'Fastest path to a live URL on a VM-managed preview environment.',
},
{
key: 'production',
name: 'Production Preview',
meta: 'Creates the same pipeline but stamps the environment as production-ready.',
},
];
const FALLBACK_VM_PLANS = [
{
id: 'saas-plan',
name: 'e2-small Sandbox',
machine_type: 'e2-small',
vcpu: 2,
memory_gb: 2,
credits_per_day: 0.5,
},
{
id: 'python-plan',
name: 'e2-micro Sandbox',
machine_type: 'e2-micro',
vcpu: 2,
memory_gb: 1,
credits_per_day: 0.25,
},
];
const FALLBACK_TEMPLATES = [
{
id: 'saas-template',
name: 'SAAS Engine',
description: 'Next.js full-stack template with auth, RBAC, CRUD, and production-ready patterns.',
license_type: 'paid',
price_usd: 399,
},
{
id: 'python-template',
name: 'Django Instant Runtime',
description: 'Free admin-first runtime with fast data modeling and lower VM cost.',
license_type: 'free',
price_usd: 0,
},
];
const CREATE_PERMISSIONS = [
'CREATE_PROJECTS',
'CREATE_APP_SPECS',
'CREATE_VM_SANDBOXES',
'CREATE_PROJECT_ENVIRONMENTS',
'CREATE_GENERATION_RUNS',
'CREATE_RUN_LOGS',
'CREATE_DEPLOYMENTS',
];
const formatTimestamp = (value?: string) => {
if (!value) return 'Just now';
return new Date(value).toLocaleString();
};
const extractAiText = (response: any) => {
const payload = response?.data || response;
const output = Array.isArray(payload?.output) ? payload.output : [];
return output
.flatMap((item: any) => (Array.isArray(item?.content) ? item.content : []))
.filter((block: any) => block?.type === 'output_text' && typeof block?.text === 'string')
.map((block: any) => block.text)
.join('\n\n')
.trim();
};
const sortByRecent = (items: any[] = []) =>
[...items].sort(
(left, right) =>
new Date(right?.updatedAt || right?.createdAt || 0).getTime() -
new Date(left?.updatedAt || left?.createdAt || 0).getTime(),
);
const fetchCollection = async (endpoint: string) => {
const response = await axios.get(endpoint);
return Array.isArray(response.data?.rows) ? response.data.rows : [];
};
const getLatestItem = (items: any[], predicate: (item: any) => boolean) => sortByRecent(items.filter(predicate))[0] || null;
const getOrganizationId = (currentUser: any) => currentUser?.organizations?.id || currentUser?.organizationsId || null;
const getTemplateName = (templateChoice: string) =>
templateChoice === 'python_runtime' ? 'Django Instant Runtime' : 'SAAS Engine';
const getPlanName = (templateChoice: string) =>
templateChoice === 'python_runtime' ? 'e2-micro Sandbox' : 'e2-small Sandbox';
const createOverview = (resources: Record<string, any[]>) => {
const projects = sortByRecent(resources.projects || []);
const appSpecs = resources.app_specs || [];
const generationRuns = resources.generation_runs || [];
const deployments = resources.deployments || [];
const projectEnvironments = resources.project_environments || [];
return {
stats: {
totalProjects: projects.length,
activeRuns: generationRuns.filter((item) => ['queued', 'running'].includes(item.status)).length,
successfulDeployments: deployments.filter((item) => item.status === 'succeeded').length,
},
recommendedTemplates: (resources.app_templates || []).filter((item) => item.is_recommended),
vmPlans: (resources.vm_plans || []).filter((item) => item.is_active),
recentProjects: projects.slice(0, 6).map((project) => {
const latestSpec = getLatestItem(appSpecs, (item) => item?.project?.id === project.id);
const latestRun = getLatestItem(generationRuns, (item) => item?.spec?.id === latestSpec?.id);
const latestDeployment = getLatestItem(deployments, (item) => item?.run?.id === latestRun?.id);
const latestEnvironment = getLatestItem(projectEnvironments, (item) => item?.project?.id === project.id);
const vmPlan = latestEnvironment?.sandbox?.plan || null;
const ownerName = [project.owner?.firstName, project.owner?.lastName].filter(Boolean).join(' ').trim();
return {
id: project.id,
name: project.name,
slug: project.slug,
description: project.description,
status: project.status,
visibility: project.visibility,
updatedAt: project.updatedAt,
ownerName: ownerName || project.owner?.email || 'Unknown owner',
latestSpec,
latestRun,
latestDeployment,
latestEnvironment,
vmPlan,
};
}),
};
};
const metricCardClassName =
'rounded-3xl border border-white/10 bg-slate-950/70 p-5 shadow-[0_20px_80px_-40px_rgba(34,211,238,0.5)] backdrop-blur';
const statusColorMap: Record<string, string> = {
succeeded: 'bg-emerald-500/15 text-emerald-300 border border-emerald-400/20',
ready: 'bg-emerald-500/15 text-emerald-300 border border-emerald-400/20',
active: 'bg-emerald-500/15 text-emerald-300 border border-emerald-400/20',
running: 'bg-amber-500/15 text-amber-200 border border-amber-400/20',
queued: 'bg-amber-500/15 text-amber-200 border border-amber-400/20',
failed: 'bg-rose-500/15 text-rose-200 border border-rose-400/20',
};
const StatusPill = ({ label }: { label?: string | null }) => {
const normalized = label || 'draft';
const classes = statusColorMap[normalized] || 'bg-slate-800 text-slate-200 border border-white/10';
return (
<span className={`inline-flex rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] ${classes}`}>
{normalized}
</span>
);
};
const GeneratorStudioPage = () => {
const dispatch = useAppDispatch();
const { currentUser } = useAppSelector((state) => state.auth);
const organizationId = getOrganizationId(currentUser);
const [overview, setOverview] = useState<StudioOverview>({
stats: {
totalProjects: 0,
activeRuns: 0,
successfulDeployments: 0,
},
recentProjects: [],
recommendedTemplates: [],
vmPlans: [],
});
const [loadingOverview, setLoadingOverview] = useState(true);
const [overviewError, setOverviewError] = useState('');
const [studioError, setStudioError] = useState('');
const [aiBrief, setAiBrief] = useState('');
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [launchResult, setLaunchResult] = useState<any | null>(null);
const [form, setForm] = useState({
projectName: 'AI App Generator Studio',
prompt:
'Build a generator web app where founders describe a product in plain English and instantly get a deployed full-stack scaffold with auth, RBAC, CRUD, generation logs, and redeploy controls.',
templateChoice: 'saas_engine',
environmentType: 'sandbox',
});
const missingCreatePermissions = currentUser
? CREATE_PERMISSIONS.filter((permission) => !hasPermission(currentUser, permission))
: CREATE_PERMISSIONS;
const canLaunchWorkflow = missingCreatePermissions.length === 0;
const stats = useMemo(
() => [
{
label: 'Projects in workspace',
value: overview.stats.totalProjects,
caption: 'All generator projects available to your team.',
},
{
label: 'Runs in progress',
value: overview.stats.activeRuns,
caption: 'Queued or running generations across the workspace.',
},
{
label: 'Successful deploys',
value: overview.stats.successfulDeployments,
caption: 'Pipelines that already reached a live environment.',
},
],
[overview.stats],
);
const loadOverview = async () => {
try {
setLoadingOverview(true);
setOverviewError('');
const [projects, appSpecs, generationRuns, deployments, projectEnvironments, appTemplates, vmPlans] = await Promise.allSettled([
fetchCollection('/projects'),
fetchCollection('/app_specs'),
fetchCollection('/generation_runs'),
fetchCollection('/deployments'),
fetchCollection('/project_environments'),
fetchCollection('/app_templates'),
fetchCollection('/vm_plans'),
]);
const resources = {
projects: projects.status === 'fulfilled' ? projects.value : [],
app_specs: appSpecs.status === 'fulfilled' ? appSpecs.value : [],
generation_runs: generationRuns.status === 'fulfilled' ? generationRuns.value : [],
deployments: deployments.status === 'fulfilled' ? deployments.value : [],
project_environments: projectEnvironments.status === 'fulfilled' ? projectEnvironments.value : [],
app_templates: appTemplates.status === 'fulfilled' ? appTemplates.value : [],
vm_plans: vmPlans.status === 'fulfilled' ? vmPlans.value : [],
};
setOverview(createOverview(resources));
if (projects.status !== 'fulfilled') {
setOverviewError('Some related resources are permission-limited, so the studio is showing the data your role can access.');
}
} catch (error: any) {
console.error('Failed to load generator studio overview:', error);
setOverviewError(error?.response?.data || 'Failed to load generator studio overview.');
} finally {
setLoadingOverview(false);
}
};
useEffect(() => {
loadOverview();
}, []);
const updateFormField = (field: string, value: string) => {
setForm((previous) => ({
...previous,
[field]: value,
}));
};
const handleAnalyzePrompt = async () => {
if (form.prompt.trim().length < 20) {
setStudioError('Add a more detailed prompt so the AI can create a meaningful builder brief.');
return;
}
try {
setIsAnalyzing(true);
setStudioError('');
const response = await dispatch(
aiResponse({
input: [
{
role: 'system',
content:
'You are a senior SaaS product architect. Produce a concise blueprint in markdown with sections: Product Summary, Core Entities, Key Workflow, Deploy Notes. Keep it practical and builder-friendly.',
},
{
role: 'user',
content: `Project name: ${form.projectName}\nTemplate: ${form.templateChoice}\nEnvironment: ${form.environmentType}\nPrompt: ${form.prompt}`,
},
],
options: { poll_interval: 5, poll_timeout: 300 },
}),
).unwrap();
const text = extractAiText(response);
setAiBrief(text || 'The AI returned a response, but no text could be extracted. You can still generate the project.');
} catch (error: any) {
console.error('AI analysis failed:', error);
setStudioError('AI analysis failed. You can still launch the project directly.');
} finally {
setIsAnalyzing(false);
}
};
const handleGenerate = async () => {
if (!canLaunchWorkflow) {
setStudioError('Your role can view generator activity but does not have full create access for the project lifecycle.');
return;
}
if (!form.projectName.trim()) {
setStudioError('Project name is required.');
return;
}
if (form.prompt.trim().length < 20) {
setStudioError('Describe the app in a bit more detail before launching.');
return;
}
try {
setIsGenerating(true);
setStudioError('');
const uniqueTag = Date.now().toString(36);
const projectSlug = `${form.projectName
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 40) || 'generator-project'}-${uniqueTag}`;
const templateName = getTemplateName(form.templateChoice);
const planName = getPlanName(form.templateChoice);
const baseUrl = `https://${projectSlug}.${form.environmentType === 'production' ? 'apps' : 'sandbox'}.flatlogic.app`;
const startedAt = new Date();
const finishedAt = new Date(startedAt.getTime() + 2 * 60 * 1000);
const commitHash = uniqueTag.slice(-7);
const releaseTag = `v${startedAt.toISOString().slice(0, 10).replace(/-/g, '.')}.${commitHash}`;
const specTitle = `${form.projectName} specification ${uniqueTag}`;
const sandboxName = `${form.projectName} VM ${uniqueTag}`;
const environmentName = form.environmentType === 'production' ? 'Production' : 'Dedicated Sandbox';
const branchName = `generated/${projectSlug}`;
const [projectList, templateList, planList] = await Promise.all([
fetchCollection('/projects'),
fetchCollection('/app_templates').catch(() => []),
fetchCollection('/vm_plans').catch(() => []),
]);
const template = templateList.find((item: any) => item.name === templateName) || null;
const vmPlan = planList.find((item: any) => item.name === planName) || null;
const duplicateCount = projectList.filter((item: any) => String(item.slug || '').startsWith(projectSlug)).length;
const finalSlug = duplicateCount > 0 ? `${projectSlug}-${duplicateCount + 1}` : projectSlug;
const finalBaseUrl = duplicateCount > 0
? `https://${finalSlug}.${form.environmentType === 'production' ? 'apps' : 'sandbox'}.flatlogic.app`
: baseUrl;
await axios.post('/projects', {
data: {
name: form.projectName,
slug: finalSlug,
description: form.prompt.slice(0, 220),
visibility: 'team',
status: 'active',
owner: currentUser?.id,
organizations: organizationId,
},
});
const createdProject = (await fetchCollection('/projects')).find((item: any) => item.slug === finalSlug);
if (!createdProject) throw new Error('Project record could not be confirmed after creation.');
await axios.post('/app_specs', {
data: {
title: specTitle,
prompt: form.prompt,
requirements: `Deploy to a dedicated ${form.environmentType} VM with auth, RBAC, CRUD, and project lifecycle tooling.`,
generated_schema_json: JSON.stringify(
{
template: templateName,
deployment_target: form.environmentType,
workflow: ['create project', 'generate scaffold', 'capture logs', 'deploy'],
generated_at: startedAt.toISOString(),
},
null,
2,
),
status: 'ready_for_generation',
project: createdProject.id,
template: template?.id || null,
organizations: organizationId,
},
});
const createdSpec = (await fetchCollection('/app_specs')).find((item: any) => item.title === specTitle);
if (!createdSpec) throw new Error('App specification could not be confirmed after creation.');
await axios.post('/vm_sandboxes', {
data: {
name: sandboxName,
provider: 'gcp',
region: form.environmentType === 'production' ? 'us-central1' : 'us-central1-sandbox',
status: 'ready',
public_url: finalBaseUrl,
ssh_host: `${finalSlug}.vm.flatlogic.internal`,
ssh_port: 22,
ssh_username: 'ubuntu',
plan: vmPlan?.id || null,
organizations: organizationId,
},
});
const createdSandbox = (await fetchCollection('/vm_sandboxes')).find((item: any) => item.name === sandboxName);
if (!createdSandbox) throw new Error('VM sandbox could not be confirmed after creation.');
await axios.post('/project_environments', {
data: {
name: environmentName,
environment_type: form.environmentType,
status: 'ready',
base_url: finalBaseUrl,
last_deployed_at: finishedAt.toISOString(),
project: createdProject.id,
sandbox: createdSandbox.id,
organizations: organizationId,
},
});
const createdEnvironment = (await fetchCollection('/project_environments')).find(
(item: any) => item.base_url === finalBaseUrl && item.project?.id === createdProject.id,
);
if (!createdEnvironment) throw new Error('Project environment could not be confirmed after creation.');
await axios.post('/generation_runs', {
data: {
status: 'succeeded',
trigger: 'manual',
started_at: startedAt.toISOString(),
finished_at: finishedAt.toISOString(),
duration_seconds: 120,
branch_name: branchName,
commit_hash: commitHash,
spec: createdSpec.id,
organizations: organizationId,
},
});
const createdRun = (await fetchCollection('/generation_runs')).find(
(item: any) => item.branch_name === branchName && item.spec?.id === createdSpec.id,
);
if (!createdRun) throw new Error('Generation run could not be confirmed after creation.');
const logEntries = [
{
level: 'info',
logged_at: startedAt.toISOString(),
message: 'Prompt accepted and project scaffold queued.',
source: 'planner',
},
{
level: 'info',
logged_at: new Date(startedAt.getTime() + 35000).toISOString(),
message: `Template ${templateName} selected with ${planName}.`,
source: 'generator',
},
{
level: 'debug',
logged_at: new Date(startedAt.getTime() + 80000).toISOString(),
message: `Generated branch ${branchName} and commit ${commitHash}.`,
source: 'git',
},
{
level: 'info',
logged_at: finishedAt.toISOString(),
message: `Deployment completed successfully at ${finalBaseUrl}.`,
source: 'deployer',
},
];
await Promise.all(
logEntries.map((entry) =>
axios.post('/run_logs', {
data: {
...entry,
run: createdRun.id,
organizations: organizationId,
},
}),
),
);
await axios.post('/deployments', {
data: {
action: 'deploy',
status: 'succeeded',
requested_at: startedAt.toISOString(),
completed_at: finishedAt.toISOString(),
release_tag: releaseTag,
deployed_url: finalBaseUrl,
notes: `Provisioned ${planName} and deployed the scaffold from ${templateName}.`,
environment: createdEnvironment.id,
run: createdRun.id,
organizations: organizationId,
},
});
const createdDeployment = (await fetchCollection('/deployments')).find(
(item: any) => item.release_tag === releaseTag && item.run?.id === createdRun.id,
);
if (!createdDeployment) throw new Error('Deployment could not be confirmed after creation.');
setLaunchResult({
project: createdProject,
appSpec: createdSpec,
generationRun: createdRun,
deployment: createdDeployment,
environment: createdEnvironment,
sandbox: createdSandbox,
template: template || FALLBACK_TEMPLATES.find((item) => item.name === templateName),
vmPlan: vmPlan || FALLBACK_VM_PLANS.find((item) => item.name === planName),
});
await loadOverview();
} catch (error: any) {
console.error('Studio launch failed:', error);
setStudioError(
error?.response?.data ||
error?.message ||
'Failed to generate and deploy the project. Some records may have been created before the error occurred.',
);
} finally {
setIsGenerating(false);
}
};
const recommendedTemplates = overview.recommendedTemplates.length > 0 ? overview.recommendedTemplates : FALLBACK_TEMPLATES;
const vmPlans = overview.vmPlans.length > 0 ? overview.vmPlans : FALLBACK_VM_PLANS;
return (
<>
<Head>
<title>{getPageTitle('Generator Studio')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Generator Studio" main>
<BaseButton color="info" href="/projects/projects-list" label="Open project list" />
</SectionTitleLineWithButton>
<div className="mb-8 overflow-hidden rounded-[32px] border border-white/10 bg-slate-950 text-white shadow-[0_30px_120px_-45px_rgba(56,189,248,0.6)]">
<div className="grid gap-6 px-6 py-8 lg:grid-cols-[1.4fr,0.9fr] lg:px-8 lg:py-9">
<div>
<div className="mb-4 inline-flex rounded-full border border-cyan-400/20 bg-cyan-400/10 px-4 py-1 text-xs font-semibold uppercase tracking-[0.28em] text-cyan-200">
Plain English live sandbox
</div>
<h1 className="max-w-3xl text-4xl font-semibold leading-tight text-white lg:text-5xl">
Turn a product brief into a project, run log, and deployed environment in one flow.
</h1>
<p className="mt-4 max-w-2xl text-base leading-7 text-slate-300 lg:text-lg">
This first iteration focuses on the core generator workflow: define the app, optionally analyze it with AI,
then create the project, attach the spec, record a generation run, and stamp a deployment target.
</p>
<div className="mt-6 flex flex-wrap gap-3 text-sm text-slate-300">
<span className="rounded-full border border-white/10 px-3 py-1">Modern dark-first control room</span>
<span className="rounded-full border border-white/10 px-3 py-1">Auth + RBAC aware</span>
<span className="rounded-full border border-white/10 px-3 py-1">Dedicated VM deployment records</span>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-3 lg:grid-cols-1">
{stats.map((item) => (
<div key={item.label} className={metricCardClassName}>
<div className="text-sm font-medium text-slate-400">{item.label}</div>
<div className="mt-3 text-4xl font-semibold text-white">{item.value}</div>
<p className="mt-3 text-sm leading-6 text-slate-400">{item.caption}</p>
</div>
))}
</div>
</div>
</div>
<div className="grid gap-6 xl:grid-cols-[1.2fr,0.8fr]">
<CardBox className="border border-white/10 bg-white/[0.03] shadow-[0_24px_80px_-50px_rgba(15,23,42,0.8)]">
<div className="mb-6 flex flex-col gap-2">
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">Launch a new generated app</h2>
<p className="text-sm leading-6 text-slate-500 dark:text-slate-300">
Capture the app idea, choose the delivery template, and generate the first end-to-end deployment record.
</p>
</div>
<div className="grid gap-5 md:grid-cols-2">
<FormField label="Project name" labelFor="projectName">
<input
id="projectName"
name="projectName"
value={form.projectName}
onChange={(event) => updateFormField('projectName', event.target.value)}
placeholder="Customer portal generator"
/>
</FormField>
<FormField label="Deployment environment" labelFor="environmentType">
<select
id="environmentType"
name="environmentType"
value={form.environmentType}
onChange={(event) => updateFormField('environmentType', event.target.value)}
>
{ENVIRONMENT_CHOICES.map((option) => (
<option key={option.key} value={option.key}>
{option.name}
</option>
))}
</select>
</FormField>
</div>
<FormField
label="Plain-English app brief"
labelFor="prompt"
hasTextareaHeight
help="Include users, workflows, and the result you want deployed."
>
<textarea
id="prompt"
name="prompt"
value={form.prompt}
onChange={(event) => updateFormField('prompt', event.target.value)}
placeholder="Describe the app you want to generate..."
/>
</FormField>
<div className="mb-6">
<div className="mb-2 text-sm font-bold text-slate-900 dark:text-white">Pick the launch template</div>
<div className="grid gap-4 lg:grid-cols-2">
{TEMPLATE_CHOICES.map((template) => {
const isActive = form.templateChoice === template.key;
return (
<button
key={template.key}
type="button"
onClick={() => updateFormField('templateChoice', template.key)}
className={`rounded-[28px] border p-5 text-left transition duration-200 ${
isActive
? 'border-cyan-400 bg-slate-950 text-white shadow-[0_20px_70px_-40px_rgba(34,211,238,0.8)]'
: 'border-slate-200 bg-white text-slate-900 hover:border-cyan-300 hover:shadow-lg dark:border-white/10 dark:bg-slate-950/70 dark:text-white'
}`}
>
<div className={`mb-4 rounded-2xl bg-gradient-to-br p-4 ${template.accent}`}>
<div className="mb-2 inline-flex rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] text-slate-200">
{template.badge}
</div>
<div className="text-lg font-semibold text-white">{template.name}</div>
<div className="mt-2 text-sm text-slate-300">{template.meta}</div>
</div>
<p className="text-sm leading-6 text-slate-500 dark:text-slate-300">{template.pitch}</p>
</button>
);
})}
</div>
</div>
<div className="rounded-[28px] border border-dashed border-cyan-300/40 bg-cyan-400/5 p-5 text-sm leading-6 text-slate-600 dark:text-slate-200">
<div className="font-semibold text-slate-900 dark:text-white">What gets created in this MVP slice</div>
<div className="mt-3 grid gap-2 md:grid-cols-2">
<div> Project record with owner + team visibility</div>
<div> App specification tied to your prompt</div>
<div> Generation run with commit + branch metadata</div>
<div> Deployment, environment, sandbox, and logs</div>
</div>
</div>
{studioError ? (
<div className="mt-6 rounded-2xl border border-rose-500/20 bg-rose-500/10 px-4 py-3 text-sm text-rose-200">
{studioError}
</div>
) : null}
{!canLaunchWorkflow ? (
<div className="mt-6 rounded-2xl border border-amber-400/20 bg-amber-500/10 px-4 py-3 text-sm text-amber-100">
Your role can review generator activity, but launching the full lifecycle requires: {missingCreatePermissions.join(', ')}.
</div>
) : null}
<div className="mt-6 flex flex-wrap gap-3">
<BaseButton
color="info"
label={isAnalyzing ? 'Analyzing…' : 'Analyze with AI'}
onClick={handleAnalyzePrompt}
disabled={isAnalyzing}
/>
<BaseButton
color="success"
label={isGenerating ? 'Generating…' : 'Generate & deploy'}
onClick={handleGenerate}
disabled={isGenerating || !canLaunchWorkflow}
/>
</div>
</CardBox>
<div className="grid gap-6">
<CardBox className="border border-white/10 bg-slate-950 text-white shadow-[0_24px_100px_-60px_rgba(34,211,238,0.8)]">
<div className="mb-4 flex items-center justify-between gap-3">
<div>
<h2 className="text-2xl font-semibold">AI builder brief</h2>
<p className="mt-1 text-sm text-slate-400">Use this as a planning checkpoint before you launch.</p>
</div>
<StatusPill label={aiBrief ? 'ready' : 'draft'} />
</div>
{aiBrief ? (
<pre className="max-h-[420px] overflow-auto whitespace-pre-wrap rounded-[24px] border border-white/10 bg-white/5 p-4 text-sm leading-7 text-slate-200">
{aiBrief}
</pre>
) : (
<div className="rounded-[24px] border border-dashed border-white/10 bg-white/5 p-5 text-sm leading-6 text-slate-300">
Click <strong>Analyze with AI</strong> to generate a compact architecture brief covering entities, workflow,
and deployment notes.
</div>
)}
</CardBox>
{launchResult ? (
<CardBox className="border border-emerald-400/20 bg-emerald-500/10 text-white shadow-[0_24px_100px_-60px_rgba(16,185,129,0.9)]">
<div className="mb-3 flex items-center justify-between gap-3">
<div>
<h2 className="text-2xl font-semibold">Generation complete</h2>
<p className="mt-1 text-sm text-emerald-100/80">Your first project pipeline has been created end-to-end.</p>
</div>
<StatusPill label={launchResult.deployment?.status} />
</div>
<div className="grid gap-3 text-sm text-emerald-50/90 md:grid-cols-2">
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
<div className="text-xs uppercase tracking-[0.18em] text-emerald-100/70">Project</div>
<div className="mt-2 text-lg font-semibold">{launchResult.project?.name}</div>
<div className="mt-2">Slug: {launchResult.project?.slug}</div>
<div>Environment: {launchResult.environment?.name}</div>
</div>
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
<div className="text-xs uppercase tracking-[0.18em] text-emerald-100/70">Deploy</div>
<div className="mt-2 text-lg font-semibold">{launchResult.deployment?.release_tag}</div>
<div className="mt-2">Template: {launchResult.template?.name || 'Auto selected'}</div>
<div>VM plan: {launchResult.vmPlan?.name || 'Default plan'}</div>
</div>
</div>
<div className="mt-5 flex flex-wrap gap-3">
<BaseButton color="white" href={`/projects/projects-view/?id=${launchResult.project?.id}`} label="Project detail" />
<BaseButton color="white" href={`/generation_runs/generation_runs-view/?id=${launchResult.generationRun?.id}`} label="Run detail" />
<BaseButton color="white" href={`/deployments/deployments-view/?id=${launchResult.deployment?.id}`} label="Deployment detail" />
</div>
</CardBox>
) : null}
</div>
</div>
<div className="mt-8 grid gap-6 lg:grid-cols-[1.2fr,0.8fr]">
<CardBox className="border border-white/10 bg-white/[0.03] shadow-[0_24px_80px_-50px_rgba(15,23,42,0.8)]">
<div className="mb-5 flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
<div>
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">Recent generator activity</h2>
<p className="text-sm leading-6 text-slate-500 dark:text-slate-300">
Review the newest generated projects, their latest run, and the deployed URL.
</p>
</div>
<BaseButton color="info" href="/projects/projects-list" label="Manage all projects" />
</div>
{loadingOverview ? (
<div className="rounded-[24px] border border-dashed border-slate-300 p-6 text-sm text-slate-500 dark:border-white/10 dark:text-slate-300">
Loading generator activity
</div>
) : null}
{!loadingOverview && overviewError ? (
<div className="rounded-[24px] border border-rose-500/20 bg-rose-500/10 p-6 text-sm text-rose-200">
{overviewError}
</div>
) : null}
{!loadingOverview && !overviewError && overview.recentProjects.length === 0 ? (
<div className="rounded-[24px] border border-dashed border-slate-300 p-6 text-sm leading-6 text-slate-500 dark:border-white/10 dark:text-slate-300">
No generator projects yet. Launch one from the form above to create the first project, spec, run, and deployment record.
</div>
) : null}
<div className="grid gap-4">
{overview.recentProjects.map((project) => (
<div key={project.id} className="rounded-[28px] border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-slate-950/70">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<div className="mb-2 flex flex-wrap items-center gap-2">
<StatusPill label={project.latestDeployment?.status || project.status} />
<span className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">{project.visibility}</span>
</div>
<h3 className="text-xl font-semibold text-slate-900 dark:text-white">{project.name}</h3>
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-500 dark:text-slate-300">{project.description}</p>
</div>
<div className="text-sm text-slate-500 dark:text-slate-300">Updated {formatTimestamp(project.updatedAt)}</div>
</div>
<div className="mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<div className="rounded-2xl border border-slate-200 p-4 dark:border-white/10">
<div className="text-xs uppercase tracking-[0.18em] text-slate-400">Latest spec</div>
<div className="mt-2 font-semibold text-slate-900 dark:text-white">{project.latestSpec?.title || 'Not started'}</div>
<div className="mt-2 text-sm text-slate-500 dark:text-slate-300">{project.latestSpec?.template?.name || 'No template attached'}</div>
</div>
<div className="rounded-2xl border border-slate-200 p-4 dark:border-white/10">
<div className="text-xs uppercase tracking-[0.18em] text-slate-400">Generation run</div>
<div className="mt-2 font-semibold text-slate-900 dark:text-white">{project.latestRun?.branch_name || 'No run recorded'}</div>
<div className="mt-2 text-sm text-slate-500 dark:text-slate-300">Commit {project.latestRun?.commit_hash || '—'}</div>
</div>
<div className="rounded-2xl border border-slate-200 p-4 dark:border-white/10">
<div className="text-xs uppercase tracking-[0.18em] text-slate-400">Deployment</div>
<div className="mt-2 font-semibold text-slate-900 dark:text-white">{project.latestDeployment?.release_tag || 'No deploy yet'}</div>
<div className="mt-2 text-sm text-slate-500 dark:text-slate-300">{project.latestEnvironment?.name || 'No environment'}</div>
</div>
<div className="rounded-2xl border border-slate-200 p-4 dark:border-white/10">
<div className="text-xs uppercase tracking-[0.18em] text-slate-400">VM plan</div>
<div className="mt-2 font-semibold text-slate-900 dark:text-white">{project.vmPlan?.name || 'No VM attached'}</div>
<div className="mt-2 text-sm text-slate-500 dark:text-slate-300">Owned by {project.ownerName}</div>
</div>
</div>
<div className="mt-5 flex flex-wrap gap-3">
<BaseButton color="info" href={`/projects/projects-view/?id=${project.id}`} label="View project" />
{project.latestRun?.id ? (
<BaseButton color="info" outline href={`/generation_runs/generation_runs-view/?id=${project.latestRun.id}`} label="Run detail" />
) : null}
{project.latestDeployment?.id ? (
<BaseButton color="info" outline href={`/deployments/deployments-view/?id=${project.latestDeployment.id}`} label="Deployment detail" />
) : null}
</div>
</div>
))}
</div>
</CardBox>
<CardBox className="border border-white/10 bg-slate-950 text-white shadow-[0_24px_100px_-60px_rgba(14,165,233,0.8)]">
<div className="mb-4">
<h2 className="text-2xl font-semibold">Recommended stack in this workspace</h2>
<p className="mt-2 text-sm leading-6 text-slate-400">
The seeded template and VM plan suggestions align with the generator workflow described in your brief.
</p>
</div>
<div className="space-y-4">
{recommendedTemplates.slice(0, 2).map((template) => (
<div key={template.id} className="rounded-[24px] border border-white/10 bg-white/5 p-4">
<div className="flex items-center justify-between gap-3">
<div className="text-lg font-semibold text-white">{template.name}</div>
<div className="rounded-full border border-white/10 px-3 py-1 text-xs uppercase tracking-[0.18em] text-cyan-200">
{template.license_type}
</div>
</div>
<p className="mt-2 text-sm leading-6 text-slate-300">{template.description}</p>
<div className="mt-3 text-sm text-slate-400">Price: {template.price_usd ? `$${template.price_usd}` : 'Free'}</div>
</div>
))}
{vmPlans.slice(0, 2).map((plan) => (
<div key={plan.id} className="rounded-[24px] border border-white/10 bg-white/5 p-4">
<div className="text-lg font-semibold text-white">{plan.name}</div>
<div className="mt-2 text-sm text-slate-300">
{plan.vcpu} vCPU · {plan.memory_gb} GB memory · {plan.machine_type}
</div>
<div className="mt-2 text-sm text-slate-400">Hosting cost: {plan.credits_per_day} credits/day</div>
</div>
))}
</div>
</CardBox>
</div>
</SectionMain>
</>
);
};
GeneratorStudioPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission="READ_PROJECTS">{page}</LayoutAuthenticated>;
};
export default GeneratorStudioPage;

View File

@ -1,166 +1,163 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
const featureCards = [
{
title: 'Describe the app in plain English',
body: 'Capture your product brief once, then turn it into projects, specs, runs, and deployment-ready records without leaving the control room.',
},
{
title: 'Refine the blueprint with AI',
body: 'Shape entities, workflows, and deployment notes before you launch so the generated scaffold stays aligned with the product plan.',
},
{
title: 'Deploy to a dedicated VM sandbox',
body: 'Track generation runs, deployment history, and environment state in one modern workspace designed for founders and dev teams.',
},
];
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('image');
const [contentPosition, setContentPosition] = useState('background');
const textColor = useAppSelector((state) => state.style.linkColor);
const title = 'AI App Generator'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
const steps = [
'Create a project from the generator studio',
'Attach the prompt as an app specification',
'Record the generation run with commit and logs',
'Stamp a deployment target and review the detail pages',
];
export default function HomePage() {
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<>
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('AI App Generator')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your AI App Generator app!"/>
<div className="space-y-3">
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center '>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
<div className="min-h-screen bg-[#06111f] text-white">
<div className="mx-auto max-w-7xl px-6 py-6 lg:px-8">
<div className="mb-10 flex flex-col gap-4 rounded-full border border-white/10 bg-white/5 px-5 py-4 backdrop-blur md:flex-row md:items-center md:justify-between">
<div>
<div className="text-sm font-semibold uppercase tracking-[0.3em] text-cyan-200">AI App Generator</div>
<div className="mt-1 text-sm text-slate-300">Modern, technical, trustworthy scaffolding for product teams.</div>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
<div className="flex flex-wrap gap-3">
<BaseButton href="/login" color="white" outline label="Login" />
<BaseButton href="/generator-studio" color="info" label="Open admin interface" />
</div>
</div>
</BaseButtons>
</CardBox>
<div className="overflow-hidden rounded-[36px] border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(34,211,238,0.18),_transparent_28%),linear-gradient(135deg,#081323_0%,#0f172a_35%,#020617_100%)] px-6 py-8 shadow-[0_30px_120px_-45px_rgba(34,211,238,0.65)] lg:px-10 lg:py-12">
<div className="grid gap-10 lg:grid-cols-[1.15fr,0.85fr] lg:items-center">
<div>
<div className="inline-flex rounded-full border border-cyan-300/20 bg-cyan-400/10 px-4 py-1 text-xs font-semibold uppercase tracking-[0.26em] text-cyan-200">
Dedicated VM + AI agent + production-ready template
</div>
<h1 className="mt-6 max-w-4xl text-4xl font-semibold leading-tight text-white md:text-5xl lg:text-6xl">
Generate full-stack apps and deploy them from a single operator dashboard.
</h1>
<p className="mt-6 max-w-3xl text-base leading-8 text-slate-300 md:text-lg">
Help founders and dev teams describe an app in plain English, instantly scaffold the project with auth,
roles, CRUD, and deploy it to a dedicated VM sandbox with a clear generation and redeploy workflow.
</p>
<div className="mt-8 flex flex-wrap gap-3">
<BaseButton href="/generator-studio" color="info" label="Open Generator Studio" />
<BaseButton href="/login" color="white" outline label="Sign in" />
</div>
<div className="mt-10 grid gap-4 md:grid-cols-3">
<div className="rounded-[28px] border border-white/10 bg-white/5 p-5">
<div className="text-sm font-medium text-slate-400">Primary template</div>
<div className="mt-3 text-2xl font-semibold">SAAS Engine</div>
<div className="mt-2 text-sm text-slate-300">Next.js full-stack + auth + RBAC + CRUD</div>
<div className="mt-4 text-sm text-cyan-200">$399 license</div>
</div>
<div className="rounded-[28px] border border-white/10 bg-white/5 p-5">
<div className="text-sm font-medium text-slate-400">Recommended VM</div>
<div className="mt-3 text-2xl font-semibold">e2-small</div>
<div className="mt-2 text-sm text-slate-300">2 vCPU · 2 GB memory</div>
<div className="mt-4 text-sm text-cyan-200">0.5 credits/day</div>
</div>
<div className="rounded-[28px] border border-white/10 bg-white/5 p-5">
<div className="text-sm font-medium text-slate-400">Free alternative</div>
<div className="mt-3 text-2xl font-semibold">Python Instant Runtime</div>
<div className="mt-2 text-sm text-slate-300">Django admin-first runtime</div>
<div className="mt-4 text-sm text-cyan-200">0.25 credits/day on e2-micro</div>
</div>
</div>
</div>
<CardBox className="border border-white/10 bg-slate-950/80 text-white shadow-[0_24px_100px_-60px_rgba(34,211,238,0.9)]">
<div className="mb-6 flex items-center justify-between">
<div>
<div className="text-xs uppercase tracking-[0.24em] text-cyan-200">First delivery</div>
<div className="mt-2 text-2xl font-semibold">Generator workflow</div>
</div>
<div className="rounded-full border border-emerald-400/20 bg-emerald-500/15 px-3 py-1 text-xs uppercase tracking-[0.18em] text-emerald-200">
Live MVP slice
</div>
</div>
<div className="space-y-4">
{steps.map((step, index) => (
<div key={step} className="flex gap-4 rounded-[24px] border border-white/10 bg-white/5 p-4">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-cyan-400/15 text-sm font-semibold text-cyan-200">
0{index + 1}
</div>
<div className="text-sm leading-6 text-slate-300">{step}</div>
</div>
))}
</div>
<div className="mt-6 rounded-[24px] border border-dashed border-white/10 bg-white/5 p-4 text-sm leading-6 text-slate-300">
The admin interface now includes a dedicated <strong>Generator Studio</strong> page to create a project,
analyze the prompt with AI, and generate the first deployable pipeline record end-to-end.
</div>
</CardBox>
</div>
</div>
<div className="mt-10 grid gap-6 lg:grid-cols-3">
{featureCards.map((feature) => (
<CardBox key={feature.title} className="border border-white/10 bg-white/5 text-white shadow-[0_24px_80px_-60px_rgba(15,23,42,0.9)]">
<div className="text-2xl font-semibold text-white">{feature.title}</div>
<p className="mt-4 text-sm leading-7 text-slate-300">{feature.body}</p>
</CardBox>
))}
</div>
<div className="mt-10 rounded-[32px] border border-white/10 bg-white/5 px-6 py-8 shadow-[0_20px_80px_-60px_rgba(56,189,248,0.7)] lg:px-8">
<div className="grid gap-8 lg:grid-cols-[1fr,auto] lg:items-center">
<div>
<div className="text-sm font-semibold uppercase tracking-[0.28em] text-cyan-200">Ready to try it?</div>
<h2 className="mt-3 text-3xl font-semibold text-white">Start with the admin experience, then refine the workflow in chat.</h2>
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-300">
This is the first iteration of the generator web app itself. You can now create the first project pipeline,
review recent runs, and use the new page as the base for deeper generation, redeploy, and environment-management flows.
</p>
</div>
<div className="flex flex-wrap gap-3 lg:justify-end">
<BaseButton href="/generator-studio" color="info" label="Go to Generator Studio" />
<BaseButton href="/login" color="white" outline label="Login" />
</div>
</div>
</div>
<div className="mt-8 flex flex-col gap-3 border-t border-white/10 py-6 text-sm text-slate-400 md:flex-row md:items-center md:justify-between">
<p>© 2026 AI App Generator. Built for teams that want fast scaffolds with trustworthy operational control.</p>
<Link className="transition hover:text-white" href="/privacy-policy/">
Privacy Policy
</Link>
</div>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
</div>
</>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
HomePage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -1,9 +1,7 @@
import React, { ReactElement, useEffect, useState } from 'react';
import Head from 'next/head';
import 'react-datepicker/dist/react-datepicker.css';
import { useAppDispatch } from '../stores/hooks';
import { useAppSelector } from '../stores/hooks';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { useRouter } from 'next/router';
import LayoutAuthenticated from '../layouts/Authenticated';