741 lines
21 KiB
JavaScript
741 lines
21 KiB
JavaScript
const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron')
|
|
const path = require('path')
|
|
const { is } = require('@electron-toolkit/utils')
|
|
const fs = require('fs')
|
|
const { execFile } = require('child_process')
|
|
const { promisify } = require('util')
|
|
|
|
let mainWindow
|
|
let settingsWindow = null
|
|
const execFileAsync = promisify(execFile)
|
|
|
|
const DEFAULT_BACKEND_API_URL = 'http://127.0.0.1:8000'
|
|
const DEFAULT_OLLAMA_API_URL = 'http://127.0.0.1:11434'
|
|
const DEFAULT_EMBED_MODEL = 'nomic-embed-text:latest'
|
|
const DEFAULT_RERANK_MODEL = DEFAULT_EMBED_MODEL
|
|
const DEFAULT_TRANSCRIPTION_MODEL = 'base'
|
|
const BGE_EMBED_MODEL = 'bge-m3:latest'
|
|
const REPO_ROOT = path.resolve(__dirname, '..')
|
|
const UPDATE_REMOTE_URL = 'https://giers10.uber.space/giers10/Heimgeist.git'
|
|
const UPDATE_BRANCH = 'master'
|
|
const GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0' }
|
|
const DEV_WRAPPER_RELAUNCH_CODE = Number(process.env.HEIMGEIST_DEV_RELAUNCH_CODE || 75)
|
|
const settingsFilePath = process.env.HEIMGEIST_SETTINGS_FILE || path.join(app.getPath('userData'), 'settings.json')
|
|
let appSettings = {}
|
|
let lastUpdateCheckResult = null
|
|
let activeUpdateCheck = null
|
|
const DEFAULT_UI_SCALE = 1
|
|
const MIN_UI_SCALE = 0.7
|
|
const MAX_UI_SCALE = 1.3
|
|
const DEFAULT_OPEN_DEVTOOLS_ON_STARTUP = false
|
|
const DEFAULT_AUDIO_INPUT_ENABLED = true
|
|
const DEFAULT_AUDIO_INPUT_DEVICE_ID = ''
|
|
const DEFAULT_AUDIO_INPUT_LANGUAGE = ''
|
|
const CHANGELOG_PAGE_SIZE = 50
|
|
const CHANGELOG_MESSAGE_MAX_LENGTH = 500
|
|
|
|
const defaultSettings = {
|
|
backendApiUrl: DEFAULT_BACKEND_API_URL,
|
|
ollamaApiUrl: DEFAULT_OLLAMA_API_URL,
|
|
chatModel: 'llama3',
|
|
visionModel: '',
|
|
embedModel: DEFAULT_EMBED_MODEL,
|
|
rerankModel: DEFAULT_RERANK_MODEL,
|
|
transcriptionModel: DEFAULT_TRANSCRIPTION_MODEL,
|
|
colorScheme: 'Default',
|
|
uiScale: DEFAULT_UI_SCALE,
|
|
openDevToolsOnStartup: DEFAULT_OPEN_DEVTOOLS_ON_STARTUP,
|
|
audioInputEnabled: DEFAULT_AUDIO_INPUT_ENABLED,
|
|
audioInputDeviceId: DEFAULT_AUDIO_INPUT_DEVICE_ID,
|
|
audioInputLanguage: DEFAULT_AUDIO_INPUT_LANGUAGE,
|
|
}
|
|
|
|
function normalizeEmbedModel(value) {
|
|
const trimmed = String(value || '').trim()
|
|
if (!trimmed) {
|
|
return DEFAULT_EMBED_MODEL
|
|
}
|
|
|
|
const lowered = trimmed.toLowerCase()
|
|
if (lowered === 'bge' || lowered === 'bge-m3' || lowered === BGE_EMBED_MODEL) {
|
|
return BGE_EMBED_MODEL
|
|
}
|
|
if (lowered === 'nomic' || lowered === 'nomic-embed-text' || lowered === DEFAULT_EMBED_MODEL) {
|
|
return DEFAULT_EMBED_MODEL
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
function normalizeRerankModel(value) {
|
|
return normalizeEmbedModel(value)
|
|
}
|
|
|
|
function normalizeModelName(value, fallback = '') {
|
|
const trimmed = String(value || '').trim()
|
|
return trimmed || fallback
|
|
}
|
|
|
|
function normalizeTranscriptionModel(value) {
|
|
return normalizeModelName(value, DEFAULT_TRANSCRIPTION_MODEL)
|
|
}
|
|
|
|
function looksLikeOllamaUrl(value) {
|
|
if (typeof value !== 'string') {
|
|
return false
|
|
}
|
|
|
|
try {
|
|
const parsed = new URL(value)
|
|
if (parsed.port === '11434') {
|
|
return true
|
|
}
|
|
|
|
return /^\/api\/?$/i.test(parsed.pathname || '')
|
|
} catch (_error) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function migrateSettings(rawSettings) {
|
|
const source = rawSettings && typeof rawSettings === 'object' ? rawSettings : {}
|
|
const nextSettings = { ...defaultSettings, ...source }
|
|
let migrated = false
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(source, 'backendApiUrl') && typeof source.ollamaApiUrl === 'string') {
|
|
if (looksLikeOllamaUrl(source.ollamaApiUrl)) {
|
|
nextSettings.backendApiUrl = DEFAULT_BACKEND_API_URL
|
|
nextSettings.ollamaApiUrl = source.ollamaApiUrl
|
|
} else {
|
|
nextSettings.backendApiUrl = source.ollamaApiUrl
|
|
nextSettings.ollamaApiUrl = DEFAULT_OLLAMA_API_URL
|
|
}
|
|
migrated = true
|
|
}
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(source, 'openDevToolsOnStartup')) {
|
|
migrated = true
|
|
}
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(source, 'rerankModel')) {
|
|
nextSettings.rerankModel = nextSettings.embedModel
|
|
migrated = true
|
|
}
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(source, 'visionModel')) {
|
|
nextSettings.visionModel = nextSettings.chatModel || ''
|
|
migrated = true
|
|
}
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(source, 'transcriptionModel')) {
|
|
nextSettings.transcriptionModel = DEFAULT_TRANSCRIPTION_MODEL
|
|
migrated = true
|
|
}
|
|
|
|
nextSettings.backendApiUrl = String(nextSettings.backendApiUrl || '').trim()
|
|
nextSettings.ollamaApiUrl = String(nextSettings.ollamaApiUrl || '').trim()
|
|
nextSettings.chatModel = normalizeModelName(nextSettings.chatModel)
|
|
nextSettings.visionModel = normalizeModelName(nextSettings.visionModel)
|
|
nextSettings.embedModel = normalizeEmbedModel(nextSettings.embedModel)
|
|
nextSettings.rerankModel = normalizeRerankModel(nextSettings.rerankModel)
|
|
nextSettings.transcriptionModel = normalizeTranscriptionModel(nextSettings.transcriptionModel)
|
|
nextSettings.openDevToolsOnStartup = normalizeOpenDevToolsOnStartup(nextSettings.openDevToolsOnStartup)
|
|
nextSettings.audioInputEnabled = normalizeBooleanSetting(nextSettings.audioInputEnabled)
|
|
nextSettings.audioInputDeviceId = String(nextSettings.audioInputDeviceId || '').trim()
|
|
nextSettings.audioInputLanguage = String(nextSettings.audioInputLanguage || '').trim().toLowerCase()
|
|
|
|
return { nextSettings, migrated }
|
|
}
|
|
|
|
function normalizeUiScale(value) {
|
|
const numericValue = Number(value)
|
|
if (!Number.isFinite(numericValue)) {
|
|
return DEFAULT_UI_SCALE
|
|
}
|
|
|
|
return Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Math.round(numericValue * 100) / 100))
|
|
}
|
|
|
|
function normalizeBooleanSetting(value) {
|
|
if (typeof value === 'string') {
|
|
const trimmed = value.trim().toLowerCase()
|
|
if (trimmed === 'true' || trimmed === '1' || trimmed === 'yes' || trimmed === 'on') {
|
|
return true
|
|
}
|
|
|
|
if (trimmed === 'false' || trimmed === '0' || trimmed === 'no' || trimmed === 'off' || trimmed === '') {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return value === true
|
|
}
|
|
|
|
function normalizeOpenDevToolsOnStartup(value) {
|
|
return normalizeBooleanSetting(value)
|
|
}
|
|
|
|
function applyUiScaleToWindow(window) {
|
|
if (!window || window.isDestroyed()) {
|
|
return
|
|
}
|
|
|
|
window.webContents.setZoomFactor(normalizeUiScale(appSettings.uiScale))
|
|
}
|
|
|
|
function applyUiScaleToAllWindows() {
|
|
BrowserWindow.getAllWindows().forEach(applyUiScaleToWindow)
|
|
}
|
|
|
|
function shouldAutoOpenDevTools() {
|
|
return is.dev && appSettings.openDevToolsOnStartup === true
|
|
}
|
|
|
|
function applyDevToolsPreferenceToWindow(window) {
|
|
if (!is.dev || !window || window.isDestroyed()) {
|
|
return
|
|
}
|
|
|
|
const webContents = window.webContents
|
|
if (!webContents) {
|
|
return
|
|
}
|
|
|
|
if (shouldAutoOpenDevTools()) {
|
|
if (!webContents.isDevToolsOpened()) {
|
|
webContents.openDevTools({ mode: 'detach' })
|
|
}
|
|
return
|
|
}
|
|
|
|
if (webContents.isDevToolsOpened()) {
|
|
webContents.closeDevTools()
|
|
}
|
|
}
|
|
|
|
function applyDevToolsPreferenceToAllWindows() {
|
|
BrowserWindow.getAllWindows().forEach(applyDevToolsPreferenceToWindow)
|
|
}
|
|
|
|
function loadSettings() {
|
|
try {
|
|
if (fs.existsSync(settingsFilePath)) {
|
|
const data = fs.readFileSync(settingsFilePath, 'utf8')
|
|
const { nextSettings, migrated } = migrateSettings(JSON.parse(data))
|
|
appSettings = nextSettings
|
|
if (migrated) {
|
|
saveSettings()
|
|
}
|
|
} else {
|
|
appSettings = { ...defaultSettings }
|
|
saveSettings()
|
|
}
|
|
appSettings.uiScale = normalizeUiScale(appSettings.uiScale)
|
|
} catch (error) {
|
|
console.error('Failed to load settings:', error)
|
|
appSettings = { ...defaultSettings }
|
|
}
|
|
}
|
|
|
|
function saveSettings() {
|
|
try {
|
|
fs.mkdirSync(path.dirname(settingsFilePath), { recursive: true })
|
|
fs.writeFileSync(settingsFilePath, JSON.stringify(appSettings, null, 2), 'utf8')
|
|
} catch (error) {
|
|
console.error('Failed to save settings:', error)
|
|
}
|
|
}
|
|
|
|
function setUpdateStatus(status) {
|
|
lastUpdateCheckResult = {
|
|
state: 'idle',
|
|
message: '',
|
|
checkedAt: new Date().toISOString(),
|
|
localCommit: null,
|
|
remoteCommit: null,
|
|
branch: null,
|
|
restartScheduled: false,
|
|
...status,
|
|
}
|
|
|
|
return lastUpdateCheckResult
|
|
}
|
|
|
|
async function runGitCommand(args, options = {}) {
|
|
return execFileAsync('git', ['-C', REPO_ROOT, ...args], {
|
|
env: GIT_ENV,
|
|
maxBuffer: 1024 * 1024,
|
|
timeout: options.timeout ?? 15000,
|
|
})
|
|
}
|
|
|
|
function normalizeChangelogPage(value) {
|
|
const parsed = Number.parseInt(value, 10)
|
|
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
return 1
|
|
}
|
|
|
|
return parsed
|
|
}
|
|
|
|
function truncateChangelogMessage(message) {
|
|
const normalized = String(message || '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
|
|
if (!normalized) {
|
|
return 'No commit message.'
|
|
}
|
|
|
|
if (normalized.length <= CHANGELOG_MESSAGE_MAX_LENGTH) {
|
|
return normalized
|
|
}
|
|
|
|
return `${normalized.slice(0, CHANGELOG_MESSAGE_MAX_LENGTH - 3).trimEnd()}...`
|
|
}
|
|
|
|
function parseChangelogOutput(output) {
|
|
return String(output || '')
|
|
.split('\u0000\u0000')
|
|
.map((record) => record.trim())
|
|
.filter(Boolean)
|
|
.map((record) => {
|
|
const [hash = '', committedAt = '', ...messageParts] = record.split('\u0000')
|
|
return {
|
|
hash: hash.trim(),
|
|
committedAt: committedAt.trim(),
|
|
message: truncateChangelogMessage(messageParts.join('\u0000')),
|
|
}
|
|
})
|
|
.filter((entry) => entry.hash && entry.message)
|
|
}
|
|
|
|
async function getChangelogPage(page = 1) {
|
|
const normalizedPage = normalizeChangelogPage(page)
|
|
const pageSize = CHANGELOG_PAGE_SIZE
|
|
const skip = (normalizedPage - 1) * pageSize
|
|
const { stdout } = await runGitCommand([
|
|
'log',
|
|
`--max-count=${pageSize + 1}`,
|
|
`--skip=${skip}`,
|
|
'--format=%H%x00%cI%x00%B%x00%x00',
|
|
], { timeout: 30000 })
|
|
|
|
const entries = parseChangelogOutput(stdout)
|
|
|
|
return {
|
|
page: normalizedPage,
|
|
pageSize,
|
|
hasMore: entries.length > pageSize,
|
|
entries: entries.slice(0, pageSize),
|
|
}
|
|
}
|
|
|
|
function scheduleAppRestart() {
|
|
setTimeout(() => {
|
|
if (process.env.HEIMGEIST_DEV_WRAPPER === '1') {
|
|
app.exit(DEV_WRAPPER_RELAUNCH_CODE)
|
|
return
|
|
}
|
|
|
|
app.relaunch()
|
|
app.exit(0)
|
|
}, 300)
|
|
}
|
|
|
|
function parseGitStatusPaths(statusOutput) {
|
|
return statusOutput
|
|
.split('\n')
|
|
.map((line) => line.trimEnd())
|
|
.filter(Boolean)
|
|
.map((line) => line.slice(3).trim())
|
|
}
|
|
|
|
function formatChangedPaths(paths) {
|
|
if (!Array.isArray(paths) || paths.length === 0) {
|
|
return ''
|
|
}
|
|
|
|
const preview = paths.slice(0, 3).join(', ')
|
|
return paths.length > 3 ? `${preview}, ...` : preview
|
|
}
|
|
|
|
async function performUpdateCheck(trigger = 'manual') {
|
|
if (!fs.existsSync(path.join(REPO_ROOT, '.git'))) {
|
|
return setUpdateStatus({
|
|
state: 'unavailable',
|
|
trigger,
|
|
message: 'Update check unavailable: no Git checkout found.',
|
|
})
|
|
}
|
|
|
|
setUpdateStatus({
|
|
state: 'checking',
|
|
trigger,
|
|
message: 'Checking remote repository for updates...',
|
|
})
|
|
|
|
try {
|
|
const [
|
|
{ stdout: localStdout },
|
|
{ stdout: branchStdout },
|
|
{ stdout: remoteStdout },
|
|
{ stdout: worktreeStdout },
|
|
] = await Promise.all([
|
|
runGitCommand(['rev-parse', 'HEAD']),
|
|
runGitCommand(['branch', '--show-current']),
|
|
runGitCommand(['ls-remote', UPDATE_REMOTE_URL, `refs/heads/${UPDATE_BRANCH}`]),
|
|
runGitCommand(['status', '--porcelain', '--untracked-files=no']),
|
|
])
|
|
|
|
const localCommit = localStdout.trim()
|
|
const branch = branchStdout.trim() || null
|
|
const remoteCommit = remoteStdout.trim().split(/\s+/)[0] || null
|
|
const changedPaths = parseGitStatusPaths(worktreeStdout)
|
|
const worktreeDirty = changedPaths.length > 0
|
|
|
|
if (!localCommit) {
|
|
throw new Error('Could not resolve the current local commit hash.')
|
|
}
|
|
|
|
if (!remoteCommit) {
|
|
throw new Error(`Could not resolve the remote ${UPDATE_BRANCH} commit hash.`)
|
|
}
|
|
|
|
if (localCommit === remoteCommit) {
|
|
return setUpdateStatus({
|
|
state: 'up-to-date',
|
|
trigger,
|
|
branch,
|
|
localCommit,
|
|
remoteCommit,
|
|
message: 'Heimgeist is already up to date.',
|
|
})
|
|
}
|
|
|
|
if (branch && branch !== UPDATE_BRANCH) {
|
|
return setUpdateStatus({
|
|
state: 'skipped',
|
|
trigger,
|
|
branch,
|
|
localCommit,
|
|
remoteCommit,
|
|
message: `Update skipped: current branch is "${branch}", expected "${UPDATE_BRANCH}".`,
|
|
})
|
|
}
|
|
|
|
if (worktreeDirty) {
|
|
const changedPathSummary = formatChangedPaths(changedPaths)
|
|
return setUpdateStatus({
|
|
state: 'skipped',
|
|
trigger,
|
|
branch: branch || UPDATE_BRANCH,
|
|
localCommit,
|
|
remoteCommit,
|
|
message: changedPathSummary
|
|
? `Update skipped: tracked local changes detected in ${changedPathSummary}.`
|
|
: 'Update skipped: tracked local changes detected.',
|
|
})
|
|
}
|
|
|
|
setUpdateStatus({
|
|
state: 'updating',
|
|
trigger,
|
|
branch: branch || UPDATE_BRANCH,
|
|
localCommit,
|
|
remoteCommit,
|
|
message: 'Update found. Pulling latest changes and restarting Heimgeist...',
|
|
})
|
|
|
|
await runGitCommand(['pull', '--ff-only', UPDATE_REMOTE_URL, UPDATE_BRANCH], { timeout: 120000 })
|
|
|
|
const { stdout: updatedStdout } = await runGitCommand(['rev-parse', 'HEAD'])
|
|
const updatedLocalCommit = updatedStdout.trim()
|
|
|
|
if (!updatedLocalCommit || updatedLocalCommit === localCommit) {
|
|
return setUpdateStatus({
|
|
state: 'up-to-date',
|
|
trigger,
|
|
branch: branch || UPDATE_BRANCH,
|
|
localCommit,
|
|
remoteCommit,
|
|
message: 'No newer remote update was applied. Local checkout already contains the latest pulled state.',
|
|
})
|
|
}
|
|
|
|
const result = setUpdateStatus({
|
|
state: 'updated',
|
|
trigger,
|
|
branch: branch || UPDATE_BRANCH,
|
|
localCommit: updatedLocalCommit || localCommit,
|
|
remoteCommit,
|
|
message: 'Update installed. Heimgeist restarts now.',
|
|
restartScheduled: true,
|
|
})
|
|
|
|
scheduleAppRestart()
|
|
return result
|
|
} catch (error) {
|
|
console.error('Failed to check for updates:', error)
|
|
|
|
return setUpdateStatus({
|
|
state: 'error',
|
|
trigger,
|
|
message: `Update check failed: ${error.message || String(error)}`,
|
|
})
|
|
}
|
|
}
|
|
|
|
function checkForUpdates(trigger = 'manual') {
|
|
if (!activeUpdateCheck) {
|
|
activeUpdateCheck = performUpdateCheck(trigger).finally(() => {
|
|
activeUpdateCheck = null
|
|
})
|
|
}
|
|
|
|
return activeUpdateCheck
|
|
}
|
|
|
|
async function createMainWindow() {
|
|
console.log('Electron: creating main window')
|
|
mainWindow = new BrowserWindow({
|
|
width: 1000,
|
|
height: 720,
|
|
minWidth: 680,
|
|
minHeight: 300,
|
|
show: false,
|
|
webPreferences: {
|
|
preload: path.join(__dirname, 'preload.cjs'),
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
},
|
|
})
|
|
|
|
applyUiScaleToWindow(mainWindow)
|
|
|
|
mainWindow.on('ready-to-show', () => {
|
|
mainWindow.show()
|
|
})
|
|
|
|
mainWindow.webContents.on('did-finish-load', () => {
|
|
applyUiScaleToWindow(mainWindow)
|
|
})
|
|
|
|
mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
|
|
console.error('Main window failed to load:', { errorCode, errorDescription, validatedURL })
|
|
})
|
|
|
|
mainWindow.on('focus', () => {
|
|
mainWindow.webContents.send('window-focused')
|
|
})
|
|
|
|
if (is.dev && process.env.VITE_DEV_SERVER_URL) {
|
|
console.log(`Electron: loading renderer ${process.env.VITE_DEV_SERVER_URL}`)
|
|
await mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL)
|
|
applyDevToolsPreferenceToWindow(mainWindow)
|
|
} else {
|
|
console.log('Electron: loading bundled renderer')
|
|
await mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
|
|
}
|
|
|
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
shell.openExternal(url)
|
|
return { action: 'deny' }
|
|
})
|
|
}
|
|
|
|
async function createSettingsWindow() {
|
|
if (settingsWindow) {
|
|
settingsWindow.focus()
|
|
return
|
|
}
|
|
|
|
settingsWindow = new BrowserWindow({
|
|
width: 800,
|
|
height: 600,
|
|
title: 'Settings',
|
|
parent: mainWindow,
|
|
modal: true,
|
|
show: false,
|
|
webPreferences: {
|
|
preload: path.join(__dirname, 'preload.cjs'),
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
},
|
|
})
|
|
|
|
applyUiScaleToWindow(settingsWindow)
|
|
|
|
settingsWindow.on('ready-to-show', () => {
|
|
settingsWindow.show()
|
|
})
|
|
|
|
settingsWindow.webContents.on('did-finish-load', () => {
|
|
applyUiScaleToWindow(settingsWindow)
|
|
})
|
|
|
|
settingsWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
|
|
console.error('Settings window failed to load:', { errorCode, errorDescription, validatedURL })
|
|
})
|
|
|
|
settingsWindow.on('closed', () => {
|
|
settingsWindow = null
|
|
})
|
|
|
|
if (is.dev && process.env.VITE_DEV_SERVER_URL) {
|
|
await settingsWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/settings`)
|
|
applyDevToolsPreferenceToWindow(settingsWindow)
|
|
} else {
|
|
await settingsWindow.loadFile(path.join(__dirname, '../dist/index.html'), { hash: '/settings' })
|
|
}
|
|
}
|
|
|
|
app.whenReady().then(async () => {
|
|
console.log('Electron: app ready')
|
|
loadSettings()
|
|
const startupUpdateResult = await checkForUpdates('startup')
|
|
if (startupUpdateResult?.restartScheduled) {
|
|
return
|
|
}
|
|
|
|
await createMainWindow()
|
|
|
|
const menuTemplate = [
|
|
{
|
|
label: 'File',
|
|
submenu: [
|
|
{
|
|
label: 'Settings',
|
|
accelerator: 'CmdOrCtrl+,',
|
|
click: createSettingsWindow,
|
|
},
|
|
{ type: 'separator' },
|
|
{ role: 'quit' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Edit',
|
|
submenu: [
|
|
{ role: 'undo' },
|
|
{ role: 'redo' },
|
|
{ type: 'separator' },
|
|
{ role: 'cut' },
|
|
{ role: 'copy' },
|
|
{ role: 'paste' },
|
|
{ role: 'delete' },
|
|
{ type: 'separator' },
|
|
{ role: 'selectAll' },
|
|
],
|
|
},
|
|
{
|
|
label: 'View',
|
|
submenu: [
|
|
{ role: 'reload' },
|
|
{ role: 'forcereload' },
|
|
{ role: 'toggledevtools' },
|
|
{ type: 'separator' },
|
|
{ role: 'resetzoom' },
|
|
{ role: 'zoomin' },
|
|
{ role: 'zoomout' },
|
|
{ type: 'separator' },
|
|
{ role: 'togglefullscreen' },
|
|
],
|
|
},
|
|
]
|
|
|
|
const menu = Menu.buildFromTemplate(menuTemplate)
|
|
Menu.setApplicationMenu(menu)
|
|
|
|
app.on('activate', () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
|
|
})
|
|
})
|
|
|
|
ipcMain.handle('get-settings', () => appSettings)
|
|
ipcMain.handle('get-update-status', () => lastUpdateCheckResult)
|
|
ipcMain.handle('check-for-updates', () => checkForUpdates('manual'))
|
|
ipcMain.handle('get-changelog-page', (event, page) => getChangelogPage(page))
|
|
|
|
ipcMain.handle('set-setting', (event, key, value) => {
|
|
if (key === 'uiScale') {
|
|
appSettings[key] = normalizeUiScale(value)
|
|
} else if (key === 'embedModel') {
|
|
appSettings[key] = normalizeEmbedModel(value)
|
|
} else if (key === 'rerankModel') {
|
|
appSettings[key] = normalizeRerankModel(value)
|
|
} else if (key === 'chatModel' || key === 'visionModel') {
|
|
appSettings[key] = normalizeModelName(value)
|
|
} else if (key === 'transcriptionModel') {
|
|
appSettings[key] = normalizeTranscriptionModel(value)
|
|
} else if (key === 'openDevToolsOnStartup') {
|
|
appSettings[key] = normalizeOpenDevToolsOnStartup(value)
|
|
} else if (key === 'audioInputEnabled') {
|
|
appSettings[key] = normalizeBooleanSetting(value)
|
|
} else if (key === 'audioInputDeviceId') {
|
|
appSettings[key] = String(value || '').trim()
|
|
} else if (key === 'audioInputLanguage') {
|
|
appSettings[key] = String(value || '').trim().toLowerCase()
|
|
} else {
|
|
appSettings[key] = value
|
|
}
|
|
saveSettings()
|
|
if (key === 'uiScale') {
|
|
applyUiScaleToAllWindows()
|
|
} else if (key === 'openDevToolsOnStartup') {
|
|
applyDevToolsPreferenceToAllWindows()
|
|
}
|
|
return true
|
|
})
|
|
|
|
ipcMain.handle('update-settings', (event, settings) => {
|
|
appSettings = { ...appSettings, ...settings }
|
|
appSettings.uiScale = normalizeUiScale(appSettings.uiScale)
|
|
appSettings.chatModel = normalizeModelName(appSettings.chatModel)
|
|
appSettings.visionModel = normalizeModelName(appSettings.visionModel)
|
|
appSettings.embedModel = normalizeEmbedModel(appSettings.embedModel)
|
|
appSettings.rerankModel = normalizeRerankModel(appSettings.rerankModel)
|
|
appSettings.transcriptionModel = normalizeTranscriptionModel(appSettings.transcriptionModel)
|
|
appSettings.openDevToolsOnStartup = normalizeOpenDevToolsOnStartup(appSettings.openDevToolsOnStartup)
|
|
appSettings.audioInputEnabled = normalizeBooleanSetting(appSettings.audioInputEnabled)
|
|
appSettings.audioInputDeviceId = String(appSettings.audioInputDeviceId || '').trim()
|
|
appSettings.audioInputLanguage = String(appSettings.audioInputLanguage || '').trim().toLowerCase()
|
|
saveSettings()
|
|
if (Object.prototype.hasOwnProperty.call(settings, 'uiScale')) {
|
|
applyUiScaleToAllWindows()
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(settings, 'openDevToolsOnStartup')) {
|
|
applyDevToolsPreferenceToAllWindows()
|
|
}
|
|
return true
|
|
})
|
|
|
|
ipcMain.handle('pick-paths', async (event, options = {}) => {
|
|
const dialogOptions = {
|
|
properties: ['openFile', 'multiSelections'],
|
|
}
|
|
if (Array.isArray(options?.filters) && options.filters.length > 0) {
|
|
dialogOptions.filters = options.filters
|
|
}
|
|
if (typeof options?.title === 'string' && options.title.trim()) {
|
|
dialogOptions.title = options.title.trim()
|
|
}
|
|
|
|
const result = await dialog.showOpenDialog(mainWindow, {
|
|
...dialogOptions,
|
|
})
|
|
return result.canceled ? [] : result.filePaths
|
|
})
|
|
|
|
ipcMain.handle('open-path', async (event, filePath) => {
|
|
if (!filePath) return false
|
|
const err = await shell.openPath(filePath)
|
|
return err === ''
|
|
})
|
|
|
|
ipcMain.on('open-external-link', (event, url) => {
|
|
shell.openExternal(url)
|
|
})
|
|
|
|
app.on('window-all-closed', () => {
|
|
if (process.platform !== 'darwin') app.quit()
|
|
})
|