Merge pull request #10 from presenton/user_config_puppeteer_env_injection
User config puppeteer env injection
This commit is contained in:
commit
a756f6998d
9 changed files with 302 additions and 86 deletions
|
|
@ -37,7 +37,7 @@ import {
|
|||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import ToolTip from "@/components/ToolTip";
|
||||
import { BASE_URL } from "@/utils/constant";
|
||||
import { getEnv } from "@/utils/constant";
|
||||
|
||||
interface ImageEditorProps {
|
||||
initialImage: string | null;
|
||||
|
|
@ -310,6 +310,8 @@ const ImageEditor = ({
|
|||
if (!src) return "";
|
||||
return src.startsWith("user") ? `file://${src}` : `file://${src}`;
|
||||
};
|
||||
const urls = getEnv();
|
||||
const BASE_URL = urls.BASE_URL;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -129,23 +129,28 @@ const Header = ({
|
|||
|
||||
const getSlideMetadata = async () => {
|
||||
try {
|
||||
// Get the current URL without any query parameters
|
||||
const baseUrl = window.location.origin + window.location.pathname;
|
||||
const urls = getEnv();
|
||||
|
||||
const response = await fetch(
|
||||
`${urls.NEXT_PUBLIC_URL}/api/slide-metadata`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: getHeader(),
|
||||
body: JSON.stringify({
|
||||
url: window.location.href,
|
||||
url: baseUrl,
|
||||
theme: currentTheme,
|
||||
customColors: currentColors,
|
||||
tempDirectory: urls.TEMP_DIRECTORY,
|
||||
baseUrl: urls.BASE_URL,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch metadata");
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || "Failed to fetch metadata");
|
||||
}
|
||||
const metadata = await response.json();
|
||||
console.log("metadata", metadata);
|
||||
|
|
@ -153,7 +158,11 @@ const Header = ({
|
|||
} catch (error) {
|
||||
setShowLoader(false);
|
||||
console.error("Error fetching metadata:", error);
|
||||
// You might want to show an error toast/notification here
|
||||
toast({
|
||||
title: "Error fetching slide metadata",
|
||||
description: error instanceof Error ? error.message : "Failed to fetch metadata",
|
||||
variant: "destructive",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ function useDebounce<T extends (...args: any[]) => void>(
|
|||
}
|
||||
|
||||
const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
|
||||
const urls = getEnv();
|
||||
const BASE_URL = urls.BASE_URL;
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedSlide, setSelectedSlide] = useState(0);
|
||||
|
|
|
|||
30
servers/nextjs/app/api/read-config/route.ts
Normal file
30
servers/nextjs/app/api/read-config/route.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
51
servers/nextjs/app/api/save-user-config/route.ts
Normal file
51
servers/nextjs/app/api/save-user-config/route.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -102,7 +102,7 @@ export async function POST(request: NextRequest) {
|
|||
let browser;
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { url, theme, customColors,tempDirectory } = body;
|
||||
const { url, theme, customColors, tempDirectory, baseUrl } = body;
|
||||
|
||||
if (!url) {
|
||||
return NextResponse.json({ error: "Missing URL" }, { status: 400 });
|
||||
|
|
@ -116,21 +116,31 @@ export async function POST(request: NextRequest) {
|
|||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
|
||||
|
||||
// Inject the environment variables before loading the page
|
||||
await page.evaluateOnNewDocument(`
|
||||
window.env = {
|
||||
NEXT_PUBLIC_FAST_API: "${baseUrl || 'http://localhost:8000'}",
|
||||
NEXT_PUBLIC_URL: "${url}",
|
||||
};
|
||||
`);
|
||||
|
||||
try {
|
||||
await page.goto(url, {
|
||||
waitUntil: "networkidle0",
|
||||
timeout: 60000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Navigation error:", error);
|
||||
await browser.close();
|
||||
return NextResponse.json({ error: "Failed to Navigate to provided URL" }, { status: 500 });
|
||||
}
|
||||
|
||||
|
||||
|
||||
try {
|
||||
await page.waitForSelector('[data-element-type="slide-container"]', {
|
||||
timeout: 60000,
|
||||
timeout: 80000,
|
||||
});
|
||||
|
||||
await page.evaluate(
|
||||
async (params: ThemeParams) => {
|
||||
const { theme, customColors } = params;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ export interface PresentationResponse {
|
|||
}
|
||||
|
||||
export class DashboardApi {
|
||||
// static BASE_URL = "http://localhost:48388";
|
||||
|
||||
static async getPresentations(): Promise<PresentationResponse[]> {
|
||||
try {
|
||||
|
|
@ -32,7 +31,6 @@ export class DashboardApi {
|
|||
`${BASE_URL}/ppt/user_presentations`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: getHeader(),
|
||||
}
|
||||
);
|
||||
if (response.status === 200) {
|
||||
|
|
@ -54,7 +52,7 @@ export class DashboardApi {
|
|||
`${BASE_URL}/ppt/presentation?presentation_id=${id}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: getHeader(),
|
||||
|
||||
}
|
||||
);
|
||||
if (response.status === 200) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useRouter } from "next/navigation";
|
|||
import { toast } from "@/hooks/use-toast";
|
||||
import { Info, ExternalLink, PlayCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { getEnv } from "@/utils/constant";
|
||||
|
||||
interface ModelOption {
|
||||
value: string;
|
||||
|
|
@ -88,7 +89,9 @@ interface ConfigState {
|
|||
imageModel: string;
|
||||
}
|
||||
|
||||
|
||||
export default function Home() {
|
||||
const urls = getEnv();
|
||||
const router = useRouter();
|
||||
const [config, setConfig] = useState<ConfigState>({
|
||||
provider: "openai",
|
||||
|
|
@ -117,8 +120,7 @@ export default function Home() {
|
|||
};
|
||||
|
||||
const currentProvider = PROVIDER_CONFIGS[config.provider];
|
||||
const handleSaveConfig = () => {
|
||||
console.log("cllee");
|
||||
const handleSaveConfig = async () => {
|
||||
if (!config.apiKey) {
|
||||
toast({
|
||||
title: "Error",
|
||||
|
|
@ -126,11 +128,47 @@ export default function Home() {
|
|||
});
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: "Configuration saved",
|
||||
description: "You can now upload your presentation",
|
||||
});
|
||||
router.push("/upload");
|
||||
|
||||
try {
|
||||
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: 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",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -158,19 +196,17 @@ export default function Home() {
|
|||
<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"
|
||||
}`}
|
||||
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"
|
||||
}`}
|
||||
className={`font-medium text-center ${config.provider === provider
|
||||
? "text-blue-700"
|
||||
: "text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{provider.charAt(0).toUpperCase() + provider.slice(1)}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,58 +1,116 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Header from "../dashboard/components/Header";
|
||||
import Wrapper from "@/components/Wrapper";
|
||||
import { Settings, Key, Palette, Bell } from 'lucide-react';
|
||||
import { Settings, Key } from 'lucide-react';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { getEnv } from "@/utils/constant";
|
||||
|
||||
interface APIConfig {
|
||||
provider: string;
|
||||
apiKey: string;
|
||||
interface UserConfig {
|
||||
LLM?: string;
|
||||
OPENAI_API_KEY?: string;
|
||||
GOOGLE_API_KEY?: string;
|
||||
}
|
||||
|
||||
const SettingsPage = () => {
|
||||
const [apiConfigs, setApiConfigs] = useState<Record<string, APIConfig>>({
|
||||
openai: {
|
||||
provider: 'OpenAI',
|
||||
apiKey: '',
|
||||
},
|
||||
google: {
|
||||
provider: 'Google',
|
||||
apiKey: '',
|
||||
}
|
||||
});
|
||||
const [config, setConfig] = useState<UserConfig>({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const handleApiKeyChange = (provider: string, value: string) => {
|
||||
setApiConfigs(prev => ({
|
||||
...prev,
|
||||
[provider]: {
|
||||
...prev[provider],
|
||||
apiKey: value
|
||||
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);
|
||||
}
|
||||
}));
|
||||
};
|
||||
};
|
||||
|
||||
const saveApiKey = (provider: string) => {
|
||||
const config = apiConfigs[provider];
|
||||
if (!config.apiKey) {
|
||||
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: `Please enter a valid ${config.provider} API key`,
|
||||
description: 'Failed to save configuration',
|
||||
});
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(`${provider}_api_key`, config.apiKey);
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: `${config.provider} API key saved successfully`,
|
||||
});
|
||||
};
|
||||
|
||||
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 */}
|
||||
|
|
@ -69,31 +127,51 @@ const SettingsPage = () => {
|
|||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{Object.entries(apiConfigs).map(([key, config]) => (
|
||||
<div key={key} className="border-b border-gray-100 last:border-0 pb-6 last:pb-0">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{config.provider} API Key
|
||||
</label>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={config.apiKey}
|
||||
onChange={(e) => handleApiKeyChange(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 ${config.provider} API key`}
|
||||
/>
|
||||
<button
|
||||
onClick={() => saveApiKey(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 {config.provider} services
|
||||
</p>
|
||||
{/* 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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue