diff --git a/app/main.ts b/app/main.ts index 68986814..6da4995b 100644 --- a/app/main.ts +++ b/app/main.ts @@ -1,17 +1,21 @@ require("dotenv").config(); import { app, BrowserWindow } from "electron"; import path from "path"; -import { findTwoUnusedPorts, killProcess } from "./utils"; +import { createUserConfig, findTwoUnusedPorts, killProcess } from "./utils"; import { startFastApiServer, startNextJsServer } from "./servers"; import { ChildProcessByStdio } from "child_process"; import { localhost } from "./constants"; -var isDev = process.env.DEBUG === "True"; +var isDev = !app.isPackaged; var baseDir = isDev ? process.cwd() : process.resourcesPath; var resourcesDir = path.join(baseDir, "resources"); var fastapiDir = isDev ? path.join(baseDir, "servers/fastapi") : path.join(resourcesDir, "fastapi"); var nextjsDir = isDev ? path.join(baseDir, "servers/nextjs") : path.join(resourcesDir, "nextjs"); +var tempDir = app.getPath("temp"); +var dataDir = app.getPath("userData"); +var userConfigPath = path.join(dataDir, "userConfig.json"); + var win: BrowserWindow | undefined; var fastApiProcess: ChildProcessByStdio | undefined; var nextjsProcess: ChildProcessByStdio | undefined; @@ -33,13 +37,13 @@ async function startServers(fastApiPort: number, nextjsPort: number) { fastApiPort, { DEBUG: isDev ? "True" : "False", - LLM: process.env.LLM || "", - LIBREOFFICE: process.env.LIBREOFFICE || "", - OPENAI_API_KEY: process.env.OPENAI_API_KEY || "", - GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || "", - APP_DATA_DIRECTORY: process.env.APP_DATA_DIRECTORY || "", - TEMP_DIRECTORY: process.env.TEMP_DIRECTORY || "", - + LLM: process.env.LLM, + LIBREOFFICE: process.env.LIBREOFFICE, + OPENAI_API_KEY: process.env.OPENAI_API_KEY, + GOOGLE_API_KEY: process.env.GOOGLE_API_KEY, + APP_DATA_DIRECTORY: dataDir, + TEMP_DIRECTORY: tempDir, + USER_CONFIG_PATH: userConfigPath, }, isDev ); @@ -48,8 +52,9 @@ async function startServers(fastApiPort: number, nextjsPort: number) { nextjsPort, { NEXT_PUBLIC_FAST_API: `${localhost}:${fastApiPort}`, - TEMP_DIRECTORY: process.env.TEMP_DIRECTORY || "", + TEMP_DIRECTORY: tempDir, NEXT_PUBLIC_URL: `${localhost}:${nextjsPort}`, + USER_CONFIG_PATH: userConfigPath, }, isDev ); @@ -70,8 +75,15 @@ async function stopServers() { app.whenReady().then(async () => { createWindow(); win?.loadFile(path.join(resourcesDir, "ui/homepage/index.html")); + win?.webContents.openDevTools(); + createUserConfig(app, { + LLM: process.env.LLM, + OPENAI_API_KEY: process.env.OPENAI_API_KEY, + GOOGLE_API_KEY: process.env.GOOGLE_API_KEY, + }) + const [fastApiPort, nextjsPort] = await findTwoUnusedPorts(); console.log(`FastAPI port: ${fastApiPort}, NextJS port: ${nextjsPort}`); diff --git a/app/types/index.d.ts b/app/types/index.d.ts index d59e0ad4..70f53e4e 100644 --- a/app/types/index.d.ts +++ b/app/types/index.d.ts @@ -1,15 +1,23 @@ interface FastApiEnv { - DEBUG: string, - LLM: string, - LIBREOFFICE: string, - OPENAI_API_KEY: string, - GOOGLE_API_KEY: string, - APP_DATA_DIRECTORY: string, - TEMP_DIRECTORY: string, + DEBUG?: string, + LLM?: string, + LIBREOFFICE?: string, + OPENAI_API_KEY?: string, + GOOGLE_API_KEY?: string, + APP_DATA_DIRECTORY?: string, + TEMP_DIRECTORY?: string, + USER_CONFIG_PATH?: string, } interface NextJsEnv { - NEXT_PUBLIC_FAST_API: string, - TEMP_DIRECTORY: string, - NEXT_PUBLIC_URL: string, + NEXT_PUBLIC_FAST_API?: string, + TEMP_DIRECTORY?: string, + NEXT_PUBLIC_URL?: string, + USER_CONFIG_PATH?: string, +} + +interface UserConfig { + LLM?: string, + OPENAI_API_KEY?: string, + GOOGLE_API_KEY?: string, } \ No newline at end of file diff --git a/app/utils.ts b/app/utils.ts index 6bf8be26..7c5becbc 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -1,5 +1,34 @@ import net from 'net' import treeKill from 'tree-kill' +import { exec } from 'child_process' +import { promisify } from 'util' +import { platform } from 'os' +import type { App } from "electron" +import fs from 'fs' +import path from 'path' + +const execAsync = promisify(exec) + +export function createUserConfig(app: App, userConfig: UserConfig) { + const dataDir = app.getPath("userData") + const configPath = path.join(dataDir, "userConfig.json") + + let existingConfig: UserConfig = {} + + if (fs.existsSync(configPath)) { + const configData = fs.readFileSync(configPath, 'utf-8') + existingConfig = JSON.parse(configData) + } + + const mergedConfig: UserConfig = { + LLM: userConfig.LLM || existingConfig.LLM, + OPENAI_API_KEY: userConfig.OPENAI_API_KEY || existingConfig.OPENAI_API_KEY, + GOOGLE_API_KEY: userConfig.GOOGLE_API_KEY || existingConfig.GOOGLE_API_KEY, + } + + fs.writeFileSync(configPath, JSON.stringify(mergedConfig)) +} + export function killProcess(pid: number) { return new Promise((resolve, reject) => { @@ -41,4 +70,5 @@ export async function findTwoUnusedPorts(startPort: number = 40000): Promise<[nu } return [ports[0], ports[1]]; -} \ No newline at end of file +} + diff --git a/forge.config.js b/forge.config.js deleted file mode 100644 index 0bd18d97..00000000 --- a/forge.config.js +++ /dev/null @@ -1,47 +0,0 @@ -const { FusesPlugin } = require('@electron-forge/plugin-fuses'); -const { FuseV1Options, FuseVersion } = require('@electron/fuses'); - -module.exports = { - packagerConfig: { - asar: true, - extraResource: [ - 'resources', - ] - }, - rebuildConfig: {}, - makers: [ - // { - // name: '@electron-forge/maker-squirrel', - // config: {}, - // }, - // { - // name: '@electron-forge/maker-zip', - // platforms: ['darwin'], - // }, - { - name: '@electron-forge/maker-deb', - config: {}, - }, - // { - // name: '@electron-forge/maker-rpm', - // config: {}, - // }, - ], - plugins: [ - { - name: '@electron-forge/plugin-auto-unpack-natives', - config: {}, - }, - // Fuses are used to enable/disable various Electron functionality - // at package time, before code signing the application - new FusesPlugin({ - version: FuseVersion.V1, - [FuseV1Options.RunAsNode]: false, - [FuseV1Options.EnableCookieEncryption]: true, - [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, - [FuseV1Options.EnableNodeCliInspectArguments]: false, - [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, - [FuseV1Options.OnlyLoadAppFromAsar]: true, - }), - ], -}; diff --git a/servers/fastapi/api/main.py b/servers/fastapi/api/main.py index ab879b78..ce4f0e9e 100644 --- a/servers/fastapi/api/main.py +++ b/servers/fastapi/api/main.py @@ -1,11 +1,12 @@ import os -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from sqlmodel import SQLModel from contextlib import asynccontextmanager from api.routers.presentation.router import presentation_router from api.services.database import sql_engine +from api.utils import update_env_with_user_config @asynccontextmanager @@ -24,4 +25,12 @@ app.add_middleware( allow_methods=["*"], allow_headers=["*"], ) + + +@app.middleware("http") +async def update_env_middleware(request: Request, call_next): + update_env_with_user_config() + return await call_next(request) + + app.include_router(presentation_router) diff --git a/servers/fastapi/api/models.py b/servers/fastapi/api/models.py index 89513110..06a0dd23 100644 --- a/servers/fastapi/api/models.py +++ b/servers/fastapi/api/models.py @@ -35,3 +35,9 @@ class SSEResponse(BaseModel): def to_string(self): return f"event: {self.event}\ndata: {self.data}\n\n" + + +class UserConfig(BaseModel): + LLM: Optional[str] = None + OPENAI_API_KEY: Optional[str] = None + GOOGLE_API_KEY: Optional[str] = None diff --git a/servers/fastapi/api/utils.py b/servers/fastapi/api/utils.py index 383bf5a8..31646834 100644 --- a/servers/fastapi/api/utils.py +++ b/servers/fastapi/api/utils.py @@ -1,4 +1,5 @@ import asyncio +import json import os import traceback from typing import List, Optional @@ -7,7 +8,7 @@ import aiohttp from fastapi import HTTPException, UploadFile from fastapi.responses import StreamingResponse -from api.models import LogMetadata +from api.models import LogMetadata, UserConfig from api.services.logging import LoggingService @@ -17,6 +18,35 @@ def get_presentation_dir(presentation_id: str) -> str: return presentation_dir +def get_user_config(): + user_config_path = os.getenv("USER_CONFIG_PATH") + + existing_config = UserConfig() + try: + if os.path.exists(user_config_path): + with open(user_config_path, "r") as f: + existing_config = UserConfig(**json.load(f)) + except Exception as e: + print("Error while loading user config") + pass + + return UserConfig( + LLM=os.getenv("LLM") or existing_config.LLM, + OPENAI_API_KEY=os.getenv("OPENAI_API_KEY") or existing_config.OPENAI_API_KEY, + GOOGLE_API_KEY=os.getenv("GOOGLE_API_KEY") or existing_config.GOOGLE_API_KEY, + ) + + +def update_env_with_user_config(): + user_config = get_user_config() + if user_config.LLM: + os.environ["LLM"] = user_config.LLM + if user_config.OPENAI_API_KEY: + os.environ["OPENAI_API_KEY"] = user_config.OPENAI_API_KEY + if user_config.GOOGLE_API_KEY: + os.environ["GOOGLE_API_KEY"] = user_config.GOOGLE_API_KEY + + def replace_file_name(old_name: str, new_name: str) -> str: splitted = old_name.split(".") if len(splitted) < 1: diff --git a/servers/fastapi/ppt_config_generator/document_summary_generator.py b/servers/fastapi/ppt_config_generator/document_summary_generator.py index dc60e51e..01aa3261 100644 --- a/servers/fastapi/ppt_config_generator/document_summary_generator.py +++ b/servers/fastapi/ppt_config_generator/document_summary_generator.py @@ -8,7 +8,6 @@ from langchain_core.prompts import ChatPromptTemplate from langchain_core.messages import BaseMessage from langchain_text_splitters import CharacterTextSplitter - sysmte_prompt = """ Generate a blog-style summary of the provided document in **more than 2000 words**, focusing on **prominently featuring any numerical data and statistics**. Maintain as much information as possible. diff --git a/servers/fastapi/ppt_generator/generator.py b/servers/fastapi/ppt_generator/generator.py index b264f1f5..d76ce716 100644 --- a/servers/fastapi/ppt_generator/generator.py +++ b/servers/fastapi/ppt_generator/generator.py @@ -78,6 +78,7 @@ def generate_presentation_stream( language: str, summary: str, ) -> AsyncIterator[AIMessageChunk]: + schema = LLMPresentationModel.model_json_schema() system_prompt = f"{CREATE_PRESENTATION_PROMPT} -|0|--|0|- Follow this schema while giving out response: {schema}. Make description short and obey the character limits. Output should be in JSON format. Give out only JSON, nothing else."