mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c44804d50 | |||
| 8589376c66 | |||
| d53a8b4941 | |||
| af819da623 |
@@ -0,0 +1,5 @@
|
||||
From root:
|
||||
```bash
|
||||
BIG_AGI_BUILD=standalone next build
|
||||
electron . --enable-logging
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
background: #2e2c29;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.loader-container {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 5px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 5px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: url('tray-icon.png') no-repeat center center;
|
||||
background-size: contain;
|
||||
animation: counter-spin 3.33s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes counter-spin {
|
||||
0% { transform: rotate(360deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="loader-container">
|
||||
<div class="spinner"></div>
|
||||
<div class="logo"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,178 @@
|
||||
const { app, BrowserWindow, Tray, Menu, ipcMain, screen, nativeTheme, shell } = require('electron');
|
||||
const path = require('path');
|
||||
const startServer = require('./server.js');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
|
||||
let mainWindow;
|
||||
let tray;
|
||||
const port = 3000;
|
||||
|
||||
async function createWindow() {
|
||||
try {
|
||||
console.log('Starting server...');
|
||||
await startServer(port);
|
||||
console.log('Server started successfully');
|
||||
|
||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
|
||||
|
||||
// // Set up a loading screen
|
||||
// loadingScreen = new BrowserWindow({
|
||||
// // width: 150,
|
||||
// // height: 150,
|
||||
// frame: false,
|
||||
// transparent: false,
|
||||
// alwaysOnTop: true,
|
||||
// webPreferences: {
|
||||
// nodeIntegration: true,
|
||||
// },
|
||||
// backgroundColor: '#2e2c29',
|
||||
// });
|
||||
//
|
||||
// loadingScreen.loadFile(path.join(__dirname, 'loading.html'));
|
||||
// loadingScreen.center();
|
||||
// console.log('Loading screen created');
|
||||
|
||||
console.log('Preload script path:', path.join(__dirname, 'preload.js'));
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: Math.min(1280, width * 0.8),
|
||||
height: Math.min(800, height * 0.8),
|
||||
minWidth: 430,
|
||||
minHeight: 600,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
sandbox: false,
|
||||
devTools: false,
|
||||
},
|
||||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#1a1a1a' : '#ffffff',
|
||||
show: true,
|
||||
frame: false,
|
||||
titleBarStyle: 'hidden',
|
||||
icon: path.join(__dirname, 'tray-icon.png'),
|
||||
// New "insane" features:
|
||||
// transparent: true, // Enable window transparency
|
||||
vibrancy: 'under-window', // Add vibrancy effect (macOS only)
|
||||
visualEffectState: 'active', // Keep vibrancy active even when not focused (macOS only)
|
||||
roundedCorners: true, // Enable rounded corners (macOS only)
|
||||
// thickFrame: false, // Use a thinner frame on Windows
|
||||
autoHideMenuBar: true, // Auto-hide the menu bar, press Alt to show it
|
||||
scrollBounce: true, // Enable bounce effect when scrolling (macOS only)
|
||||
});
|
||||
|
||||
mainWindow.removeMenu();
|
||||
mainWindow.setTitle('Your Professional App Name');
|
||||
|
||||
console.log('Attempting to load main window URL...');
|
||||
await mainWindow.loadURL(`http://localhost:${port}`);
|
||||
console.log('Main window URL loaded successfully');
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
console.log('Main window ready to show');
|
||||
// if (loadingScreen) {
|
||||
// loadingScreen.close();
|
||||
// }
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
});
|
||||
|
||||
createTray();
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
|
||||
// Handle window state
|
||||
let isQuitting = false;
|
||||
mainWindow.on('close', (event) => {
|
||||
if (!isQuitting) {
|
||||
event.preventDefault();
|
||||
mainWindow.hide();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
isQuitting = true;
|
||||
});
|
||||
|
||||
// Adjust window behavior
|
||||
mainWindow.on('maximize', () => {
|
||||
mainWindow.webContents.send('window-maximized');
|
||||
});
|
||||
|
||||
mainWindow.on('unmaximize', () => {
|
||||
mainWindow.webContents.send('window-unmaximized');
|
||||
});
|
||||
|
||||
|
||||
// Warn if preloads fail
|
||||
mainWindow.webContents.on('preload-error', (event, preloadPath, error) => {
|
||||
console.error('Preload error:', preloadPath, error);
|
||||
});
|
||||
|
||||
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
|
||||
console.error('Failed to load:', errorCode, errorDescription);
|
||||
});
|
||||
|
||||
|
||||
// Handle external links
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error in createWindow:', err);
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
tray = new Tray(path.join(__dirname, 'tray-icon.png'));
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: 'Show App', click: () => mainWindow.show() },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Quit', click: () => app.quit() },
|
||||
]);
|
||||
tray.setToolTip('Your Professional App Name');
|
||||
tray.setContextMenu(contextMenu);
|
||||
|
||||
tray.on('click', () => {
|
||||
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
console.log('App is ready, creating window...');
|
||||
createWindow().catch((err) => {
|
||||
console.error('Failed to create window:', err);
|
||||
app.quit();
|
||||
});
|
||||
|
||||
app.on('activate', function() {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', function() {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
// IPC handlers for window controls
|
||||
ipcMain.on('minimize-window', () => mainWindow.minimize());
|
||||
ipcMain.on('maximize-window', () => {
|
||||
if (mainWindow.isMaximized()) {
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
});
|
||||
ipcMain.on('close-window', () => mainWindow.close());
|
||||
|
||||
|
||||
// Auto-updater events
|
||||
autoUpdater.on('update-available', () => {
|
||||
mainWindow.webContents.send('update_available');
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
mainWindow.webContents.send('update_downloaded');
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
const { contextBridge, desktopCapturer, ipcRenderer } = require('electron');
|
||||
const { readFileSync } = require('fs');
|
||||
const { join } = require('path');
|
||||
|
||||
// Main bridge
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
sendEvent: (event) => ipcRenderer.send('app-event', event),
|
||||
onUpdateAvailable: (callback) => ipcRenderer.on('update_available', callback),
|
||||
onUpdateDownloaded: (callback) => ipcRenderer.on('update_downloaded', callback),
|
||||
});
|
||||
|
||||
|
||||
// Screen Capture: inject renderer.js into the web page
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('Screen Capture: Injecting renderer.js into the web page');
|
||||
const rendererScript = document.createElement('script');
|
||||
rendererScript.text = readFileSync(join(__dirname, 'renderer.js'), 'utf8');
|
||||
document.body.appendChild(rendererScript);
|
||||
});
|
||||
|
||||
// Screen Capture: expose desktopCapturer to the web page
|
||||
contextBridge.exposeInMainWorld('myCustomGetDisplayMedia', async () => {
|
||||
console.log('Screen Capture: Calling desktopCapturer.getSources');
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['window', 'screen'],
|
||||
});
|
||||
|
||||
console.log('Available sources:', sources);
|
||||
|
||||
// you should create some kind of UI to prompt the user
|
||||
// to select the correct source like Google Chrome does
|
||||
// this is just for testing purposes
|
||||
return sources[0];
|
||||
});
|
||||
|
||||
console.log('Preload script loaded');
|
||||
@@ -0,0 +1,30 @@
|
||||
// https://github.com/aabuhijleh/override-getDisplayMedia/blob/main/renderer.js
|
||||
|
||||
// This file is required by the index.html file and will
|
||||
// be executed in the renderer process for that window.
|
||||
// No Node.js APIs are available in this process because
|
||||
// `nodeIntegration` is turned off. Use `preload.js` to
|
||||
// selectively enable features needed in the rendering
|
||||
// process.
|
||||
|
||||
// override getDisplayMedia
|
||||
navigator.mediaDevices.getDisplayMedia = async () => {
|
||||
const selectedSource = await globalThis.myCustomGetDisplayMedia();
|
||||
|
||||
// create MediaStream
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: false,
|
||||
video: {
|
||||
mandatory: {
|
||||
chromeMediaSource: 'desktop',
|
||||
chromeMediaSourceId: selectedSource.id,
|
||||
minWidth: 1280,
|
||||
maxWidth: 1280,
|
||||
minHeight: 720,
|
||||
maxHeight: 720,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return stream;
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
const { createServer } = require('http');
|
||||
const { parse } = require('url');
|
||||
const next = require('next');
|
||||
const path = require('path');
|
||||
|
||||
// const dev = process.env.NODE_ENV !== 'production';
|
||||
const dir = path.join(__dirname, '..'); // This points to the root of your project
|
||||
const app = next({ dev: false, dir });
|
||||
const handle = app.getRequestHandler();
|
||||
|
||||
function startServer(port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
app.prepare()
|
||||
.then(() => {
|
||||
const server = createServer((req, res) => {
|
||||
// Basic request logging
|
||||
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
|
||||
|
||||
// Simple rate limiting
|
||||
if (rateLimiter(req)) {
|
||||
res.statusCode = 429;
|
||||
res.end('Too Many Requests');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the request
|
||||
const parsedUrl = parse(req.url, true);
|
||||
handle(req, res, parsedUrl);
|
||||
});
|
||||
|
||||
server.listen(port, (err) => {
|
||||
if (err) reject(err);
|
||||
console.log(`> Ready on http://localhost:${port}`);
|
||||
resolve(server);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('SIGTERM signal received: closing HTTP server');
|
||||
server.close(() => {
|
||||
console.log('HTTP server closed');
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(err => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
// Simple in-memory rate limiter
|
||||
const MAX_REQUESTS_PER_MINUTE = 100;
|
||||
const requestCounts = new Map();
|
||||
|
||||
function rateLimiter(req) {
|
||||
const ip = req.socket.remoteAddress;
|
||||
const now = Date.now();
|
||||
const windowStart = now - 60000; // 1 minute ago
|
||||
|
||||
const requestTimestamps = requestCounts.get(ip) || [];
|
||||
const requestsInWindow = requestTimestamps.filter(timestamp => timestamp > windowStart);
|
||||
|
||||
if (requestsInWindow.length >= MAX_REQUESTS_PER_MINUTE) {
|
||||
return true; // Rate limit exceeded
|
||||
}
|
||||
|
||||
requestTimestamps.push(now);
|
||||
requestCounts.set(ip, requestTimestamps);
|
||||
|
||||
return false; // Rate limit not exceeded
|
||||
}
|
||||
|
||||
module.exports = startServer;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 993 B |
+1
-1
@@ -13,7 +13,7 @@ let nextConfig = {
|
||||
// [exports] https://nextjs.org/docs/advanced-features/static-html-export
|
||||
...buildType && {
|
||||
output: buildType,
|
||||
distDir: 'dist',
|
||||
// distDir: 'dist',
|
||||
|
||||
// disable image optimization for exports
|
||||
images: { unoptimized: true },
|
||||
|
||||
Generated
+3383
-16
File diff suppressed because it is too large
Load Diff
+29
-4
@@ -4,15 +4,20 @@
|
||||
"private": true,
|
||||
"author": "Enrico Ros <enrico.ros@gmail.com>",
|
||||
"repository": "https://github.com/enricoros/big-agi",
|
||||
"main": "electron/main.js",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "node electron/server.js",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start": "NODE_ENV=production node electron/server.js",
|
||||
"lint": "next lint",
|
||||
"postinstall": "prisma generate",
|
||||
"db:push": "prisma db push",
|
||||
"db:studio": "prisma studio",
|
||||
"vercel:env:pull": "npx vercel env pull .env.development.local"
|
||||
"vercel:env:pull": "npx vercel env pull .env.development.local",
|
||||
"electron": "electron .",
|
||||
"electron-dev": "concurrently \"npm run dev\" \"electron .\"",
|
||||
"electron-build": "next build && electron-builder",
|
||||
"electron-start": "npm run build && electron ."
|
||||
},
|
||||
"prisma": {
|
||||
"schema": "src/server/prisma/schema.prisma"
|
||||
@@ -41,6 +46,7 @@
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"dexie": "^4.0.7",
|
||||
"dexie-react-hooks": "^1.1.7",
|
||||
"electron-updater": "^6.2.1",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"nanoid": "^5.0.7",
|
||||
@@ -82,6 +88,9 @@
|
||||
"@types/react-katex": "^3.0.4",
|
||||
"@types/react-timeago": "^4.1.7",
|
||||
"@types/turndown": "^5.0.4",
|
||||
"concurrently": "^8.2.2",
|
||||
"electron": "^31.1.0",
|
||||
"electron-builder": "^24.13.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^14.2.4",
|
||||
"prettier": "^3.3.2",
|
||||
@@ -90,5 +99,21 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^18.0.0"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.yourcompany.yourappname",
|
||||
"productName": "Your App Name",
|
||||
"files": [
|
||||
"electron/**/*",
|
||||
".next/**/*",
|
||||
"public/**/*",
|
||||
"next.config.js"
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "electron"
|
||||
},
|
||||
"extraMetadata": {
|
||||
"main": "electron/main.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user