fix: change 'layout' to 'layout'
This commit is contained in:
parent
3b49264b24
commit
36bbefa5aa
39 changed files with 224 additions and 67 deletions
|
|
@ -0,0 +1,18 @@
|
|||
import React from "react";
|
||||
import Header from "@/components/Header";
|
||||
|
||||
export const OpenAIKeyWarning: React.FC = () => {
|
||||
return (
|
||||
<div className="min-h-screen font-roboto bg-gradient-to-br from-slate-50 to-slate-100">
|
||||
<Header />
|
||||
<div className="flex items-center justify-center aspect-video mx-auto px-6">
|
||||
<div className="text-center space-y-2 my-6 bg-white p-10 rounded-lg shadow-lg">
|
||||
<h1 className="text-xl font-bold text-gray-900">Please add your OpenAI API Key to process the layout</h1>
|
||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||
This feature requires an OpenAI model GPT-5. Configure your key in settings or via environment variables.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -25,12 +25,12 @@ export const SaveLayoutButton: React.FC<SaveLayoutButtonProps> = ({
|
|||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
|
||||
Saving Layout...
|
||||
Saving Template...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="w-5 h-5 mr-2" />
|
||||
Save Layout
|
||||
Save Template
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
@ -53,22 +53,22 @@ export const SaveLayoutModal: React.FC<SaveLayoutModalProps> = ({
|
|||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Save className="w-5 h-5 text-green-600" />
|
||||
Save Layout
|
||||
Save Template
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter a name and description for your layout. This will help you identify it later.
|
||||
Enter a name and description for your template. This will help you identify it later.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="layout-name" className="text-sm font-medium">
|
||||
Layout Name *
|
||||
Template Name *
|
||||
</Label>
|
||||
<Input
|
||||
id="layout-name"
|
||||
value={layoutName}
|
||||
onChange={(e) => setLayoutName(e.target.value)}
|
||||
placeholder="Enter layout name..."
|
||||
placeholder="Enter template name..."
|
||||
disabled={isSaving}
|
||||
className="w-full"
|
||||
/>
|
||||
|
|
@ -81,7 +81,7 @@ export const SaveLayoutModal: React.FC<SaveLayoutModalProps> = ({
|
|||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Enter a description for your layout..."
|
||||
placeholder="Enter a description for your template..."
|
||||
disabled={isSaving}
|
||||
className="w-full resize-none"
|
||||
rows={3}
|
||||
|
|
@ -109,7 +109,7 @@ export const SaveLayoutModal: React.FC<SaveLayoutModalProps> = ({
|
|||
) : (
|
||||
<>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Save Layout
|
||||
Save Template
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
export const useOpenAIKeyCheck = () => {
|
||||
const [hasOpenAIKey, setHasOpenAIKey] = useState(false);
|
||||
const [isOpenAIKeyLoading, setIsOpenAIKeyLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/has-openai-key")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setHasOpenAIKey(Boolean(data.hasKey));
|
||||
setIsOpenAIKeyLoading(false);
|
||||
})
|
||||
.catch(() => setIsOpenAIKeyLoading(false));
|
||||
}, []);
|
||||
|
||||
return { hasOpenAIKey, isOpenAIKeyLoading };
|
||||
};
|
||||
|
|
@ -125,7 +125,7 @@ const CustomLayoutPage = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Floating Save Layout Button */}
|
||||
{/* Floating Save Template Button */}
|
||||
{slides.length > 0 && slides.some((s) => s.processed) && (
|
||||
<SaveLayoutButton
|
||||
onSave={openSaveModal}
|
||||
|
|
@ -134,7 +134,7 @@ const CustomLayoutPage = () => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Save Layout Modal */}
|
||||
{/* Save Template Modal */}
|
||||
<SaveLayoutModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={closeSaveModal}
|
||||
|
|
@ -25,13 +25,13 @@ const Header = () => {
|
|||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/layout-preview"
|
||||
href="/template-preview"
|
||||
prefetch={false}
|
||||
className="flex items-center gap-2 px-3 py-2 text-white hover:bg-primary/80 rounded-md transition-colors outline-none"
|
||||
role="menuitem"
|
||||
>
|
||||
<Layout className="w-5 h-5" />
|
||||
<span className="text-sm font-medium font-inter">Layouts</span>
|
||||
<span className="text-sm font-medium font-inter">Templates</span>
|
||||
</Link>
|
||||
<HeaderNav />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const GenerateButton: React.FC<GenerateButtonProps> = ({
|
|||
const getButtonText = () => {
|
||||
if (loadingState.isLoading) return loadingState.message;
|
||||
if (streamState.isLoading || streamState.isStreaming) return "Loading...";
|
||||
if (!selectedLayoutGroup) return "Select a Layout Style";
|
||||
if (!selectedLayoutGroup) return "Select a Templae";
|
||||
return "Generate Presentation";
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -82,10 +82,10 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
|
|||
<div className="space-y-6">
|
||||
<div className="text-center py-8">
|
||||
<h5 className="text-lg font-medium mb-2 text-gray-700">
|
||||
No Layout Styles Available
|
||||
No Templates Available
|
||||
</h5>
|
||||
<p className="text-gray-600 text-sm">
|
||||
No presentation layout styles could be loaded. Please try refreshing the page.
|
||||
No presentation templates could be loaded. Please try refreshing the page.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const OutlinePage: React.FC = () => {
|
|||
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
|
||||
<TabsList className="grid w-[50%] mx-auto my-4 grid-cols-2">
|
||||
<TabsTrigger value={TABS.OUTLINE}>Outline & Content</TabsTrigger>
|
||||
<TabsTrigger value={TABS.LAYOUTS}>Layout Style</TabsTrigger>
|
||||
<TabsTrigger value={TABS.LAYOUTS}>Select Template</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex-grow w-full overflow-y-auto custom_scrollbar">
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const GroupLayoutPreview = () => {
|
|||
method: "DELETE",
|
||||
});
|
||||
if (response.ok) {
|
||||
router.push("/layout-preview");
|
||||
router.push("/template-preview");
|
||||
}
|
||||
}
|
||||
return (
|
||||
|
|
@ -66,7 +66,7 @@ const GroupLayoutPreview = () => {
|
|||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push("/layout-preview")}
|
||||
onClick={() => router.push("/template-preview")}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
|
|
@ -23,9 +23,9 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const response = await fetch('/api/layouts')
|
||||
const response = await fetch('/api/templates')
|
||||
if (!response.ok) {
|
||||
toast.error('Error loading layouts', {
|
||||
toast.error('Error loading templates', {
|
||||
description: response.statusText,
|
||||
})
|
||||
return
|
||||
|
|
@ -38,7 +38,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
const groupLayouts: LayoutInfo[] = []
|
||||
|
||||
const groupSettings: GroupSetting = groupData.settings ? groupData.settings : {
|
||||
description: `${groupData.groupName} presentation layouts`,
|
||||
description: `${groupData.groupName} presentation templates`,
|
||||
ordered: false,
|
||||
default: false
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
|
||||
if (!module.default) {
|
||||
toast.error(`${layoutName} has no default export`, {
|
||||
description: 'Please ensure the layout file exports a default component',
|
||||
description: 'Please ensure the template file exports a default component',
|
||||
})
|
||||
console.warn(`${layoutName} has no default export`)
|
||||
throw new Error(`${layoutName} has no default export`)
|
||||
|
|
@ -58,14 +58,12 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
|
||||
if (!module.Schema) {
|
||||
toast.error(`${layoutName} is missing required Schema export`, {
|
||||
description: 'Please ensure the layout file exports a Schema',
|
||||
description: 'Please ensure the template file exports a Schema',
|
||||
})
|
||||
console.error(`${layoutName} is missing required Schema export`)
|
||||
throw new Error(`${layoutName} is missing required Schema export`)
|
||||
}
|
||||
|
||||
// Use empty object to let schema apply its default values
|
||||
// User will need to provide actual data when using the layouts
|
||||
const sampleData = module.Schema.parse({})
|
||||
const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '')
|
||||
|
||||
|
|
@ -85,15 +83,12 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
} catch (importError) {
|
||||
console.error(`Failed to import ${fileName} from ${groupData.groupName}:`, importError)
|
||||
|
||||
// Try alternative import path
|
||||
try {
|
||||
const layoutName = fileName.replace('.tsx', '').replace('.ts', '')
|
||||
const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`)
|
||||
|
||||
if (module.default && module.Schema) {
|
||||
// Use empty object to let schema apply its default values
|
||||
const sampleData = module.Schema.parse({})
|
||||
// if layoutId is not provided, use the layoutName
|
||||
const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '')
|
||||
|
||||
const layoutInfo: LayoutInfo = {
|
||||
|
|
@ -108,7 +103,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
groupLayouts.push(layoutInfo)
|
||||
allLayouts.push(layoutInfo)
|
||||
} else {
|
||||
console.error(`${layoutName} is missing required exports (default component or Schema)`)
|
||||
console.error(`${layoutName} is missing required exports (default component or Schema)`)
|
||||
}
|
||||
} catch (altError) {
|
||||
console.error(`Alternative import also failed for ${fileName} from ${groupData.groupName}:`, altError)
|
||||
|
|
@ -126,10 +121,10 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
}
|
||||
|
||||
if (allLayouts.length === 0) {
|
||||
toast.error('No valid layouts found', {
|
||||
description: 'Make sure your layout files export both a default component and a Schema.',
|
||||
toast.error('No valid templates found', {
|
||||
description: 'Make sure your template files export both a default component and a Schema.',
|
||||
})
|
||||
setError('No valid layouts found. Make sure your layout files export both a default component and a Schema.')
|
||||
setError('No valid templates found. Make sure your template files export both a default component and a Schema.')
|
||||
} else {
|
||||
setLayoutGroups(loadedGroups)
|
||||
setLayouts(allLayouts)
|
||||
|
|
@ -137,8 +132,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading layouts:', error)
|
||||
setError(error instanceof Error ? error.message : 'Failed to load layouts')
|
||||
console.error('Error loading templates:', error)
|
||||
setError(error instanceof Error ? error.message : 'Failed to load templates')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ const LayoutPreview = () => {
|
|||
key={group.groupName}
|
||||
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
|
||||
onClick={() =>
|
||||
router.push(`/layout-preview/${group.groupName}`)
|
||||
router.push(`/template-preview/${group.groupName}`)
|
||||
}
|
||||
>
|
||||
<div className="p-6">
|
||||
25
servers/nextjs/app/api/has-openai-key/route.ts
Normal file
25
servers/nextjs/app/api/has-openai-key/route.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import fs from "fs";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
const userConfigPath = process.env.USER_CONFIG_PATH;
|
||||
|
||||
let keyFromFile = "";
|
||||
if (userConfigPath && fs.existsSync(userConfigPath)) {
|
||||
try {
|
||||
const raw = fs.readFileSync(userConfigPath, "utf-8");
|
||||
const cfg = JSON.parse(raw || "{}");
|
||||
keyFromFile = cfg?.OPENAI_API_KEY || "";
|
||||
} catch {}
|
||||
}
|
||||
|
||||
console.log(keyFromFile);
|
||||
|
||||
const keyFromEnv = process.env.OPENAI_API_KEY || "";
|
||||
console.log(keyFromEnv);
|
||||
const hasKey = Boolean((keyFromFile || keyFromEnv).trim());
|
||||
|
||||
return NextResponse.json({ hasKey });
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { GroupSetting } from '@/app/(presentation-generator)/layout-preview/types'
|
||||
import { GroupSetting } from '@/app/(presentation-generator)/template-preview/types'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
|
|
|
|||
60
servers/nextjs/app/api/template/route.ts
Normal file
60
servers/nextjs/app/api/template/route.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import puppeteer from "puppeteer";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const groupName = searchParams.get("group");
|
||||
|
||||
if (!groupName) {
|
||||
return NextResponse.json({ error: "Missing group name" }, { status: 400 });
|
||||
}
|
||||
|
||||
const schemaPageUrl = `http://localhost/schema?group=${encodeURIComponent(groupName)}`;
|
||||
|
||||
let browser;
|
||||
try {
|
||||
browser = await puppeteer.launch({ headless: true, args: ["--no-sandbox", "--disable-web-security"] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1280, height: 720 });
|
||||
await page.goto(schemaPageUrl, { waitUntil: "networkidle0", timeout: 80000 });
|
||||
|
||||
await page.waitForSelector("[data-layouts]", { timeout: 30000 });
|
||||
|
||||
const { dataLayouts, dataGroupSettings } = await page.$eval(
|
||||
"[data-layouts]",
|
||||
(el) => ({
|
||||
dataLayouts: el.getAttribute("data-layouts"),
|
||||
dataGroupSettings: el.getAttribute("data-group-settings"),
|
||||
})
|
||||
);
|
||||
|
||||
let slides, groupSettings;
|
||||
try {
|
||||
slides = JSON.parse(dataLayouts || "[]");
|
||||
} catch (e) {
|
||||
slides = [];
|
||||
}
|
||||
try {
|
||||
groupSettings = JSON.parse(dataGroupSettings || "null");
|
||||
} catch (e) {
|
||||
groupSettings = null;
|
||||
}
|
||||
|
||||
const response = {
|
||||
name: groupName,
|
||||
ordered: groupSettings?.ordered ?? false,
|
||||
slides: slides.map((slide: any) => ({
|
||||
id: slide.id,
|
||||
name: slide.name,
|
||||
description: slide.description,
|
||||
json_schema: slide.json_schema,
|
||||
})),
|
||||
};
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: "Failed to fetch or parse client page" }, { status: 500 });
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
}
|
||||
}
|
||||
57
servers/nextjs/app/api/templates/route.ts
Normal file
57
servers/nextjs/app/api/templates/route.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { GroupSetting } from '@/app/(presentation-generator)/template-preview/types'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const layoutsDirectory = path.join(process.cwd(), 'presentation-layouts')
|
||||
const items = await fs.readdir(layoutsDirectory, { withFileTypes: true })
|
||||
|
||||
const groupDirectories = items.filter(item => item.isDirectory()).map(dir => dir.name)
|
||||
|
||||
const allLayouts: { groupName: string; files: string[]; settings: GroupSetting | null }[] = []
|
||||
|
||||
for (const groupName of groupDirectories) {
|
||||
try {
|
||||
const groupPath = path.join(layoutsDirectory, groupName)
|
||||
const groupFiles = await fs.readdir(groupPath)
|
||||
|
||||
const layoutFiles = groupFiles.filter(file =>
|
||||
file.endsWith('.tsx') &&
|
||||
!file.startsWith('.') &&
|
||||
!file.includes('.test.') &&
|
||||
!file.includes('.spec.') &&
|
||||
file !== 'settings.json'
|
||||
)
|
||||
|
||||
let settings: GroupSetting | null = null
|
||||
const settingsPath = path.join(groupPath, 'settings.json')
|
||||
try {
|
||||
const settingsContent = await fs.readFile(settingsPath, 'utf-8')
|
||||
settings = JSON.parse(settingsContent) as GroupSetting
|
||||
} catch {
|
||||
settings = {
|
||||
description: `${groupName} presentation templates`,
|
||||
ordered: false,
|
||||
default: false,
|
||||
}
|
||||
}
|
||||
|
||||
if (layoutFiles.length > 0) {
|
||||
allLayouts.push({ groupName, files: layoutFiles, settings })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading group directory ${groupName}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(allLayouts)
|
||||
} catch (error) {
|
||||
console.error('Error reading presentation-layouts directory:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to read presentation layouts directory' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +1,27 @@
|
|||
"use client";
|
||||
|
||||
import Wrapper from "@/components/Wrapper";
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import BackBtn from "@/components/BackBtn";
|
||||
import { usePathname } from "next/navigation";
|
||||
import HeaderNav from "@/app/(presentation-generator)/components/HeaderNab";
|
||||
import { Layout } from "lucide-react";
|
||||
const Header = () => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const Header: React.FC = () => {
|
||||
return (
|
||||
<div className="bg-[#5146E5] w-full shadow-lg sticky top-0 z-50">
|
||||
<Wrapper>
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div className="flex items-center gap-3">
|
||||
{pathname !== "/upload" && <BackBtn />}
|
||||
<Link href="/dashboard">
|
||||
<img
|
||||
src="/logo-white.png"
|
||||
alt="Presentation logo"
|
||||
className="h-16"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/layout-preview"
|
||||
prefetch={false}
|
||||
className="flex items-center gap-2 px-3 py-2 text-white hover:bg-primary/80 rounded-md transition-colors outline-none"
|
||||
role="menuitem"
|
||||
>
|
||||
<header className="w-full border-b bg-white/60 backdrop-blur supports-[backdrop-filter]:bg-white/60 sticky top-0 z-50">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<img src="/logo-white.png" alt="Presenton" className="h-6 w-auto" />
|
||||
</Link>
|
||||
|
||||
<nav className="flex items-center gap-4">
|
||||
<Link href="/template-preview" className="inline-flex items-center gap-2 text-gray-700 hover:text-gray-900">
|
||||
<Layout className="w-5 h-5" />
|
||||
<span className="text-sm font-medium font-inter">Layouts</span>
|
||||
<span className="text-sm font-medium font-inter">Templates</span>
|
||||
</Link>
|
||||
<HeaderNav />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</Wrapper>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue