Merge pull request #5 from presenton/shared_user_config_implementation

Adds: shared user config
This commit is contained in:
Saurav Niraula 2025-05-11 03:31:21 +05:45 committed by GitHub
commit d01165f06b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 119 additions and 71 deletions

View file

@ -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<any, any, any> | undefined;
var nextjsProcess: ChildProcessByStdio<any, any, any> | 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}`);

28
app/types/index.d.ts vendored
View file

@ -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,
}

View file

@ -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]];
}
}

View file

@ -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,
}),
],
};

View file

@ -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)

View file

@ -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

View file

@ -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:

View file

@ -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.

View file

@ -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."