userconfig ipc & build error fixed
This commit is contained in:
parent
96f60288f5
commit
d02ee3afb1
16 changed files with 1427 additions and 770 deletions
|
|
@ -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
9
app/ipc/read_file.ts
Normal 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
268
app/ipc/slide_metadata.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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
673
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
150
servers/nextjs/app/setting/SettingPage.tsx
Normal file
150
servers/nextjs/app/setting/SettingPage.tsx
Normal 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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
335
servers/nextjs/components/Home.tsx
Normal file
335
servers/nextjs/components/Home.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue