userconfig ipc & build error fixed

This commit is contained in:
shiva raj badu 2025-05-12 02:02:50 +05:45
parent 96f60288f5
commit d02ee3afb1
16 changed files with 1427 additions and 770 deletions

View file

@ -1,7 +1,8 @@
import { setupExportHandlers } from "./export_handlers";
import { setupUserConfigHandlers } from "./user_config_handlers";
import { setupReadFile } from "./read_file";
export function setupIpcHandlers() {
setupExportHandlers();
setupUserConfigHandlers();
setupReadFile();
}

9
app/ipc/read_file.ts Normal file
View file

@ -0,0 +1,9 @@
import { ipcMain } from "electron";
import fs from "fs";
import path from "path";
export function setupReadFile() {
ipcMain.handle("read-file", async (_, filePath: string) => {
const normalizedPath = path.normalize(filePath);
return fs.readFileSync(normalizedPath, 'utf-8');
});
}

268
app/ipc/slide_metadata.ts Normal file
View file

@ -0,0 +1,268 @@
import { ipcMain } from "electron";
import puppeteer from "puppeteer";
import fs from 'fs';
import path from 'path';
import { tempDir } from "../constants";
interface Position {
left: number;
top: number;
width: number;
height: number;
}
interface FontStyles {
name: string;
size: number;
bold: boolean;
weight: number;
color: string;
}
interface TextElement {
position: Position;
paragraphs: {
alignment: number;
text: string;
font: FontStyles;
}[];
}
interface PictureElement {
position: Position;
picture: {
is_network: boolean;
path: string;
};
shape: string | null;
object_fit: {
fit: string | null;
focus: number[];
};
overlay: string | null;
border_radius: number[];
}
interface BoxElement {
position: Position;
type: number;
fill: {
color: string;
};
border_radius: number;
stroke: {
color: string;
thickness: number;
};
shadow: {
radius: number;
color: string;
offset: number;
opacity: number;
angle: number;
};
}
interface LineElement {
position: Position;
lineType: number;
thickness: string;
color: string;
}
interface GraphElement {
position: Position;
picture: {
is_network: boolean;
path: string;
};
border_radius: number[];
}
type SlideElement = TextElement | PictureElement | BoxElement | LineElement | GraphElement;
interface SlideMetadata {
slideIndex: number;
backgroundColor: string;
elements: SlideElement[];
}
interface ThemeParams {
theme: string;
customColors?: {
slideBg: string;
slideTitle: string;
slideHeading: string;
slideDescription: string;
slideBox: string;
};
}
export function setupSlideMetadataHandlers() {
ipcMain.handle("get-slide-metadata", async (_, url: string, theme: string, customColors?: ThemeParams["customColors"]) => {
let browser;
try {
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto(url, { waitUntil: "networkidle0", timeout: 60000 });
await page.waitForSelector('[data-element-type="slide-container"]', { timeout: 80000 });
// Apply theme
await page.evaluate((params: ThemeParams) => {
const { theme, customColors } = params;
document.querySelectorAll(".slide-theme").forEach((container) => {
container.setAttribute("data-theme", theme);
});
if (theme === "custom" && customColors) {
const root = document.documentElement;
root.style.setProperty("--custom-slide-bg", customColors.slideBg);
root.style.setProperty("--custom-slide-title", customColors.slideTitle);
root.style.setProperty("--custom-slide-heading", customColors.slideHeading);
root.style.setProperty("--custom-slide-description", customColors.slideDescription);
root.style.setProperty("--custom-slide-box", customColors.slideBox);
}
}, { theme, customColors });
// Get slide metadata
const metadata = await page.evaluate(() => {
const rgbToHex = (color: string): string => {
if (!color || color === "transparent" || color === "none") return "000000";
if (color.startsWith("#")) return color.replace("#", "");
const matches = color.match(/\d+/g);
if (!matches) return "000000";
const [r, g, b] = matches.map(x => parseInt(x));
return [r, g, b].map(x => x.toString(16).padStart(2, "0")).join("");
};
const slidesMetadata: SlideMetadata[] = [];
const slideContainers = document.querySelectorAll('[data-element-type="slide-container"]');
slideContainers.forEach((container) => {
const containerEl = container as HTMLElement;
containerEl.style.width = "1280px";
containerEl.style.height = "720px";
containerEl.style.transform = "none";
const containerRect = containerEl.getBoundingClientRect();
const slideIndex = parseInt(containerEl.getAttribute("data-slide-index") || "0");
const backgroundColor = rgbToHex(window.getComputedStyle(containerEl).backgroundColor);
const elements: SlideElement[] = [];
const slideElements = containerEl.querySelectorAll('[data-slide-element]:not([data-element-type="slide-container"])');
slideElements.forEach((element) => {
const el = element as HTMLElement;
const elementRect = el.getBoundingClientRect();
const computedStyle = window.getComputedStyle(el);
const position: Position = {
left: Math.round(elementRect.left - containerRect.left),
top: Math.round(elementRect.top - containerRect.top),
width: Math.round(elementRect.width),
height: Math.round(elementRect.height),
};
const elementType = el.getAttribute("data-element-type");
if (!elementType) return;
switch (elementType) {
case "text":
elements.push({
position,
paragraphs: [{
alignment: el.getAttribute("data-is-align") === 'true' ? 2 : 1,
text: el.getAttribute("data-text-content") || el.textContent || "",
font: {
name: computedStyle.fontFamily.split('_')[2] || 'Inter',
size: parseInt(computedStyle.fontSize),
bold: parseInt(computedStyle.fontWeight) >= 500,
weight: parseInt(computedStyle.fontWeight),
color: rgbToHex(computedStyle.color),
},
}],
} as TextElement);
break;
case "picture":
const imgEl = el.tagName.toLowerCase() === "img" ? el as HTMLImageElement : el.querySelector("img") as HTMLImageElement;
if (imgEl) {
elements.push({
position,
picture: {
is_network: imgEl.src.startsWith("http"),
path: imgEl.src || imgEl.getAttribute("data-image-path") || "",
},
shape: imgEl.getAttribute('data-image-type'),
object_fit: {
fit: imgEl.getAttribute('data-object-fit'),
focus: [
parseFloat(imgEl.getAttribute('data-focial-point-x') || '0'),
parseFloat(imgEl.getAttribute('data-focial-point-y') || '0'),
],
},
overlay: el.getAttribute("data-is-icon") ? "ffffff" : null,
border_radius: Array(4).fill(parseInt(computedStyle.borderRadius) || 0),
} as PictureElement);
}
break;
case "graph":
elements.push({
position,
picture: {
is_network: true,
path: `__GRAPH_PLACEHOLDER__${el.getAttribute("data-element-id")}`,
},
border_radius: [0, 0, 0, 0],
} as GraphElement);
break;
}
});
slidesMetadata.push({ slideIndex, backgroundColor, elements });
});
return slidesMetadata;
});
// Handle graph elements
const graphElements = await page.$$('[data-element-type="graph"]');
for (const graphElement of graphElements) {
const graphId = await graphElement.evaluate(el => el.getAttribute("data-element-id"));
const screenshot = await graphElement.screenshot({
type: "jpeg",
encoding: "base64",
quality: 100,
omitBackground: true,
});
const filename = `chart-${graphId}-${Date.now()}.jpg`;
const filePath = path.join(tempDir, filename);
fs.writeFileSync(filePath, Buffer.from(screenshot, 'base64'));
metadata.forEach(slide => {
slide.elements.forEach(element => {
if ('picture' in element && element.picture.path === `__GRAPH_PLACEHOLDER__${graphId}`) {
element.picture.path = filePath;
}
});
});
}
return metadata;
} catch (error) {
console.error("Error during page preparation:", error);
throw error;
} finally {
if (browser) await browser.close();
}
});
}

View file

@ -12,4 +12,6 @@ contextBridge.exposeInMainWorld('electron', {
fileDownloaded: (filePath: string) => ipcRenderer.invoke("file-downloaded", filePath),
getUserConfig: () => ipcRenderer.invoke("get-user-config"),
setUserConfig: (userConfig: UserConfig) => ipcRenderer.invoke("set-user-config", userConfig),
readFile: (filePath: string) => ipcRenderer.invoke("read-file", filePath),
getSlideMetadata: (url: string, theme: string, customColors?: any) => ipcRenderer.invoke("get-slide-metadata", url, theme, customColors),
});

673
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -30,7 +30,8 @@
"@tailwindcss/cli": "^4.1.5",
"dotenv": "^16.5.0",
"electron-squirrel-startup": "^1.0.1",
"puppeteer": "^24.8.2",
"tailwindcss": "^4.1.5",
"tree-kill": "^1.2.2"
}
}
}

View file

@ -27,7 +27,6 @@ import { Button } from "@/components/ui/button";
import { toast } from "@/hooks/use-toast";
import Header from "@/app/dashboard/components/Header";
import MarkdownRenderer from "./MarkdownRenderer";
import { fetchTextFromURL } from "../../utils/download";
import { getIconFromFile, removeUUID } from "../../utils/others";
import { ChevronRight, PanelRightOpen, X } from "lucide-react";
import ToolTip from "@/components/ToolTip";
@ -83,8 +82,8 @@ const DocumentsPreviewPage: React.FC = () => {
}
};
const maintainDocumentTexts = async () => {
const maintainDocumentTexts = async () => {
const newDocuments: string[] = [];
const promises: Promise<string>[] = [];
@ -92,7 +91,8 @@ const DocumentsPreviewPage: React.FC = () => {
documentKeys.forEach(key => {
if (!(key in textContents)) {
newDocuments.push(key);
promises.push(fetchTextFromURL(documents[key]));
// @ts-ignore
promises.push(window.electron.readFile(documents[key]));
}
});
@ -100,20 +100,30 @@ const DocumentsPreviewPage: React.FC = () => {
reportKeys.forEach(key => {
if (!(key in textContents)) {
newDocuments.push(key);
promises.push(fetchTextFromURL(reports[key]));
// @ts-ignore
promises.push(window.electron.readFile(reports[key]));
}
});
if (promises.length > 0) {
setDownloadingDocuments(newDocuments);
const results = await Promise.all(promises);
setTextContents(prev => {
const newContents = { ...prev };
newDocuments.forEach((key, index) => {
newContents[key] = results[index];
try {
const results = await Promise.all(promises);
setTextContents(prev => {
const newContents = { ...prev };
newDocuments.forEach((key, index) => {
newContents[key] = results[index];
});
return newContents;
});
return newContents;
});
} catch (error) {
console.error('Error reading files:', error);
toast({
title: "Error",
description: "Failed to read document content",
variant: "destructive",
});
}
setDownloadingDocuments([]);
}
};

View file

@ -1,42 +0,0 @@
/**
* Fetches text content from a URL or local file path
* @param url - The URL or file path to fetch content from
* @returns Promise<string> - The text content
*/
export async function fetchTextFromURL(url: string): Promise<string> {
if (!url) return "";
try {
// Remove file:// prefix if present
const cleanUrl = url.replace('file://', '');
// If it's a local file path, use the API endpoint
if (cleanUrl.startsWith('/tmp/') || cleanUrl.startsWith('/')) {
const response = await fetch('/api/read-file', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ filePath: cleanUrl }),
});
if (!response.ok) {
throw new Error(`Failed to fetch file: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data.content;
}
// For remote URLs, use regular fetch
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
}
return await response.text();
} catch (error) {
console.error("Error fetching text:", error);
return "";
}
}

View file

@ -1,30 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import fs from "fs";
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const path = searchParams.get('path');
if (!path) {
return NextResponse.json(
{ error: "Path parameter is required" },
{ status: 400 }
);
}
let config = {};
if (fs.existsSync(path)) {
const configData = fs.readFileSync(path, 'utf-8');
config = JSON.parse(configData);
}
return NextResponse.json({ config });
} catch (error) {
console.error("Error reading config:", error);
return NextResponse.json(
{ error: "Failed to read configuration" },
{ status: 500 }
);
}
}

View file

@ -1,37 +0,0 @@
import { NextResponse } from 'next/server';
import fs from 'fs/promises';
import path from 'path';
export async function POST(request: Request) {
try {
const { filePath } = await request.json();
// Validate file path
if (!filePath || typeof filePath !== 'string') {
return NextResponse.json(
{ error: 'Invalid file path' },
{ status: 400 }
);
}
// Security check: ensure the path is within /tmp directory
const normalizedPath = path.normalize(filePath);
if (!normalizedPath.startsWith('/tmp/')) {
return NextResponse.json(
{ error: 'Access denied: File must be in /tmp directory' },
{ status: 403 }
);
}
// Read file content
const content = await fs.readFile(normalizedPath, 'utf-8');
return NextResponse.json({ content });
} catch (error) {
console.error('Error reading file:', error);
return NextResponse.json(
{ error: 'Failed to read file' },
{ status: 500 }
);
}
}

View file

@ -1,51 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import fs from "fs";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { provider, apiKey, userConfigPath } = body;
if (!userConfigPath) {
return NextResponse.json(
{ error: "User config path not found" },
{ status: 500 }
);
}
// Create config object based on provider
const config = {
LLM: provider === "google" ? "gemini-pro" : "gpt-4",
OPENAI_API_KEY: provider === "openai" ? apiKey : undefined,
GOOGLE_API_KEY: provider === "google" ? apiKey : undefined,
};
// Read existing config if it exists
let existingConfig = {};
if (fs.existsSync(userConfigPath)) {
try {
const existingData = fs.readFileSync(userConfigPath, 'utf-8');
existingConfig = JSON.parse(existingData);
} catch (error) {
console.error("Error reading existing config:", error);
}
}
// Merge with existing config
const mergedConfig = {
...existingConfig,
...config,
};
// Write to file
fs.writeFileSync(userConfigPath, JSON.stringify(mergedConfig, null, 2));
return NextResponse.json({ success: true, config: mergedConfig });
} catch (error) {
console.error("Error saving user config:", error);
return NextResponse.json(
{ error: "Failed to save configuration" },
{ status: 500 }
);
}
}

View file

@ -1,369 +1,9 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { toast } from "@/hooks/use-toast";
import { Info, ExternalLink, PlayCircle, Loader2 } from "lucide-react";
import Link from "next/link";
import { getEnv } from "@/utils/constant";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
interface ModelOption {
value: string;
label: string;
description?: string;
}
interface ProviderConfig {
textModels: ModelOption[];
imageModels: ModelOption[];
apiGuide: {
title: string;
steps: string[];
videoUrl?: string;
docsUrl: string;
};
}
const PROVIDER_CONFIGS: Record<string, ProviderConfig> = {
openai: {
textModels: [
{
value: "gpt-4",
label: "GPT-4",
description: "Most capable model, best for complex tasks",
},
],
imageModels: [
{
value: "dall-e-3",
label: "DALL-E 3",
description: "Latest version with highest quality",
},
],
apiGuide: {
title: "How to get your OpenAI API Key",
steps: [
"Go to platform.openai.com and sign in or create an account",
'Click on your profile icon and select "View API keys"',
'Click "Create new secret key" and give it a name',
"Copy your API key immediately (you won't be able to see it again)",
"Make sure you have sufficient credits in your account",
],
videoUrl: "https://www.youtube.com/watch?v=OB99E7Y1cMA",
docsUrl: "https://platform.openai.com/docs/api-reference/authentication",
},
},
google: {
textModels: [
{
value: "gemini-pro",
label: "Gemini Pro",
description: "Balanced model for most tasks",
},
],
imageModels: [
{
value: "imagen",
label: "Imagen",
description: "Google's primary image generation model",
},
],
apiGuide: {
title: "How to get your Google AI Studio API Key",
steps: [
"Visit aistudio.google.com",
'Click on "Get API key" in the top navigation',
'Click "Create API key" on the next page',
'Choose either "Create API Key in new Project" or select an existing project',
"Copy your API key - you're ready to go!",
],
videoUrl: "https://www.youtube.com/watch?v=o8iyrtQyrZM&t=66s",
docsUrl: "https://aistudio.google.com/app/apikey",
},
},
};
interface ConfigState {
provider: string;
apiKey: string;
textModel: string;
imageModel: string;
}
export default function Home() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(true);
const [config, setConfig] = useState<ConfigState>({
provider: "openai",
apiKey: "",
textModel: PROVIDER_CONFIGS.openai.textModels[0].value,
imageModel: PROVIDER_CONFIGS.openai.imageModels[0].value,
});
useEffect(() => {
const checkExistingConfig = async () => {
try {
const urls = getEnv();
const userConfigPath = urls.USER_CONFIG_PATH;
if (!userConfigPath) {
console.error("User config path not found");
setIsLoading(false);
return;
}
const response = await fetch(`/api/read-config?path=${encodeURIComponent(userConfigPath)}`);
if (!response.ok) {
throw new Error('Failed to load configuration');
}
const { config: savedConfig } = await response.json();
// If either API key exists, redirect to upload
if (savedConfig?.OPENAI_API_KEY || savedConfig?.GOOGLE_API_KEY) {
router.push('/upload');
} else {
setIsLoading(false);
}
} catch (error) {
console.error("Error checking config:", error);
setIsLoading(false);
}
};
checkExistingConfig();
}, [router]);
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-white flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-8 h-8 animate-spin text-blue-600 mx-auto mb-4" />
<p className="text-gray-600">Loading configuration...</p>
</div>
</div>
);
}
const handleProviderChange = (provider: string) => {
setConfig((prev) => ({
...prev,
provider,
textModel: PROVIDER_CONFIGS[provider].textModels[0].value,
imageModel: PROVIDER_CONFIGS[provider].imageModels[0].value,
}));
};
const handleConfigChange = (
field: keyof ConfigState,
value: string | number
) => {
setConfig((prev) => ({
...prev,
[field]: value,
}));
};
const currentProvider = PROVIDER_CONFIGS[config.provider];
const handleSaveConfig = async () => {
if (!config.apiKey) {
toast({
title: "Error",
description: "Please enter an API key",
});
return;
}
try {
const userConfigPath = getEnv().USER_CONFIG_PATH;
if (!userConfigPath) {
toast({
title: "Error",
description: "Configuration path not found",
});
return;
}
const response = await fetch('/api/save-user-config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider: config.provider,
apiKey: config.apiKey,
userConfigPath,
}),
});
if (!response.ok) {
throw new Error('Failed to save configuration');
}
toast({
title: "Configuration saved",
description: "You can now upload your presentation",
});
router.push("/upload");
} catch (error) {
console.error('Error saving configuration:', error);
toast({
title: "Error",
description: "Failed to save configuration",
});
}
};
import Home from '../components/Home'
const page = () => {
return (
<div className="min-h-screen bg-gradient-to-b font-satoshi from-gray-50 to-white">
<main className="container mx-auto px-4 py-12 max-w-3xl">
{/* Branding Header */}
<div className="text-center mb-12">
<div className="flex items-center justify-center gap-3 mb-4">
<img src="/Logo.png" alt="Presenton Logo" className="" />
</div>
<p className="text-gray-600 text-lg">
Open-source AI presentation generator
</p>
</div>
{/* Main Configuration Card */}
<div className="bg-white rounded-xl shadow-lg border border-gray-100 p-8">
{/* Provider Selection */}
<div className="mb-8">
<label className="block text-sm font-medium text-gray-700 mb-3">
Select AI Provider
</label>
<div className="grid grid-cols-2 gap-4">
{Object.keys(PROVIDER_CONFIGS).map((provider) => (
<button
key={provider}
onClick={() => handleProviderChange(provider)}
className={`relative p-4 rounded-lg border-2 transition-all duration-200 ${config.provider === provider
? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-blue-200 hover:bg-gray-50"
}`}
>
<div className="flex items-center justify-center gap-3">
<span
className={`font-medium text-center ${config.provider === provider
? "text-blue-700"
: "text-gray-700"
}`}
>
{provider.charAt(0).toUpperCase() + provider.slice(1)}
</span>
</div>
</button>
))}
</div>
</div>
{/* API Key Input */}
<div className="mb-8">
<label className="block text-sm font-medium text-gray-700 mb-2">
{config.provider.charAt(0).toUpperCase() +
config.provider.slice(1)}{" "}
API Key
</label>
<div className="relative">
<input
type="text"
value={config.apiKey}
onChange={(e) => handleConfigChange("apiKey", e.target.value)}
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
placeholder="Enter your API key"
/>
</div>
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
Your API key will be stored locally and never shared
</p>
</div>
{/* Model Information */}
<div className="mb-8 p-4 bg-blue-50 rounded-lg border border-blue-100">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-blue-500 mt-0.5" />
<div>
<h3 className="text-sm font-medium text-blue-900 mb-1">
Selected Models
</h3>
<p className="text-sm text-blue-700">
Using {currentProvider.textModels[0].label} for text
generation and {currentProvider.imageModels[0].label} for
images
</p>
<p className="text-sm text-blue-600 mt-2 opacity-75">
We've pre-selected the best models for optimal presentation
generation
</p>
</div>
</div>
</div>
{/* API Guide Section */}
<Accordion type="single" collapsible className="mb-8 bg-gray-50 rounded-lg border border-gray-200">
<AccordionItem value="guide" className="border-none">
<AccordionTrigger className="px-6 py-4 hover:no-underline">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-blue-600 mt-1" />
<h3 className="text-lg font-medium text-gray-900">
{currentProvider.apiGuide.title}
</h3>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-6">
<div className="space-y-4">
<ol className="list-decimal list-inside space-y-2 text-gray-600">
{currentProvider.apiGuide.steps.map((step, index) => (
<li key={index} className="text-sm">
{step}
</li>
))}
</ol>
<div className="flex flex-col sm:flex-row gap-4 mt-6">
{currentProvider.apiGuide.videoUrl && (
<Link
href={currentProvider.apiGuide.videoUrl}
target="_blank"
className="flex items-center gap-2 text-sm text-blue-600 hover:text-blue-700 transition-colors"
>
<PlayCircle className="w-4 h-4" />
Watch Video Tutorial
<ExternalLink className="w-3 h-3" />
</Link>
)}
<Link
href={currentProvider.apiGuide.docsUrl}
target="_blank"
className="flex items-center gap-2 text-sm text-blue-600 hover:text-blue-700 transition-colors"
>
<span>Official Documentation</span>
<ExternalLink className="w-3 h-3" />
</Link>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
{/* Save Button */}
<button
onClick={handleSaveConfig}
className="mt-8 w-full font-semibold bg-gradient-to-r from-blue-600 to-indigo-600 text-white py-3 px-4 rounded-lg hover:from-blue-700 hover:to-indigo-700 focus:ring-4 focus:ring-blue-200 transition-all duration-500"
>
Save Configuration
</button>
</div>
</main>
</div>
);
<Home />
)
}
export default page

View file

@ -0,0 +1,150 @@
'use client';
import React, { useState, useEffect } from "react";
import Header from "../dashboard/components/Header";
import Wrapper from "@/components/Wrapper";
import { Settings, Key } from 'lucide-react';
import { toast } from '@/hooks/use-toast';
import { getEnv } from "@/utils/constant";
interface UserConfig {
LLM?: string;
OPENAI_API_KEY?: string;
GOOGLE_API_KEY?: string;
}
const SettingsPage = () => {
const [config, setConfig] = useState<UserConfig>({});
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadConfig = async () => {
try {
// @ts-ignore
const config = await window.electron.getUserConfig();
setConfig(config);
} catch (error) {
console.error("Error loading config:", error);
toast({
title: 'Error',
description: 'Failed to load configuration',
});
} finally {
setIsLoading(false);
}
};
loadConfig();
}, []);
const handleSaveConfig = async (provider: string, apiKey: string) => {
try {
const newConfig = {
...config,
LLM: provider,
[provider === 'openai' ? 'OPENAI_API_KEY' : 'GOOGLE_API_KEY']: apiKey
};
// @ts-ignore
await window.electron.setUserConfig(newConfig);
setConfig(newConfig);
toast({
title: 'Success',
description: 'Configuration saved successfully',
});
} catch (error) {
console.error('Error:', error);
toast({
title: 'Error',
description: 'Failed to save configuration',
});
}
};
if (isLoading) {
return (
<div className="min-h-screen bg-[#E9E8F8]">
<Header />
<Wrapper className="lg:w-[60%]">
<div className="py-8">
<div className="text-center">Loading configuration...</div>
</div>
</Wrapper>
</div>
);
}
return (
<div className="min-h-screen bg-[#E9E8F8]">
<Header />
<Wrapper className="lg:w-[60%]">
<div className="py-8 space-y-6">
{/* Settings Header */}
<div className="flex items-center gap-3 mb-6">
<Settings className="w-8 h-8 text-blue-600" />
<h1 className="text-2xl font-semibold text-gray-900">Settings</h1>
</div>
{/* API Configuration Section */}
<div className="bg-white rounded-xl shadow-sm p-6">
<div className="flex items-center gap-3 mb-6">
<Key className="w-5 h-5 text-blue-600" />
<h2 className="text-lg font-medium text-gray-900">API Configuration</h2>
</div>
<div className="space-y-6">
{/* OpenAI Configuration */}
<div className="border-b border-gray-100 pb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
OpenAI API Key
</label>
<div className="flex gap-3">
<input
type="text"
value={config.OPENAI_API_KEY || ''}
onChange={(e) => setConfig(prev => ({ ...prev, OPENAI_API_KEY: e.target.value }))}
className="flex-1 px-4 py-2.5 border border-gray-300 outline-none rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
placeholder="Enter your OpenAI API key"
/>
<button
onClick={() => handleSaveConfig('openai', config.OPENAI_API_KEY || '')}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Save
</button>
</div>
<p className="mt-2 text-sm text-gray-500">Required for using OpenAI services</p>
</div>
{/* Google Configuration */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Google API Key
</label>
<div className="flex gap-3">
<input
type="password"
value={config.GOOGLE_API_KEY || ''}
onChange={(e) => setConfig(prev => ({ ...prev, GOOGLE_API_KEY: e.target.value }))}
className="flex-1 px-4 py-2.5 border border-gray-300 outline-none rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
placeholder="Enter your Google API key"
/>
<button
onClick={() => handleSaveConfig('google', config.GOOGLE_API_KEY || '')}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Save
</button>
</div>
<p className="mt-2 text-sm text-gray-500">Required for using Google services</p>
</div>
</div>
</div>
</div>
</Wrapper>
</div>
);
};
export default SettingsPage;

View file

@ -1,183 +1,2 @@
'use client';
import React, { useState, useEffect } from "react";
import Header from "../dashboard/components/Header";
import Wrapper from "@/components/Wrapper";
import { Settings, Key } from 'lucide-react';
import { toast } from '@/hooks/use-toast';
import { getEnv } from "@/utils/constant";
interface UserConfig {
LLM?: string;
OPENAI_API_KEY?: string;
GOOGLE_API_KEY?: string;
}
const SettingsPage = () => {
const [config, setConfig] = useState<UserConfig>({});
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadConfig = async () => {
try {
const urls = getEnv();
const userConfigPath = urls.USER_CONFIG_PATH;
if (!userConfigPath) {
console.error("User config path not found");
return;
}
// Use the Node.js fs API through an API route
const response = await fetch(`/api/read-config?path=${encodeURIComponent(userConfigPath)}`);
if (!response.ok) {
throw new Error('Failed to load configuration');
}
const { config } = await response.json();
setConfig(config);
} catch (error) {
console.error("Error loading config:", error);
toast({
title: 'Error',
description: 'Failed to load configuration',
});
} finally {
setIsLoading(false);
}
};
loadConfig();
}, []);
const handleSaveConfig = async (provider: string, apiKey: string) => {
try {
const urls = getEnv();
const userConfigPath = urls.USER_CONFIG_PATH;
if (!userConfigPath) {
toast({
title: 'Error',
description: 'Configuration path not found',
});
return;
}
const response = await fetch('/api/save-user-config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider,
apiKey,
userConfigPath,
}),
});
if (!response.ok) {
throw new Error('Failed to save configuration');
}
const { config: newConfig } = await response.json();
setConfig(newConfig);
toast({
title: 'Success',
description: 'Configuration saved successfully',
});
} catch (error) {
console.error('Error:', error);
toast({
title: 'Error',
description: 'Failed to save configuration',
});
}
};
if (isLoading) {
return (
<div className="min-h-screen bg-[#E9E8F8]">
<Header />
<Wrapper className="lg:w-[60%]">
<div className="py-8">
<div className="text-center">Loading configuration...</div>
</div>
</Wrapper>
</div>
);
}
return (
<div className="min-h-screen bg-[#E9E8F8]">
<Header />
<Wrapper className="lg:w-[60%]">
<div className="py-8 space-y-6">
{/* Settings Header */}
<div className="flex items-center gap-3 mb-6">
<Settings className="w-8 h-8 text-blue-600" />
<h1 className="text-2xl font-semibold text-gray-900">Settings</h1>
</div>
{/* API Configuration Section */}
<div className="bg-white rounded-xl shadow-sm p-6">
<div className="flex items-center gap-3 mb-6">
<Key className="w-5 h-5 text-blue-600" />
<h2 className="text-lg font-medium text-gray-900">API Configuration</h2>
</div>
<div className="space-y-6">
{/* OpenAI Configuration */}
<div className="border-b border-gray-100 pb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
OpenAI API Key
</label>
<div className="flex gap-3">
<input
type="text"
value={config.OPENAI_API_KEY || ''}
onChange={(e) => setConfig(prev => ({ ...prev, OPENAI_API_KEY: e.target.value }))}
className="flex-1 px-4 py-2.5 border border-gray-300 outline-none rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
placeholder="Enter your OpenAI API key"
/>
<button
onClick={() => handleSaveConfig('openai', config.OPENAI_API_KEY || '')}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Save
</button>
</div>
<p className="mt-2 text-sm text-gray-500">Required for using OpenAI services</p>
</div>
{/* Google Configuration */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Google API Key
</label>
<div className="flex gap-3">
<input
type="password"
value={config.GOOGLE_API_KEY || ''}
onChange={(e) => setConfig(prev => ({ ...prev, GOOGLE_API_KEY: e.target.value }))}
className="flex-1 px-4 py-2.5 border border-gray-300 outline-none rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
placeholder="Enter your Google API key"
/>
<button
onClick={() => handleSaveConfig('google', config.GOOGLE_API_KEY || '')}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Save
</button>
</div>
<p className="mt-2 text-sm text-gray-500">Required for using Google services</p>
</div>
</div>
</div>
</div>
</Wrapper>
</div>
);
};
export default SettingsPage;
import SettingPage from './SettingPage'
export default SettingPage

View file

@ -0,0 +1,335 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { toast } from "@/hooks/use-toast";
import { Info, ExternalLink, PlayCircle, Loader2 } from "lucide-react";
import Link from "next/link";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
interface ModelOption {
value: string;
label: string;
description?: string;
}
interface ProviderConfig {
textModels: ModelOption[];
imageModels: ModelOption[];
apiGuide: {
title: string;
steps: string[];
videoUrl?: string;
docsUrl: string;
};
}
const PROVIDER_CONFIGS: Record<string, ProviderConfig> = {
openai: {
textModels: [
{
value: "gpt-4",
label: "GPT-4",
description: "Most capable model, best for complex tasks",
},
],
imageModels: [
{
value: "dall-e-3",
label: "DALL-E 3",
description: "Latest version with highest quality",
},
],
apiGuide: {
title: "How to get your OpenAI API Key",
steps: [
"Go to platform.openai.com and sign in or create an account",
'Click on your profile icon and select "View API keys"',
'Click "Create new secret key" and give it a name',
"Copy your API key immediately (you won't be able to see it again)",
"Make sure you have sufficient credits in your account",
],
videoUrl: "https://www.youtube.com/watch?v=OB99E7Y1cMA",
docsUrl: "https://platform.openai.com/docs/api-reference/authentication",
},
},
google: {
textModels: [
{
value: "gemini-pro",
label: "Gemini Pro",
description: "Balanced model for most tasks",
},
],
imageModels: [
{
value: "imagen",
label: "Imagen",
description: "Google's primary image generation model",
},
],
apiGuide: {
title: "How to get your Google AI Studio API Key",
steps: [
"Visit aistudio.google.com",
'Click on "Get API key" in the top navigation',
'Click "Create API key" on the next page',
'Choose either "Create API Key in new Project" or select an existing project',
"Copy your API key - you're ready to go!",
],
videoUrl: "https://www.youtube.com/watch?v=o8iyrtQyrZM&t=66s",
docsUrl: "https://aistudio.google.com/app/apikey",
},
},
};
interface ConfigState {
provider: string;
apiKey: string;
textModel: string;
imageModel: string;
}
export default function Home() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(true);
const [config, setConfig] = useState<ConfigState>({
provider: "openai",
apiKey: "",
textModel: PROVIDER_CONFIGS.openai.textModels[0].value,
imageModel: PROVIDER_CONFIGS.openai.imageModels[0].value,
});
useEffect(() => {
const checkExistingConfig = async () => {
try {
// @ts-ignore
const savedConfig = await window.electron.getUserConfig();
// If either API key exists, redirect to upload
if (savedConfig?.OPENAI_API_KEY || savedConfig?.GOOGLE_API_KEY) {
router.push('/upload');
} else {
setIsLoading(false);
}
} catch (error) {
console.error("Error checking config:", error);
setIsLoading(false);
}
};
checkExistingConfig();
}, [router]);
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-white flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-8 h-8 animate-spin text-blue-600 mx-auto mb-4" />
<p className="text-gray-600">Loading configuration...</p>
</div>
</div>
);
}
const handleProviderChange = (provider: string) => {
setConfig((prev) => ({
...prev,
provider,
textModel: PROVIDER_CONFIGS[provider].textModels[0].value,
imageModel: PROVIDER_CONFIGS[provider].imageModels[0].value,
}));
};
const handleConfigChange = (
field: keyof ConfigState,
value: string | number
) => {
setConfig((prev) => ({
...prev,
[field]: value,
}));
};
const currentProvider = PROVIDER_CONFIGS[config.provider];
const handleSaveConfig = async () => {
if (!config.apiKey) {
toast({
title: "Error",
description: "Please enter an API key",
});
return;
}
try {
// @ts-ignore
await window.electron.setUserConfig({
LLM: config.provider,
[config.provider === 'openai' ? 'OPENAI_API_KEY' : 'GOOGLE_API_KEY']: config.apiKey
});
toast({
title: "Configuration saved",
description: "You can now upload your presentation",
});
router.push("/upload");
} catch (error) {
console.error('Error saving configuration:', error);
toast({
title: "Error",
description: "Failed to save configuration",
});
}
};
return (
<div className="min-h-screen bg-gradient-to-b font-satoshi from-gray-50 to-white">
<main className="container mx-auto px-4 py-12 max-w-3xl">
{/* Branding Header */}
<div className="text-center mb-12">
<div className="flex items-center justify-center gap-3 mb-4">
<img src="/Logo.png" alt="Presenton Logo" className="" />
</div>
<p className="text-gray-600 text-lg">
Open-source AI presentation generator
</p>
</div>
{/* Main Configuration Card */}
<div className="bg-white rounded-xl shadow-lg border border-gray-100 p-8">
{/* Provider Selection */}
<div className="mb-8">
<label className="block text-sm font-medium text-gray-700 mb-3">
Select AI Provider
</label>
<div className="grid grid-cols-2 gap-4">
{Object.keys(PROVIDER_CONFIGS).map((provider) => (
<button
key={provider}
onClick={() => handleProviderChange(provider)}
className={`relative p-4 rounded-lg border-2 transition-all duration-200 ${config.provider === provider
? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-blue-200 hover:bg-gray-50"
}`}
>
<div className="flex items-center justify-center gap-3">
<span
className={`font-medium text-center ${config.provider === provider
? "text-blue-700"
: "text-gray-700"
}`}
>
{provider.charAt(0).toUpperCase() + provider.slice(1)}
</span>
</div>
</button>
))}
</div>
</div>
{/* API Key Input */}
<div className="mb-8">
<label className="block text-sm font-medium text-gray-700 mb-2">
{config.provider.charAt(0).toUpperCase() +
config.provider.slice(1)}{" "}
API Key
</label>
<div className="relative">
<input
type="text"
value={config.apiKey}
onChange={(e) => handleConfigChange("apiKey", e.target.value)}
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
placeholder="Enter your API key"
/>
</div>
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
Your API key will be stored locally and never shared
</p>
</div>
{/* Model Information */}
<div className="mb-8 p-4 bg-blue-50 rounded-lg border border-blue-100">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-blue-500 mt-0.5" />
<div>
<h3 className="text-sm font-medium text-blue-900 mb-1">
Selected Models
</h3>
<p className="text-sm text-blue-700">
Using {currentProvider.textModels[0].label} for text
generation and {currentProvider.imageModels[0].label} for
images
</p>
<p className="text-sm text-blue-600 mt-2 opacity-75">
We've pre-selected the best models for optimal presentation
generation
</p>
</div>
</div>
</div>
{/* API Guide Section */}
<Accordion type="single" collapsible className="mb-8 bg-gray-50 rounded-lg border border-gray-200">
<AccordionItem value="guide" className="border-none">
<AccordionTrigger className="px-6 py-4 hover:no-underline">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-blue-600 mt-1" />
<h3 className="text-lg font-medium text-gray-900">
{currentProvider.apiGuide.title}
</h3>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-6">
<div className="space-y-4">
<ol className="list-decimal list-inside space-y-2 text-gray-600">
{currentProvider.apiGuide.steps.map((step, index) => (
<li key={index} className="text-sm">
{step}
</li>
))}
</ol>
<div className="flex flex-col sm:flex-row gap-4 mt-6">
{currentProvider.apiGuide.videoUrl && (
<Link
href={currentProvider.apiGuide.videoUrl}
target="_blank"
className="flex items-center gap-2 text-sm text-blue-600 hover:text-blue-700 transition-colors"
>
<PlayCircle className="w-4 h-4" />
Watch Video Tutorial
<ExternalLink className="w-3 h-3" />
</Link>
)}
<Link
href={currentProvider.apiGuide.docsUrl}
target="_blank"
className="flex items-center gap-2 text-sm text-blue-600 hover:text-blue-700 transition-colors"
>
<span>Official Documentation</span>
<ExternalLink className="w-3 h-3" />
</Link>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
{/* Save Button */}
<button
onClick={handleSaveConfig}
className="mt-8 w-full font-semibold bg-gradient-to-r from-blue-600 to-indigo-600 text-white py-3 px-4 rounded-lg hover:from-blue-700 hover:to-indigo-700 focus:ring-4 focus:ring-blue-200 transition-all duration-500"
>
Save Configuration
</button>
</div>
</main>
</div>
);
}

View file

@ -56,10 +56,11 @@
"tailwind-merge": "^2.5.3",
"tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"tiptap-markdown": "^0.8.10"
"tiptap-markdown": "^0.8.10",
"@tailwindcss/typography": "^0.5.16"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.16",
"@types/animejs": "^3.1.12",
"@types/node": "^20",
"@types/puppeteer": "^5.4.7",