WIP: refactoring in process

This commit is contained in:
samtiz 2025-07-07 03:47:12 +09:00
parent 454e67da4f
commit a18e93583f
46 changed files with 1966 additions and 2857 deletions

87
IPC_CHANNELS.md Normal file
View File

@ -0,0 +1,87 @@
# IPC Channel Reference
This document lists all the IPC channels used for communication between the main process and renderer processes in the Glass application. It serves as a central reference for developers.
---
## 1. Two-Way Channels (`ipcMain.handle` / `ipcRenderer.invoke`)
These channels are used for request-response communication where the renderer process expects a value to be returned from the main process.
### Window & System Management
- `get-header-position`: Gets the current [x, y] coordinates of the header window.
- `move-header-to`: Moves the header window to a specific [x, y] coordinate.
- `resize-header-window`: Resizes the header window to a specific width and height.
- `move-window-step`: Moves the header window by a predefined step in a given direction.
- `is-window-visible`: Checks if a specific feature window (e.g., 'ask') is currently visible.
- `force-close-window`: Forces a specific feature window to hide.
- `toggle-all-windows-visibility`: Toggles the visibility of all application windows.
- `toggle-feature`: Shows or hides a specific feature window (e.g., 'ask', 'listen').
- `quit-application`: Quits the entire application.
### Authentication & User
- `get-current-user`: Retrieves the full state of the current user (Firebase or local).
- `start-firebase-auth`: Opens the Firebase login flow in the system browser.
- `firebase-logout`: Initiates the user logout process.
- `save-api-key`: Saves a user-provided API key.
- `remove-api-key`: Removes the currently stored API key.
- `get-stored-api-key`: Retrieves the currently active API key.
- `get-ai-provider`: Gets the currently configured AI provider (e.g., 'openai').
### Permissions & Settings
- `get-content-protection-status`: Checks if window content protection is enabled.
- `check-system-permissions`: Checks for microphone and screen recording permissions.
- `request-microphone-permission`: Prompts the user for microphone access.
- `open-system-preferences`: Opens the macOS System Preferences for a specific privacy setting.
- `mark-permissions-completed`: Marks the initial permission setup as completed.
- `check-permissions-completed`: Checks if the initial permission setup was completed.
- `update-google-search-setting`: Updates the setting for Google Search integration.
### Data & Content
- `get-user-presets`: Fetches all custom prompt presets for the current user.
- `get-preset-templates`: Fetches the default prompt preset templates.
- `save-ask-message`: Saves a user question and AI response pair to the database.
- `get-web-url`: Gets the dynamically assigned URLs for the backend and web frontend.
### Features (Listen, Ask, etc.)
- `initialize-openai`: Initializes the STT and other AI services for a new session.
- `is-session-active`: Checks if a listen/summary session is currently active.
- `send-audio-content`: Sends a chunk of audio data from renderer to main for processing.
- `start-macos-audio`, `stop-macos-audio`: Controls the background process for system audio capture on macOS.
- `close-session`: Stops the current listen session.
- `message-sending`: Notifies the main process that an 'ask' message is being sent.
- `send-question-to-ask`: Sends a question from one feature window (e.g., listen) to the 'ask' window.
- `capture-screenshot`: Requests the main process to take a screenshot.
- `get-current-screenshot`: Retrieves the most recent screenshot.
- `adjust-window-height`: Requests the main process to resize a window to a specific height.
---
## 2. One-Way Channels (`ipcMain.on` / `ipcRenderer.send` / `webContents.send`)
These channels are used for events or commands that do not require a direct response.
### Main to Renderer (Events & Updates)
- `user-state-changed`: Notifies all windows that the user's authentication state has changed.
- `auth-failed`: Informs the UI that a Firebase authentication attempt failed.
- `session-state-changed`: Broadcasts whether a listen session is active or inactive.
- `api-key-validated`, `api-key-updated`, `api-key-removed`: Events related to the lifecycle of the API key.
- `stt-update`: Sends real-time speech-to-text transcription updates to the UI.
- `update-structured-data`: Sends processed data (summaries, topics) to the UI.
- `ask-response-chunk`, `ask-response-stream-end`: Sends streaming AI responses for the 'ask' feature.
- `window-show-animation`: Triggers a show/fade-in animation.
- `window-hide-animation`, `settings-window-hide-animation`: Triggers a hide/fade-out animation.
- `window-blur`: Notifies a window that it has lost focus.
- `window-did-show`: Confirms to a window that it is now visible.
- `click-through-toggled`: Informs the UI about the status of click-through mode.
- `start-listening-session`: Commands to control the main application view.
- `receive-question-from-assistant`: Delivers a question to the `AskView`.
- `ask-global-send`, `toggle-text-input`, `clear-ask-response`, `hide-text-input`: Commands for controlling the state of the `AskView`.
### Renderer to Main (Commands & Events)
- `header-state-changed`: Informs the `windowManager` that the header's state (e.g., `apikey` vs `app`) has changed.
- `update-keybinds`: Sends updated keybinding preferences to the main process.
- `view-changed`: Notifies the main process that the visible view in `PickleGlassApp` has changed.
- `header-animation-complete`: Lets the main process know that a show/hide animation has finished.
- `cancel-hide-window`, `show-window`, `hide-window`: Commands to manage feature window visibility from the renderer.
- `session-did-close`: Notifies the main process that the user has manually closed the listen session.

119
REFACTORING_PLAN.md Normal file
View File

@ -0,0 +1,119 @@
# DB & Architecture Refactoring Plan
## 1. Goal
DB 접근 로직을 **Electron 메인 프로세스로 중앙화**하고, **Feature 중심의 계층형 아키텍처**를 도입하여 코드의 안정성과 유지보수성을 극대화한다.
---
## 2. Architecture: AS-IS → TO-BE
```mermaid
graph TD
subgraph "AS-IS: 다중 접근 (충돌 위험)"
A_Main[Main Process] --> A_DB[(DB)]
A_Backend[Web Backend] --> A_DB
A_Browser[Browser] -- HTTP --> A_Backend
style A_Backend fill:#f99
end
subgraph "TO-BE: 단일 접근 (안정)"
B_Main[Main Process] --> B_DB[(DB)]
B_Backend[Web Backend] -- IPC --> B_Main
B_Browser[Browser] -- HTTP --> B_Backend
style B_Main fill:#9f9
end
```
---
## 3. Target Folder Structure
- `features` : 기능별로 View, Service, Repository를 그룹화하여 응집도 상승
- `common`: 여러 기능이 공유하는 Repository 및 저수준 Service 배치
```
src/
├── features/
│ └── listen/
│ ├── listenView.js # View (UI)
│ ├── listenService.js # ViewModel (Logic)
│ └── repositories/ # Model (Feature-Data)
│ ├── index.js # <- 저장소 전략 선택
│ ├── sqlite.repository.js
│ └── firebase.repository.js
└── common/
├── services/
│ └── sqliteClient.js # <- DB 연결/관리
└── repositories/ # <- 공통 데이터
├── user/
└── session/
```
---
## 4. Key Changes
- **DB 접근 단일화**
- 모든 DB Read/Write는 **Electron 메인 프로세스**에서만 수행.
- 웹 백엔드(`backend_node`)는 IPC 통신을 통해 메인 프로세스에 데이터를 요청.
- 웹 백엔드의 DB 직접 연결 (`better-sqlite3`, `db.js`) 코드 전부 제거.
- **Repository 패턴 도입**
- 모든 SQL 쿼리는 `*.repository.js` 파일로 분리 및 이동.
- **공통 Repository:** `common/repositories/` (e.g., `user`, `session`)
- **기능별 Repository:** `features/*/repositories/` (e.g., `transcript`, `summary`)
- **Service 역할 명확화**
- `*Service.js`는 비즈니스 로직만 담당. Repository를 호출하여 데이터를 요청/저장하지만, **직접 SQL을 다루지 않음.**
- **`renderer.js` 제거**
- `renderer.js`의 기능들을 각 책임에 맞는 View, Service로 이전 후 최종적으로 파일 삭제.
---
## 5. Future-Proofing: 저장소 전략 패턴
로그인 여부에 따라 `SQLite``Firebase` 저장소를 동적으로 스위칭할 수 있는 기반을 마련한다. Service 코드는 수정할 필요 없이, Repository 구현체만 추가하면 된다.
```javascript
// e.g., common/repositories/session/index.js
const sqliteRepo = require('./sqlite.repository.js');
const firebaseRepo = require('./firebase.repository.js');
// 현재 상태에 따라 적절한 저장소를 동적으로 반환
exports.getRepository = () => {
return isUserLoggedIn() ? firebaseRepo : sqliteRepo;
};
```
---
## 6. Design Pattern & Feature Structure Guide
각 Feature는 아래의 3-Layer 구조를 따르며, 이는 MVVM 패턴의 정신을 차용한 것입니다.
| 파일 / 폴더 | 계층 (Layer) | 역할 (Role) & 책임 (Responsibilities) |
| ---------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `*View.js` | **View** | - **UI 렌더링:** Service로부터 받은 데이터를 화면에 표시.<br>- **사용자 입력:** 클릭, 입력 등의 이벤트를 감지하여 Service에 명령 전달. |
| `*Service.js` | **ViewModel** | - **상태 관리:** Feature에 필요한 모든 상태(State)를 소유하고 관리.<br>- **비즈니스 로직:** View의 요청을 받아 로직 수행.<br>- **데이터 조율:** Repository를 통해 데이터를 요청/저장하고, Main 프로세스와 IPC 통신. |
| `repositories/` | **Model / Data** | - **데이터 추상화:** DB(SQLite/Firebase)의 복잡한 쿼리를 숨기고, Service에 간단한 함수(`getById`, `save` 등)를 제공. |
### 확장성 가이드: 파일이 너무 커진다면?
"단일 책임 원칙"에 따라, 한 파일이 너무 많은 일을 하게 되면 주저 없이 분리합니다.
- **Service Layer 분할:**
- **언제?** `listenService.js`가 STT 연결, 오디오 처리, 텍스트 분석 등 너무 많은 역할을 맡게 될 때.
- **어떻게?** 책임을 기준으로 더 작은 서비스로 분리합니다.
- `listenService.js`**`sttService.js`**, **`audioService.js`**, **`analysisService.js`**
- 이때, 최상위 `listenService.js`는 여러 하위 서비스를 조율하는 **"Orchestrator(조정자)"**의 역할을 맡게 됩니다.
- **View Layer 분할:**
- **언제?** `AssistantView.js` 하나의 파일이 너무 많은 UI 요소를 포함하고 복잡해질 때.
- **어떻게?** 재사용 가능한 UI 조각으로 분리합니다.
- `AssistantView.js`**`TranscriptLog.js`**, **`SummaryDisplay.js`**, **`ActionButtons.js`**
이 가이드를 따르면, 기능이 추가되거나 복잡해지더라도 코드베이스 전체가 일관되고 예측 가능한 구조를 유지할 수 있습니다.

80
REFACTORING_STATUS.md Normal file
View File

@ -0,0 +1,80 @@
# 리팩터링 진행 상황 보고서
> 이 문서는 `REFACTORING_PLAN.md`를 기준으로 현재까지의 진행 상황과 남은 과제를 추적합니다.
### Phase 1: 아키텍처 기반 구축 (완료)
이 단계의 목표는 **DB 접근을 메인 프로세스로 중앙화**하고, **계층형 아키텍처의 뼈대를 구축**하는 것이었습니다. 이 목표는 성공적으로 달성되었습니다.
- **[✓] DB 접근 단일화:**
- `dataService.js`와 웹 백엔드의 DB 직접 접근 코드를 모두 제거했습니다.
- 이제 모든 DB 접근은 메인 프로세스의 `Repository` 계층을 통해서만 안전하게 이루어집니다.
- **[✓] Repository 패턴 도입:**
- `userRepository`, `systemSettingsRepository`, `presetRepository` 등 공통 데이터 관리를 위한 저장소 계층을 성공적으로 구현하고 적용했습니다.
- **[✓] Service 역할 명확화:**
- `authService`를 도입하여 모든 인증 로직을 중앙에서 관리하도록 했습니다.
- `liveSummaryService` 등 기존 서비스들이 더 이상 직접 DB에 접근하지 않고 Repository에 의존하도록 수정했습니다.
### Phase 1.5: 인증 시스템 현대화 및 안정화 (완료)
1단계에서 구축한 뼈대 위에, 앱의 핵심인 인증 시스템을 더욱 견고하고 사용자 친화적으로 만드는 작업을 완료했습니다.
- **[✓] 완전한 인증 생명주기 관리:**
- `authService`가 Firebase 로그인 시 가상 API 키를 **자동으로 발급/저장**하고, 로그아웃 시 **자동으로 삭제**하는 전체 흐름을 구현했습니다.
- **[✓] 상태 관리 중앙화 및 전파:**
- `authService`가 **유일한 진실의 원천(Single Source of Truth)**이 되어, `isLoggedIn``hasApiKey` 상태를 명확히 구분하여 관리합니다.
- `user-state-changed`라는 단일 이벤트를 통해 모든 UI 컴포넌트가 일관된 상태 정보를 받도록 데이터 흐름을 정리했습니다.
- **[✓] 레거시 코드 제거:**
- `windowManager`와 여러 렌더러 파일에 흩어져 있던 레거시 인증 로직, API 키 캐싱, 불필요한 IPC 핸들러들을 모두 제거하여 코드베이스를 깔끔하게 정리했습니다.
- **[✓] 사용자 경험 최적화:**
- 로그인 세션을 영속화하여 **자동 로그인**을 구현했으며, 느린 네트워크 요청을 **비동기 백그라운드 처리**하여 UI 반응성을 극대화했습니다.
---
### Phase 2: 기능별 모듈화 및 레거시 코드 완전 제거 (진행 예정)
핵심 아키텍처가 안정화된 지금, 남아있는 기술 부채를 청산하고 앱 전체에 일관된 구조를 적용하는 마지막 단계를 진행합니다.
#### 1. `renderer.js` 책임 분산 및 제거 (최우선 과제)
- **최종 목표:** 거대 파일 `renderer.js` (1100+줄)를 완전히 제거하여, 각 기능의 UI 로직이 독립적인 모듈에서 관리되도록 합니다.
- **현황:** 현재 `renderer.js``ask` 기능의 AI 메시지 전송, `listen` 기능의 오디오/스크린 캡처 및 전처리, 상태 관리 등 여러 책임이 복잡하게 얽혀있습니다.
- **수행 작업:**
1. **`ask` 기능 책임 이전:**
- `renderer.js``sendMessage`, `PICKLE_GLASS_SYSTEM_PROMPT`, 관련 유틸 함수(`formatRealtimeConversationHistory`, `getCurrentScreenshot` 등)를 `features/ask/askService.js` (또는 유사 모듈)로 이전합니다.
- `liveSummaryService.js``save-ask-message` IPC 핸들러 로직을 `features/ask/repositories/askRepository.js`를 사용하는 `askService.js`로 이동시킵니다.
2. **`listen` 기능 책임 이전:**
- `renderer.js``startCapture`, `stopCapture`, `setupMicProcessing` 등 오디오/스크린 캡처 및 처리 관련 로직을 `features/listen/` 폴더 내의 적절한 파일 (예: `listenView.js` 또는 신규 `listenCaptureService.js`)로 이전합니다.
3. **`renderer.js` 최종 제거:**
- 위 1, 2번 작업이 완료되어 `renderer.js`에 더 이상 의존하는 코드가 없을 때, 이 파일을 프로젝트에서 **삭제**합니다.
#### 2. `listen` 기능 심화 리팩터링
- **목표:** 거대해진 `liveSummaryService.js`를 단일 책임 원칙에 따라 여러 개의 작은 서비스로 분리하여 유지보수성을 극대화합니다.
- **수행 작업:**
- `sttService.js`: STT 세션 연결 및 데이터 스트리밍을 전담합니다.
- `analysisService.js`: STT 텍스트를 받아 AI 모델에게 분석/요약을 요청하는 로직을 전담합니다.
- `listenService.js`: 상위 서비스로서, `sttService``analysisService`를 조율하고 전체 "Listen" 기능의 상태를 관리하는 **조정자(Orchestrator)** 역할을 수행합니다.
#### 3. `windowManager` 모듈화
- **목표:** 비대해진 `windowManager.js`의 책임들을 기능별로 분리하여, 각 모듈이 자신의 창 관련 로직만 책임지도록 구조를 개선합니다.
- **현황:** 현재 `windowManager.js`는 모든 창의 생성, 위치 계산, 단축키 등록, IPC 핸들링 등 너무 많은 역할을 수행하고 있습니다.
- **수행 작업:**
- **`windowLayoutManager.js`:** 여러 창의 위치를 동적으로 계산하고 배치하는 로직을 분리합니다.
- **`shortcutManager.js`:** 전역 단축키를 등록하고 관리하는 로직을 분리합니다.
- **`featureWindowManager.js` (가칭):** `ask`, `listen` 등 각 기능 창의 생성/소멸 및 관련 IPC 핸들러를 관리하는 로직을 분리합니다.
- `windowManager.js`: 최상위 관리자로서, `header` 창과 각 하위 관리자 모듈을 총괄하는 역할만 남깁니다.
---
### Phase 3: 최종 정리 및 고도화 (향후)
- **[ ] 전체 기능 회귀 테스트:** 모든 리팩터링 완료 후, 앱의 모든 기능이 의도대로 동작하는지 검증합니다.
- **[ ] 코드 클린업:** 디버깅용 `console.log`, 불필요한 주석 등을 최종적으로 정리합니다.
- **[ ] 문서 최신화:** `IPC_CHANNELS.md`를 포함한 모든 관련 문서를 최종 아키텍처에 맞게 업데이트합니다.

View File

@ -1,9 +1,9 @@
const express = require('express');
const cors = require('cors');
const db = require('./db');
// const db = require('./db'); // No longer needed
const { identifyUser } = require('./middleware/auth');
function createApp() {
function createApp(eventBridge) {
const app = express();
const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000';
@ -20,6 +20,11 @@ function createApp() {
res.json({ message: "pickleglass API is running" });
});
app.use((req, res, next) => {
req.bridge = eventBridge;
next();
});
app.use('/api', identifyUser);
app.use('/api/auth', require('./routes/auth'));

View File

@ -0,0 +1,19 @@
const crypto = require('crypto');
function ipcRequest(req, channel, payload) {
return new Promise((resolve, reject) => {
const responseChannel = `${channel}-${crypto.randomUUID()}`;
req.bridge.once(responseChannel, (response) => {
if (response.success) {
resolve(response.data);
} else {
reject(new Error(response.error || `IPC request to ${channel} failed`));
}
});
req.bridge.emit('web-data-request', channel, responseChannel, payload);
});
}
module.exports = { ipcRequest };

View File

@ -1,13 +0,0 @@
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET_KEY || 'change-me';
const EXPIRE = 60 * 24; // minutes
exports.sign = (sub, extra = {}) => jwt.sign({ sub, ...extra }, SECRET, { algorithm: 'HS256', expiresIn: `${EXPIRE}m` });
exports.verify = token => {
try {
return jwt.verify(token, SECRET).sub;
} catch {
return null;
}
};

View File

@ -1,5 +1,3 @@
const { verify } = require('../jwt');
function identifyUser(req, res, next) {
const userId = req.get('X-User-ID');

View File

@ -1,9 +1,10 @@
const express = require('express');
const db = require('../db');
const router = express.Router();
const { ipcRequest } = require('../ipcBridge');
router.get('/status', (req, res) => {
const user = db.prepare('SELECT uid, display_name FROM users WHERE uid = ?').get('default_user');
router.get('/status', async (req, res) => {
try {
const user = await ipcRequest(req, 'get-user-profile');
if (!user) {
return res.status(500).json({ error: 'Default user not initialized' });
}
@ -14,6 +15,10 @@ router.get('/status', (req, res) => {
name: user.display_name
}
});
} catch (error) {
console.error('Failed to get auth status via IPC:', error);
res.status(500).json({ error: 'Failed to retrieve auth status' });
}
});
module.exports = router;

View File

@ -1,121 +1,54 @@
const express = require('express');
const db = require('../db');
const router = express.Router();
const crypto = require('crypto');
const validator = require('validator');
const { ipcRequest } = require('../ipcBridge');
router.get('/', (req, res) => {
router.get('/', async (req, res) => {
try {
const sessions = db.prepare(
"SELECT id, uid, title, session_type, started_at, ended_at, sync_state, updated_at FROM sessions WHERE uid = ? ORDER BY started_at DESC"
).all(req.uid);
const sessions = await ipcRequest(req, 'get-sessions');
res.json(sessions);
} catch (error) {
console.error('Failed to get sessions:', error);
console.error('Failed to get sessions via IPC:', error);
res.status(500).json({ error: 'Failed to retrieve sessions' });
}
});
router.post('/', (req, res) => {
const { title } = req.body;
const sessionId = crypto.randomUUID();
const now = Math.floor(Date.now() / 1000);
router.post('/', async (req, res) => {
try {
db.prepare(
`INSERT INTO sessions (id, uid, title, started_at, updated_at)
VALUES (?, ?, ?, ?, ?)`
).run(sessionId, req.uid, title || 'New Conversation', now, now);
res.status(201).json({ id: sessionId, message: 'Session created successfully' });
const result = await ipcRequest(req, 'create-session', req.body);
res.status(201).json({ ...result, message: 'Session created successfully' });
} catch (error) {
console.error('Failed to create session:', error);
console.error('Failed to create session via IPC:', error);
res.status(500).json({ error: 'Failed to create session' });
}
});
router.get('/:session_id', (req, res) => {
const { session_id } = req.params;
router.get('/:session_id', async (req, res) => {
try {
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(session_id);
if (!session) {
const details = await ipcRequest(req, 'get-session-details', req.params.session_id);
if (!details) {
return res.status(404).json({ error: 'Session not found' });
}
const transcripts = db.prepare("SELECT * FROM transcripts WHERE session_id = ? ORDER BY start_at ASC").all(session_id);
const ai_messages = db.prepare("SELECT * FROM ai_messages WHERE session_id = ? ORDER BY sent_at ASC").all(session_id);
const summary = db.prepare("SELECT * FROM summaries WHERE session_id = ?").get(session_id);
res.json({
session,
transcripts,
ai_messages,
summary: summary || null
});
res.json(details);
} catch (error) {
console.error(`Failed to get session ${session_id}:`, error);
console.error(`Failed to get session details via IPC for ${req.params.session_id}:`, error);
res.status(500).json({ error: 'Failed to retrieve session details' });
}
});
router.delete('/:session_id', (req, res) => {
const { session_id } = req.params;
const session = db.prepare("SELECT id FROM sessions WHERE id = ?").get(session_id);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
router.delete('/:session_id', async (req, res) => {
try {
db.transaction(() => {
db.prepare("DELETE FROM transcripts WHERE session_id = ?").run(session_id);
db.prepare("DELETE FROM ai_messages WHERE session_id = ?").run(session_id);
db.prepare("DELETE FROM summaries WHERE session_id = ?").run(session_id);
db.prepare("DELETE FROM sessions WHERE id = ?").run(session_id);
})();
await ipcRequest(req, 'delete-session', req.params.session_id);
res.status(200).json({ message: 'Session deleted successfully' });
} catch (error) {
console.error(`Failed to delete session ${session_id}:`, error);
console.error(`Failed to delete session via IPC for ${req.params.session_id}:`, error);
res.status(500).json({ error: 'Failed to delete session' });
}
});
// The search functionality will be more complex to move to IPC.
// For now, we can disable it or leave it as is, knowing it's a future task.
router.get('/search', (req, res) => {
const { q } = req.query;
if (!q || !validator.isLength(q, { min: 3 })) {
return res.status(400).json({ error: 'Query parameter "q" is required' });
}
// Sanitize and validate input
const sanitizedQuery = validator.escape(q.trim()); // Escapes HTML and special chars
if (sanitizedQuery.length === 0 || sanitizedQuery.length > 255) {
return res.status(400).json({ error: 'Query parameter "q" must be between 3 and 255 characters' });
}
try {
const searchQuery = `%${sanitizedQuery}%`;
const sessionIds = db.prepare(`
SELECT DISTINCT session_id FROM (
SELECT session_id FROM transcripts WHERE text LIKE ?
UNION
SELECT session_id FROM ai_messages WHERE content LIKE ?
UNION
SELECT session_id FROM summaries WHERE text LIKE ? OR tldr LIKE ?
)
`).all(searchQuery, searchQuery, searchQuery, searchQuery).map(row => row.session_id);
if (sessionIds.length === 0) {
return res.json([]);
}
const placeholders = sessionIds.map(() => '?').join(',');
const sessions = db.prepare(
`SELECT id, uid, title, started_at, ended_at, sync_state, updated_at FROM sessions WHERE id IN (${placeholders}) ORDER BY started_at DESC`
).all(sessionIds);
res.json(sessions);
} catch (error) {
console.error('Search failed:', error);
res.status(500).json({ error: 'Failed to perform search' });
}
res.status(501).json({ error: 'Search not implemented for IPC bridge yet.' });
});
module.exports = router;

View File

@ -1,85 +1,43 @@
const express = require('express');
const crypto = require('crypto');
const db = require('../db');
const router = express.Router();
const { ipcRequest } = require('../ipcBridge');
router.get('/', (req, res) => {
router.get('/', async (req, res) => {
try {
const presets = db.prepare(
`SELECT * FROM prompt_presets
WHERE uid = ? OR is_default = 1
ORDER BY is_default DESC, title ASC`
).all(req.uid);
const presets = await ipcRequest(req, 'get-presets');
res.json(presets);
} catch (error) {
console.error('Failed to get presets:', error);
console.error('Failed to get presets via IPC:', error);
res.status(500).json({ error: 'Failed to retrieve presets' });
}
});
router.post('/', (req, res) => {
const { title, prompt } = req.body;
if (!title || !prompt) {
return res.status(400).json({ error: 'Title and prompt are required' });
}
const presetId = crypto.randomUUID();
const now = Math.floor(Date.now() / 1000);
router.post('/', async (req, res) => {
try {
db.prepare(
`INSERT INTO prompt_presets (id, uid, title, prompt, is_default, created_at, sync_state)
VALUES (?, ?, ?, ?, 0, ?, 'dirty')`
).run(presetId, req.uid, title, prompt, now);
res.status(201).json({ id: presetId, message: 'Preset created successfully' });
const result = await ipcRequest(req, 'create-preset', req.body);
res.status(201).json({ ...result, message: 'Preset created successfully' });
} catch (error) {
console.error('Failed to create preset:', error);
console.error('Failed to create preset via IPC:', error);
res.status(500).json({ error: 'Failed to create preset' });
}
});
router.put('/:id', (req, res) => {
const { id } = req.params;
const { title, prompt } = req.body;
if (!title || !prompt) {
return res.status(400).json({ error: 'Title and prompt are required' });
}
router.put('/:id', async (req, res) => {
try {
const result = db.prepare(
`UPDATE prompt_presets
SET title = ?, prompt = ?, sync_state = 'dirty'
WHERE id = ? AND uid = ? AND is_default = 0`
).run(title, prompt, id, req.uid);
if (result.changes === 0) {
return res.status(404).json({ error: "Preset not found or you don't have permission to edit it." });
}
await ipcRequest(req, 'update-preset', { id: req.params.id, data: req.body });
res.json({ message: 'Preset updated successfully' });
} catch (error) {
console.error('Failed to update preset:', error);
console.error('Failed to update preset via IPC:', error);
res.status(500).json({ error: 'Failed to update preset' });
}
});
router.delete('/:id', (req, res) => {
const { id } = req.params;
router.delete('/:id', async (req, res) => {
try {
const result = db.prepare(
`DELETE FROM prompt_presets
WHERE id = ? AND uid = ? AND is_default = 0`
).run(id, req.uid);
if (result.changes === 0) {
return res.status(404).json({ error: "Preset not found or you don't have permission to delete it." });
}
await ipcRequest(req, 'delete-preset', req.params.id);
res.json({ message: 'Preset deleted successfully' });
} catch (error) {
console.error('Failed to delete preset:', error);
console.error('Failed to delete preset via IPC:', error);
res.status(500).json({ error: 'Failed to delete preset' });
}
});

View File

@ -1,144 +1,76 @@
const express = require('express');
const db = require('../db');
const router = express.Router();
const { ipcRequest } = require('../ipcBridge');
router.put('/profile', (req, res) => {
const { displayName } = req.body;
if (!displayName) return res.status(400).json({ error: 'displayName is required' });
router.put('/profile', async (req, res) => {
try {
db.prepare("UPDATE users SET display_name = ? WHERE uid = ?").run(displayName, req.uid);
await ipcRequest(req, 'update-user-profile', req.body);
res.json({ message: 'Profile updated successfully' });
} catch (error) {
console.error('Failed to update profile:', error);
console.error('Failed to update profile via IPC:', error);
res.status(500).json({ error: 'Failed to update profile' });
}
});
router.get('/profile', (req, res) => {
router.get('/profile', async (req, res) => {
try {
const user = db.prepare('SELECT uid, display_name, email FROM users WHERE uid = ?').get(req.uid);
const user = await ipcRequest(req, 'get-user-profile');
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
} catch (error) {
console.error('Failed to get profile:', error);
console.error('Failed to get profile via IPC:', error);
res.status(500).json({ error: 'Failed to get profile' });
}
});
router.post('/find-or-create', (req, res) => {
const { uid, displayName, email } = req.body;
if (!uid || !displayName || !email) {
return res.status(400).json({ error: 'uid, displayName, and email are required' });
}
router.post('/find-or-create', async (req, res) => {
try {
const now = Math.floor(Date.now() / 1000);
db.prepare(
`INSERT INTO users (uid, display_name, email, created_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(uid) DO NOTHING`
).run(uid, displayName, email, now);
const user = db.prepare('SELECT * FROM users WHERE uid = ?').get(uid);
const user = await ipcRequest(req, 'find-or-create-user', req.body);
res.status(200).json(user);
} catch (error) {
console.error('Failed to find or create user:', error);
console.error('Failed to find or create user via IPC:', error);
res.status(500).json({ error: 'Failed to find or create user' });
}
});
router.post('/api-key', (req, res) => {
const { apiKey } = req.body;
if (typeof apiKey !== 'string') {
return res.status(400).json({ error: 'API key must be a string' });
}
router.post('/api-key', async (req, res) => {
try {
db.prepare("UPDATE users SET api_key = ? WHERE uid = ?").run(apiKey, req.uid);
await ipcRequest(req, 'save-api-key', req.body.apiKey);
res.json({ message: 'API key saved successfully' });
} catch (error) {
console.error('Failed to save API key:', error);
console.error('Failed to save API key via IPC:', error);
res.status(500).json({ error: 'Failed to save API key' });
}
});
router.get('/api-key-status', (req, res) => {
router.get('/api-key-status', async (req, res) => {
try {
const row = db.prepare('SELECT api_key FROM users WHERE uid = ?').get(req.uid);
if (!row) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ hasApiKey: !!row.api_key && row.api_key.length > 0 });
const status = await ipcRequest(req, 'check-api-key-status');
res.json(status);
} catch (error) {
console.error('Failed to get API key status:', error);
console.error('Failed to get API key status via IPC:', error);
res.status(500).json({ error: 'Failed to get API key status' });
}
});
router.delete('/profile', (req, res) => {
router.delete('/profile', async (req, res) => {
try {
const user = db.prepare('SELECT uid FROM users WHERE uid = ?').get(req.uid);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const userSessions = db.prepare('SELECT id FROM sessions WHERE uid = ?').all(user.uid);
const sessionIds = userSessions.map(s => s.id);
db.transaction(() => {
if (sessionIds.length > 0) {
const placeholders = sessionIds.map(() => '?').join(',');
db.prepare(`DELETE FROM transcripts WHERE session_id IN (${placeholders})`).run(...sessionIds);
db.prepare(`DELETE FROM ai_messages WHERE session_id IN (${placeholders})`).run(...sessionIds);
db.prepare(`DELETE FROM summaries WHERE session_id IN (${placeholders})`).run(...sessionIds);
db.prepare(`DELETE FROM sessions WHERE uid = ?`).run(user.uid);
}
db.prepare('DELETE FROM prompt_presets WHERE uid = ?').run(user.uid);
db.prepare('DELETE FROM users WHERE uid = ?').run(user.uid);
})();
await ipcRequest(req, 'delete-account');
res.status(200).json({ message: 'User account and all data deleted successfully.' });
} catch (error) {
console.error('Failed to delete user account:', error);
console.error('Failed to delete user account via IPC:', error);
res.status(500).json({ error: 'Failed to delete user account' });
}
});
async function getUserBatchData(req, res) {
const { include = 'profile,presets,sessions' } = req.query;
router.get('/batch', async (req, res) => {
try {
const includes = include.split(',').map(item => item.trim());
const result = {};
if (includes.includes('profile')) {
const user = db.prepare('SELECT uid, display_name, email FROM users WHERE uid = ?').get(req.uid);
result.profile = user || null;
}
if (includes.includes('presets')) {
const presets = db.prepare('SELECT * FROM prompt_presets WHERE uid = ? OR is_default = 1').all(req.uid);
result.presets = presets || [];
}
if (includes.includes('sessions')) {
const recent_sessions = db.prepare(
"SELECT id, title, started_at, updated_at FROM sessions WHERE uid = ? ORDER BY updated_at DESC LIMIT 10"
).all(req.uid);
result.sessions = recent_sessions || [];
}
const result = await ipcRequest(req, 'get-batch-data', req.query.include);
res.json(result);
} catch (error) {
console.error('Failed to get batch data:', error);
} catch(error) {
console.error('Failed to get batch data via IPC:', error);
res.status(500).json({ error: 'Failed to get batch data' });
}
}
router.get('/batch', getUserBatchData);
});
module.exports = router;

View File

@ -87,7 +87,11 @@ export interface SessionDetails {
const isFirebaseMode = (): boolean => {
return firebaseAuth.currentUser !== null;
// The web frontend can no longer directly access Firebase state,
// so we assume communication always goes through the backend API.
// In the future, we can create an endpoint like /api/auth/status
// in the backend to retrieve the authentication state.
return false;
};
const timestampToUnix = (timestamp: Timestamp): number => {
@ -185,41 +189,13 @@ const loadRuntimeConfig = async (): Promise<string | null> => {
return null;
};
const getApiUrlFromElectron = (): string | null => {
if (typeof window !== 'undefined') {
try {
const { ipcRenderer } = window.require?.('electron') || {};
if (ipcRenderer) {
try {
const apiUrl = ipcRenderer.sendSync('get-api-url-sync');
if (apiUrl) {
console.log('✅ API URL from Electron IPC:', apiUrl);
return apiUrl;
}
} catch (error) {
console.log('⚠️ Electron IPC failed:', error);
}
}
} catch (error) {
console.log(' Not in Electron environment');
}
}
return null;
};
let apiUrlInitialized = false;
let initializationPromise: Promise<void> | null = null;
const initializeApiUrl = async () => {
if (apiUrlInitialized) return;
const electronUrl = getApiUrlFromElectron();
if (electronUrl) {
API_ORIGIN = electronUrl;
apiUrlInitialized = true;
return;
}
// Electron IPC 관련 코드를 모두 제거하고 runtime-config.json 또는 fallback에만 의존합니다.
const runtimeUrl = await loadRuntimeConfig();
if (runtimeUrl) {
API_ORIGIN = runtimeUrl;

View File

@ -36,21 +36,12 @@ export const useAuth = () => {
setUser(profile);
setUserInfo(profile);
if (window.ipcRenderer) {
window.ipcRenderer.send('set-current-user', profile.uid);
}
} else {
console.log('🏠 Local mode activated');
setMode('local');
setUser(defaultLocalUser);
setUserInfo(defaultLocalUser);
if (window.ipcRenderer) {
window.ipcRenderer.send('set-current-user', defaultLocalUser.uid);
}
}
setIsLoading(false);
});

0
preload.js Normal file
View File

View File

@ -304,19 +304,6 @@ export class AppHeader extends LitElement {
this.settingsHideTimer = null;
this.isSessionActive = false;
this.animationEndTimer = null;
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.on('toggle-header-visibility', () => {
this.toggleVisibility();
});
ipcRenderer.on('cancel-hide-settings', () => {
this.cancelHideWindow('settings');
});
}
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleAnimationEnd = this.handleAnimationEnd.bind(this);
@ -478,8 +465,6 @@ export class AppHeader extends LitElement {
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.removeAllListeners('toggle-header-visibility');
ipcRenderer.removeAllListeners('cancel-hide-settings');
if (this._sessionStateListener) {
ipcRenderer.removeListener('session-state-changed', this._sessionStateListener);
}

View File

@ -1,26 +1,9 @@
import { initializeApp } from 'firebase/app';
import { getAuth, onAuthStateChanged, GoogleAuthProvider, signInWithCredential, signInWithCustomToken, signOut } from 'firebase/auth';
import './AppHeader.js';
import './ApiKeyHeader.js';
import './PermissionSetup.js';
const firebaseConfig = {
apiKey: 'AIzaSyAgtJrmsFWG1C7m9S55HyT1laICEzuUS2g',
authDomain: 'pickle-3651a.firebaseapp.com',
projectId: 'pickle-3651a',
storageBucket: 'pickle-3651a.firebasestorage.app',
messagingSenderId: '904706892885',
appId: '1:904706892885:web:0e42b3dda796674ead20dc',
measurementId: 'G-SQ0WM6S28T',
};
const firebaseApp = initializeApp(firebaseConfig);
const auth = getAuth(firebaseApp);
class HeaderTransitionManager {
constructor() {
this.headerContainer = document.getElementById('header-container');
this.currentHeaderType = null; // 'apikey' | 'app' | 'permission'
this.apiKeyHeader = null;
@ -60,151 +43,25 @@ class HeaderTransitionManager {
console.log('[HeaderController] Manager initialized');
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer
.invoke('get-current-api-key')
.then(storedKey => {
this.hasApiKey = !!storedKey;
})
.catch(() => {});
}
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.on('login-successful', async (event, payload) => {
const { customToken, token, error } = payload || {};
try {
if (customToken) {
console.log('[HeaderController] Received custom token, signing in with custom token...');
await signInWithCustomToken(auth, customToken);
return;
}
if (token) {
console.log('[HeaderController] Received ID token, attempting Google credential sign-in...');
const credential = GoogleAuthProvider.credential(token);
await signInWithCredential(auth, credential);
return;
}
if (error) {
console.warn('[HeaderController] Login payload indicates verification failure. Showing permission setup.');
// Show permission setup after login error
this.transitionToPermissionSetup();
}
} catch (error) {
console.error('[HeaderController] Sign-in failed', error);
// Show permission setup after sign-in failure
this.transitionToPermissionSetup();
}
});
ipcRenderer.on('request-firebase-logout', async () => {
console.log('[HeaderController] Received request to sign out.');
try {
this.hasApiKey = false;
await signOut(auth);
} catch (error) {
console.error('[HeaderController] Sign out failed', error);
}
});
ipcRenderer.on('api-key-validated', () => {
this.hasApiKey = true;
// Wait for animation to complete before transitioning
setTimeout(() => {
this.transitionToPermissionSetup();
}, 350); // Give time for slide-out animation to complete
});
ipcRenderer.on('api-key-removed', () => {
this.hasApiKey = false;
this.transitionToApiKeyHeader();
});
ipcRenderer.on('api-key-updated', () => {
this.hasApiKey = true;
if (!auth.currentUser) {
this.transitionToPermissionSetup();
}
});
ipcRenderer.on('firebase-auth-success', async (event, firebaseUser) => {
console.log('[HeaderController] Received firebase-auth-success:', firebaseUser.uid);
try {
if (firebaseUser.idToken) {
const credential = GoogleAuthProvider.credential(firebaseUser.idToken);
await signInWithCredential(auth, credential);
console.log('[HeaderController] Firebase sign-in successful via ID token');
} else {
console.warn('[HeaderController] No ID token received from deeplink, showing permission setup');
// Show permission setup after Firebase auth
this.transitionToPermissionSetup();
}
} catch (error) {
console.error('[HeaderController] Firebase auth failed:', error);
this.transitionToPermissionSetup();
}
});
}
this._bootstrap();
onAuthStateChanged(auth, async user => {
console.log('[HeaderController] Auth state changed. User:', user ? user.email : 'null');
if (window.require) {
const { ipcRenderer } = window.require('electron');
let userDataWithToken = null;
if (user) {
try {
const idToken = await user.getIdToken();
userDataWithToken = {
uid: user.uid,
email: user.email,
name: user.displayName,
photoURL: user.photoURL,
idToken: idToken,
};
} catch (error) {
console.error('[HeaderController] Failed to get ID token:', error);
userDataWithToken = {
uid: user.uid,
email: user.email,
name: user.displayName,
photoURL: user.photoURL,
idToken: null,
};
}
}
ipcRenderer.on('user-state-changed', (event, userState) => {
console.log('[HeaderController] Received user state change:', userState);
this.handleStateUpdate(userState);
});
ipcRenderer.invoke('firebase-auth-state-changed', userDataWithToken).catch(console.error);
}
if (!this.isInitialized) {
this.isInitialized = true;
return; // Skip on initial load - bootstrap handles it
}
// Only handle state changes after initial load
if (user) {
console.log('[HeaderController] User logged in, updating hasApiKey and checking permissions...');
this.hasApiKey = true; // User login should provide API key
// Delay permission check to ensure smooth login flow
setTimeout(() => this.transitionToPermissionSetup(), 500);
} else if (this.hasApiKey) {
console.log('[HeaderController] No Firebase user but API key exists, checking if permission setup is needed...');
setTimeout(() => this.transitionToPermissionSetup(), 500);
} else {
console.log('[HeaderController] No auth & no API key — showing ApiKeyHeader');
this.transitionToApiKeyHeader();
ipcRenderer.on('auth-failed', (event, { message }) => {
console.error('[HeaderController] Received auth failure from main process:', message);
if (this.apiKeyHeader) {
this.apiKeyHeader.errorMessage = 'Authentication failed. Please try again.';
this.apiKeyHeader.isLoading = false;
}
});
}
}
notifyHeaderState(stateOverride) {
const state = stateOverride || this.currentHeaderType || 'apikey';
@ -214,40 +71,36 @@ class HeaderTransitionManager {
}
async _bootstrap() {
let storedKey = null;
// The initial state will be sent by the main process via 'user-state-changed'
// We just need to request it.
if (window.require) {
try {
storedKey = await window
.require('electron')
.ipcRenderer.invoke('get-current-api-key');
} catch (_) {}
}
this.hasApiKey = !!storedKey;
const user = await new Promise(resolve => {
const unsubscribe = onAuthStateChanged(auth, u => {
unsubscribe();
resolve(u);
});
});
// check flow order: API key -> Permissions -> App
if (!user && !this.hasApiKey) {
// No auth and no API key -> show API key input
await this._resizeForApiKey();
this.ensureHeader('apikey');
const userState = await window.require('electron').ipcRenderer.invoke('get-current-user');
console.log('[HeaderController] Bootstrapping with initial user state:', userState);
this.handleStateUpdate(userState);
} else {
// Has API key or user -> check permissions first
// Fallback for non-electron environment (testing/web)
this.ensureHeader('apikey');
}
}
async handleStateUpdate(userState) {
const { isLoggedIn, hasApiKey } = userState;
if (isLoggedIn) {
// Firebase user: Check permissions, then show App or Permission Setup
const permissionResult = await this.checkPermissions();
if (permissionResult.success) {
// All permissions granted -> go to app
await this._resizeForApp();
this.ensureHeader('app');
this.transitionToAppHeader();
} else {
// Permissions needed -> show permission setup
await this._resizeForPermissionSetup();
this.ensureHeader('permission');
this.transitionToPermissionSetup();
}
} else if (hasApiKey) {
// API Key only user: Skip permission check, go directly to App
this.transitionToAppHeader();
} else {
// No auth at all
await this._resizeForApiKey();
this.ensureHeader('apikey');
}
}
@ -290,25 +143,8 @@ class HeaderTransitionManager {
return this._resizeForApp();
}
const canAnimate =
animate &&
(this.apiKeyHeader || this.permissionSetup) &&
this.currentHeaderType !== 'app';
if (canAnimate && this.apiKeyHeader?.startSlideOutAnimation) {
const old = this.apiKeyHeader;
const onEnd = () => {
clearTimeout(fallback);
this._resizeForApp().then(() => this.ensureHeader('app'));
};
old.addEventListener('animationend', onEnd, { once: true });
old.startSlideOutAnimation();
const fallback = setTimeout(onEnd, 450);
} else {
await this._resizeForApp();
this.ensureHeader('app');
this._resizeForApp();
}
}
_resizeForApp() {
@ -335,16 +171,6 @@ class HeaderTransitionManager {
.catch(() => {});
}
async transitionToApiKeyHeader() {
await this._resizeForApiKey();
if (this.currentHeaderType !== 'apikey') {
this.ensureHeader('apikey');
}
if (this.apiKeyHeader) this.apiKeyHeader.reset();
}
async checkPermissions() {
if (!window.require) {
return { success: true };
@ -353,7 +179,6 @@ class HeaderTransitionManager {
const { ipcRenderer } = window.require('electron');
try {
// Check permission status
const permissions = await ipcRenderer.invoke('check-system-permissions');
console.log('[HeaderController] Current permissions:', permissions);
@ -361,7 +186,6 @@ class HeaderTransitionManager {
return { success: true };
}
// If permissions are not set up, return false
let errorMessage = '';
if (!permissions.microphone && !permissions.screen) {
errorMessage = 'Microphone and screen recording access required';

View File

@ -83,10 +83,6 @@ export class PickleGlassApp extends LitElement {
ipcRenderer.on('click-through-toggled', (_, isEnabled) => {
this._isClickThrough = isEnabled;
});
ipcRenderer.on('show-view', (_, view) => {
this.currentView = view;
this.isMainViewVisible = true;
});
ipcRenderer.on('start-listening-session', () => {
console.log('Received start-listening-session command, calling handleListenClick.');
this.handleListenClick();
@ -100,7 +96,6 @@ export class PickleGlassApp extends LitElement {
const { ipcRenderer } = window.require('electron');
ipcRenderer.removeAllListeners('update-status');
ipcRenderer.removeAllListeners('click-through-toggled');
ipcRenderer.removeAllListeners('show-view');
ipcRenderer.removeAllListeners('start-listening-session');
}
}
@ -110,10 +105,7 @@ export class PickleGlassApp extends LitElement {
this.requestWindowResize();
}
if (changedProperties.has('currentView') && window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('view-changed', this.currentView);
if (changedProperties.has('currentView')) {
const viewContainer = this.shadowRoot?.querySelector('.view-container');
if (viewContainer) {
viewContainer.classList.add('entering');

View File

@ -253,17 +253,6 @@
}, 250);
});
ipcRenderer.on('settings-window-show-animation', () => {
console.log('Starting settings window show animation');
app.classList.remove('window-hidden', 'window-sliding-up', 'settings-window-hide');
app.classList.add('settings-window-show');
if (animationTimeout) clearTimeout(animationTimeout);
animationTimeout = setTimeout(() => {
app.classList.remove('settings-window-show');
}, 220);
});
ipcRenderer.on('window-hide-animation', () => {
console.log('Starting window hide animation');
app.classList.remove('window-sliding-down', 'settings-window-show');

View File

@ -0,0 +1,19 @@
const sqliteRepository = require('./sqlite.repository');
// const firebaseRepository = require('./firebase.repository');
const authService = require('../../../common/services/authService');
function getRepository() {
// const user = authService.getCurrentUser();
// if (user.isLoggedIn) {
// return firebaseRepository;
// }
return sqliteRepository;
}
module.exports = {
getPresets: (...args) => getRepository().getPresets(...args),
getPresetTemplates: (...args) => getRepository().getPresetTemplates(...args),
create: (...args) => getRepository().create(...args),
update: (...args) => getRepository().update(...args),
delete: (...args) => getRepository().delete(...args),
};

View File

@ -0,0 +1,87 @@
const sqliteClient = require('../../services/sqliteClient');
function getPresets(uid) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = `
SELECT * FROM prompt_presets
WHERE uid = ? OR is_default = 1
ORDER BY is_default DESC, title ASC
`;
db.all(query, [uid], (err, rows) => {
if (err) {
console.error('SQLite: Failed to get presets:', err);
reject(err);
} else {
resolve(rows);
}
});
});
}
function getPresetTemplates() {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = `
SELECT * FROM prompt_presets
WHERE is_default = 1
ORDER BY title ASC
`;
db.all(query, [], (err, rows) => {
if (err) {
console.error('SQLite: Failed to get preset templates:', err);
reject(err);
} else {
resolve(rows);
}
});
});
}
function create({ uid, title, prompt }) {
const db = sqliteClient.getDb();
const presetId = require('crypto').randomUUID();
const now = Math.floor(Date.now() / 1000);
const query = `INSERT INTO prompt_presets (id, uid, title, prompt, is_default, created_at, sync_state) VALUES (?, ?, ?, ?, 0, ?, 'dirty')`;
return new Promise((resolve, reject) => {
db.run(query, [presetId, uid, title, prompt, now], function(err) {
if (err) reject(err);
else resolve({ id: presetId });
});
});
}
function update(id, { title, prompt }, uid) {
const db = sqliteClient.getDb();
const query = `UPDATE prompt_presets SET title = ?, prompt = ?, sync_state = 'dirty' WHERE id = ? AND uid = ? AND is_default = 0`;
return new Promise((resolve, reject) => {
db.run(query, [title, prompt, id, uid], function(err) {
if (err) reject(err);
else if (this.changes === 0) reject(new Error("Preset not found or permission denied."));
else resolve({ changes: this.changes });
});
});
}
function del(id, uid) {
const db = sqliteClient.getDb();
const query = `DELETE FROM prompt_presets WHERE id = ? AND uid = ? AND is_default = 0`;
return new Promise((resolve, reject) => {
db.run(query, [id, uid], function(err) {
if (err) reject(err);
else if (this.changes === 0) reject(new Error("Preset not found or permission denied."));
else resolve({ changes: this.changes });
});
});
}
module.exports = {
getPresets,
getPresetTemplates,
create,
update,
delete: del
};

View File

@ -0,0 +1,26 @@
const sqliteRepository = require('./sqlite.repository');
// const firebaseRepository = require('./firebase.repository'); // Future implementation
const authService = require('../../../common/services/authService');
function getRepository() {
// In the future, we can check the user's login status from authService
// const user = authService.getCurrentUser();
// if (user.isLoggedIn) {
// return firebaseRepository;
// }
return sqliteRepository;
}
// Directly export functions for ease of use, decided by the strategy
module.exports = {
getById: (...args) => getRepository().getById(...args),
create: (...args) => getRepository().create(...args),
getAllByUserId: (...args) => getRepository().getAllByUserId(...args),
updateTitle: (...args) => getRepository().updateTitle(...args),
deleteWithRelatedData: (...args) => getRepository().deleteWithRelatedData(...args),
end: (...args) => getRepository().end(...args),
updateType: (...args) => getRepository().updateType(...args),
touch: (...args) => getRepository().touch(...args),
getOrCreateActive: (...args) => getRepository().getOrCreateActive(...args),
endAllActiveSessions: (...args) => getRepository().endAllActiveSessions(...args),
};

View File

@ -0,0 +1,189 @@
const sqliteClient = require('../../services/sqliteClient');
function getById(id) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
db.get('SELECT * FROM sessions WHERE id = ?', [id], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
}
function create(uid, type = 'ask') {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const sessionId = require('crypto').randomUUID();
const now = Math.floor(Date.now() / 1000);
const query = `INSERT INTO sessions (id, uid, title, session_type, started_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`;
db.run(query, [sessionId, uid, `Session @ ${new Date().toLocaleTimeString()}`, type, now, now], function(err) {
if (err) {
console.error('SQLite: Failed to create session:', err);
reject(err);
} else {
console.log(`SQLite: Created session ${sessionId} for user ${uid} (type: ${type})`);
resolve(sessionId);
}
});
});
}
function getAllByUserId(uid) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = "SELECT id, uid, title, session_type, started_at, ended_at, sync_state, updated_at FROM sessions WHERE uid = ? ORDER BY started_at DESC";
db.all(query, [uid], (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
});
}
function updateTitle(id, title) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
db.run('UPDATE sessions SET title = ? WHERE id = ?', [title, id], function(err) {
if (err) reject(err);
else resolve({ changes: this.changes });
});
});
}
function deleteWithRelatedData(id) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
db.serialize(() => {
db.run("BEGIN TRANSACTION;");
const queries = [
"DELETE FROM transcripts WHERE session_id = ?",
"DELETE FROM ai_messages WHERE session_id = ?",
"DELETE FROM summaries WHERE session_id = ?",
"DELETE FROM sessions WHERE id = ?"
];
queries.forEach(query => {
db.run(query, [id], (err) => {
if (err) {
db.run("ROLLBACK;");
return reject(err);
}
});
});
db.run("COMMIT;", (err) => {
if (err) {
db.run("ROLLBACK;");
return reject(err);
}
resolve({ success: true });
});
});
});
}
function end(id) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const query = `UPDATE sessions SET ended_at = ?, updated_at = ? WHERE id = ?`;
db.run(query, [now, now, id], function(err) {
if (err) reject(err);
else resolve({ changes: this.changes });
});
});
}
function updateType(id, type) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const query = 'UPDATE sessions SET session_type = ?, updated_at = ? WHERE id = ?';
db.run(query, [type, now, id], function(err) {
if (err) {
reject(err);
} else {
resolve({ changes: this.changes });
}
});
});
}
function touch(id) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const query = 'UPDATE sessions SET updated_at = ? WHERE id = ?';
db.run(query, [now, id], function(err) {
if (err) reject(err);
else resolve({ changes: this.changes });
});
});
}
async function getOrCreateActive(uid, requestedType = 'ask') {
const db = sqliteClient.getDb();
// 1. Look for ANY active session for the user (ended_at IS NULL).
// Prefer 'listen' sessions over 'ask' sessions to ensure continuity.
const findQuery = `
SELECT id, session_type FROM sessions
WHERE uid = ? AND ended_at IS NULL
ORDER BY CASE session_type WHEN 'listen' THEN 1 WHEN 'ask' THEN 2 ELSE 3 END
LIMIT 1
`;
const activeSession = await new Promise((resolve, reject) => {
db.get(findQuery, [uid], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
if (activeSession) {
// An active session exists.
console.log(`[Repo] Found active session ${activeSession.id} of type ${activeSession.session_type}`);
// 2. Promotion Logic: If it's an 'ask' session and we need 'listen', promote it.
if (activeSession.session_type === 'ask' && requestedType === 'listen') {
await updateType(activeSession.id, 'listen');
console.log(`[Repo] Promoted session ${activeSession.id} to 'listen' type.`);
}
// 3. Touch the session and return its ID.
await touch(activeSession.id);
return activeSession.id;
} else {
// 4. No active session found, create a new one.
console.log(`[Repo] No active session for user ${uid}. Creating new '${requestedType}' session.`);
return create(uid, requestedType);
}
}
function endAllActiveSessions() {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const query = `UPDATE sessions SET ended_at = ?, updated_at = ? WHERE ended_at IS NULL`;
db.run(query, [now, now], function(err) {
if (err) {
console.error('SQLite: Failed to end all active sessions:', err);
reject(err);
} else {
console.log(`[Repo] Ended ${this.changes} active session(s).`);
resolve({ changes: this.changes });
}
});
});
}
module.exports = {
getById,
create,
getAllByUserId,
updateTitle,
deleteWithRelatedData,
end,
updateType,
touch,
getOrCreateActive,
endAllActiveSessions,
};

View File

@ -0,0 +1,11 @@
const sqliteRepository = require('./sqlite.repository');
// This repository is not user-specific, so we always return sqlite.
function getRepository() {
return sqliteRepository;
}
module.exports = {
markPermissionsAsCompleted: (...args) => getRepository().markPermissionsAsCompleted(...args),
checkPermissionsCompleted: (...args) => getRepository().checkPermissionsCompleted(...args),
};

View File

@ -0,0 +1,14 @@
const sqliteClient = require('../../services/sqliteClient');
async function markPermissionsAsCompleted() {
return sqliteClient.markPermissionsAsCompleted();
}
async function checkPermissionsCompleted() {
return sqliteClient.checkPermissionsCompleted();
}
module.exports = {
markPermissionsAsCompleted,
checkPermissionsCompleted,
};

View File

@ -0,0 +1,19 @@
const sqliteRepository = require('./sqlite.repository');
// const firebaseRepository = require('./firebase.repository');
const authService = require('../../../common/services/authService');
function getRepository() {
// const user = authService.getCurrentUser();
// if (user.isLoggedIn) {
// return firebaseRepository;
// }
return sqliteRepository;
}
module.exports = {
findOrCreate: (...args) => getRepository().findOrCreate(...args),
getById: (...args) => getRepository().getById(...args),
saveApiKey: (...args) => getRepository().saveApiKey(...args),
update: (...args) => getRepository().update(...args),
deleteById: (...args) => getRepository().deleteById(...args),
};

View File

@ -0,0 +1,107 @@
const sqliteClient = require('../../services/sqliteClient');
function findOrCreate(user) {
const db = sqliteClient.getDb();
const { uid, displayName, email } = user;
const now = Math.floor(Date.now() / 1000);
const query = `
INSERT INTO users (uid, display_name, email, created_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(uid) DO UPDATE SET
display_name=excluded.display_name,
email=excluded.email
`;
return new Promise((resolve, reject) => {
db.run(query, [uid, displayName, email, now], (err) => {
if (err) {
console.error('SQLite: Failed to find or create user:', err);
return reject(err);
}
getById(uid).then(resolve).catch(reject);
});
});
}
function getById(uid) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
db.get('SELECT * FROM users WHERE uid = ?', [uid], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
}
function saveApiKey(apiKey, uid, provider = 'openai') {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
db.run(
'UPDATE users SET api_key = ?, provider = ? WHERE uid = ?',
[apiKey, provider, uid],
function(err) {
if (err) {
console.error('SQLite: Failed to save API key:', err);
reject(err);
} else {
console.log(`SQLite: API key saved for user ${uid} with provider ${provider}.`);
resolve({ changes: this.changes });
}
}
);
});
}
function update({ uid, displayName }) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
db.run('UPDATE users SET display_name = ? WHERE uid = ?', [displayName, uid], function(err) {
if (err) reject(err);
else resolve({ changes: this.changes });
});
});
}
function deleteById(uid) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const userSessions = db.prepare('SELECT id FROM sessions WHERE uid = ?').all(uid);
const sessionIds = userSessions.map(s => s.id);
db.serialize(() => {
db.run("BEGIN TRANSACTION;");
try {
if (sessionIds.length > 0) {
const placeholders = sessionIds.map(() => '?').join(',');
db.prepare(`DELETE FROM transcripts WHERE session_id IN (${placeholders})`).run(...sessionIds);
db.prepare(`DELETE FROM ai_messages WHERE session_id IN (${placeholders})`).run(...sessionIds);
db.prepare(`DELETE FROM summaries WHERE session_id IN (${placeholders})`).run(...sessionIds);
db.prepare(`DELETE FROM sessions WHERE uid = ?`).run(uid);
}
db.prepare('DELETE FROM prompt_presets WHERE uid = ? AND is_default = 0').run(uid);
db.prepare('DELETE FROM users WHERE uid = ?').run(uid);
db.run("COMMIT;", (err) => {
if (err) {
db.run("ROLLBACK;");
return reject(err);
}
resolve({ success: true });
});
} catch (err) {
db.run("ROLLBACK;");
reject(err);
}
});
});
}
module.exports = {
findOrCreate,
getById,
saveApiKey,
update,
deleteById
};

View File

@ -1,239 +0,0 @@
const axios = require('axios');
const config = require('../config/config');
class APIClient {
constructor() {
this.baseURL = config.get('apiUrl');
this.client = axios.create({
baseURL: this.baseURL,
timeout: config.get('apiTimeout'),
headers: {
'Content-Type': 'application/json'
}
});
this.client.interceptors.response.use(
(response) => response,
(error) => {
console.error('API request failed:', error.message);
if (error.response) {
console.error('response status:', error.response.status);
console.error('response data:', error.response.data);
}
return Promise.reject(error);
}
);
}
async initialize() {
try {
const response = await this.client.get('/api/auth/status');
console.log('[APIClient] checked default user status:', response.data);
return true;
} catch (error) {
console.error('[APIClient] failed to initialize:', error);
return false;
}
}
async checkConnection() {
try {
const response = await this.client.get('/');
return response.status === 200;
} catch (error) {
return false;
}
}
async saveApiKey(apiKey) {
try {
const response = await this.client.post('/api/user/api-key', { apiKey });
return response.data;
} catch (error) {
console.error('failed to save api key:', error);
throw error;
}
}
async checkApiKey() {
try {
const response = await this.client.get('/api/user/api-key');
return response.data;
} catch (error) {
console.error('failed to check api key:', error);
return { hasApiKey: false };
}
}
async getUserBatchData(includes = ['profile', 'context', 'presets']) {
try {
const includeParam = includes.join(',');
const response = await this.client.get(`/api/user/batch?include=${includeParam}`);
return response.data;
} catch (error) {
console.error('failed to get user batch data:', error);
return null;
}
}
async getUserContext() {
try {
const response = await this.client.get('/api/user/context');
return response.data.context;
} catch (error) {
console.error('fail to get user context:', error);
return null;
}
}
async getUserProfile() {
try {
const response = await this.client.get('/api/user/profile');
return response.data;
} catch (error) {
console.error('failed to get user profile:', error);
return null;
}
}
async getUserPresets() {
try {
const response = await this.client.get('/api/user/presets');
return response.data;
} catch (error) {
console.error('failed to get user presets:', error);
return [];
}
}
async updateUserContext(context) {
try {
const response = await this.client.post('/api/user/context', context);
return response.data;
} catch (error) {
console.error('failed to update user context:', error);
throw error;
}
}
async addActivity(activity) {
try {
const response = await this.client.post('/api/user/activities', activity);
return response.data;
} catch (error) {
console.error('failed to add activity:', error);
throw error;
}
}
async getPresetTemplates() {
try {
const response = await this.client.get('/api/preset-templates');
return response.data;
} catch (error) {
console.error('failed to get preset templates:', error);
return [];
}
}
async updateUserProfile(profile) {
try {
const response = await this.client.post('/api/user/profile', profile);
return response.data;
} catch (error) {
console.error('failed to update user profile:', error);
throw error;
}
}
async searchUsers(name = '') {
try {
const response = await this.client.get('/api/users/search', {
params: { name }
});
return response.data;
} catch (error) {
console.error('failed to search users:', error);
return [];
}
}
async getUserProfileById(userId) {
try {
const response = await this.client.get(`/api/users/${userId}/profile`);
return response.data;
} catch (error) {
console.error('failed to get user profile by id:', error);
return null;
}
}
async saveConversationSession(sessionId, conversationHistory) {
try {
const payload = {
sessionId,
conversationHistory
};
const response = await this.client.post('/api/conversations', payload);
return response.data;
} catch (error) {
console.error('failed to save conversation session:', error);
throw error;
}
}
async getConversationSession(sessionId) {
try {
const response = await this.client.get(`/api/conversations/${sessionId}`);
return response.data;
} catch (error) {
console.error('failed to get conversation session:', error);
return null;
}
}
async getAllConversationSessions() {
try {
const response = await this.client.get('/api/conversations');
return response.data;
} catch (error) {
console.error('failed to get all conversation sessions:', error);
return [];
}
}
async deleteConversationSession(sessionId) {
try {
const response = await this.client.delete(`/api/conversations/${sessionId}`);
return response.data;
} catch (error) {
console.error('failed to delete conversation session:', error);
throw error;
}
}
async getSyncStatus() {
try {
const response = await this.client.get('/api/sync/status');
return response.data;
} catch (error) {
console.error('failed to get sync status:', error);
return null;
}
}
async getFullUserData() {
try {
const response = await this.client.get('/api/user/full');
return response.data;
} catch (error) {
console.error('failed to get full user data:', error);
return null;
}
}
}
const apiClient = new APIClient();
module.exports = apiClient;
module.exports = apiClient;

View File

@ -0,0 +1,179 @@
const { onAuthStateChanged, signInWithCustomToken, signOut } = require('firebase/auth');
const { BrowserWindow } = require('electron');
const { getFirebaseAuth } = require('./firebaseClient');
const userRepository = require('../repositories/user');
const fetch = require('node-fetch');
async function getVirtualKeyByEmail(email, idToken) {
if (!idToken) {
throw new Error('Firebase ID token is required for virtual key request');
}
const resp = await fetch('https://serverless-api-sf3o.vercel.app/api/virtual_key', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${idToken}`,
},
body: JSON.stringify({ email: email.trim().toLowerCase() }),
redirect: 'follow',
});
const json = await resp.json().catch(() => ({}));
if (!resp.ok) {
console.error('[VK] API request failed:', json.message || 'Unknown error');
throw new Error(json.message || `HTTP ${resp.status}: Virtual key request failed`);
}
const vKey = json?.data?.virtualKey || json?.data?.virtual_key || json?.data?.newVKey?.slug;
if (!vKey) throw new Error('virtual key missing in response');
return vKey;
}
class AuthService {
constructor() {
this.currentUserId = 'default_user';
this.currentUserMode = 'local'; // 'local' or 'firebase'
this.currentUser = null;
this.hasApiKey = false; // Add a flag for API key status
this.isInitialized = false;
}
initialize() {
if (this.isInitialized) return;
const auth = getFirebaseAuth();
onAuthStateChanged(auth, async (user) => {
const previousUser = this.currentUser;
if (user) {
// User signed IN
console.log(`[AuthService] Firebase user signed in:`, user.uid);
this.currentUser = user;
this.currentUserId = user.uid;
this.currentUserMode = 'firebase';
this.hasApiKey = false; // Optimistically assume no key yet
// Broadcast immediately to make UI feel responsive
this.broadcastUserState();
// Start background task to fetch and save virtual key
(async () => {
try {
const idToken = await user.getIdToken(true);
const virtualKey = await getVirtualKeyByEmail(user.email, idToken);
await userRepository.saveApiKey(virtualKey, user.uid, 'openai');
console.log(`[AuthService] BG: Virtual key for ${user.email} has been saved.`);
// Now update the key status, which will trigger another broadcast
await this.updateApiKeyStatus();
} catch (error) {
console.error('[AuthService] BG: Failed to fetch or save virtual key:', error);
}
})();
} else {
// User signed OUT
console.log(`[AuthService] Firebase user signed out.`);
if (previousUser) {
console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`);
await userRepository.saveApiKey(null, previousUser.uid);
}
this.currentUser = null;
this.currentUserId = 'default_user';
this.currentUserMode = 'local';
// Update API key status (e.g., if a local key for default_user exists)
// This will also broadcast the final logged-out state.
await this.updateApiKeyStatus();
}
});
// Check for initial API key state
this.updateApiKeyStatus();
this.isInitialized = true;
console.log('[AuthService] Initialized and attached to Firebase Auth state.');
}
async signInWithCustomToken(token) {
const auth = getFirebaseAuth();
try {
const userCredential = await signInWithCustomToken(auth, token);
console.log(`[AuthService] Successfully signed in with custom token for user:`, userCredential.user.uid);
// onAuthStateChanged will handle the state update and broadcast
} catch (error) {
console.error('[AuthService] Error signing in with custom token:', error);
throw error; // Re-throw to be handled by the caller
}
}
async signOut() {
const auth = getFirebaseAuth();
try {
await signOut(auth);
console.log('[AuthService] User sign-out initiated successfully.');
// onAuthStateChanged will handle the state update and broadcast,
// which will also re-evaluate the API key status.
} catch (error) {
console.error('[AuthService] Error signing out:', error);
}
}
broadcastUserState() {
const userState = this.getCurrentUser();
console.log('[AuthService] Broadcasting user state change:', userState);
BrowserWindow.getAllWindows().forEach(win => {
if (win && !win.isDestroyed()) {
win.webContents.send('user-state-changed', userState);
}
});
}
/**
* Updates the internal API key status from the repository and broadcasts if changed.
*/
async updateApiKeyStatus() {
try {
const user = await userRepository.getById(this.currentUserId);
const newStatus = !!(user && user.api_key);
if (this.hasApiKey !== newStatus) {
console.log(`[AuthService] API key status changed to: ${newStatus}`);
this.hasApiKey = newStatus;
this.broadcastUserState();
}
} catch (error) {
console.error('[AuthService] Error checking API key status:', error);
this.hasApiKey = false;
}
}
getCurrentUserId() {
return this.currentUserId;
}
getCurrentUser() {
const isLoggedIn = !!(this.currentUserMode === 'firebase' && this.currentUser);
if (isLoggedIn) {
return {
uid: this.currentUser.uid,
email: this.currentUser.email,
displayName: this.currentUser.displayName,
mode: 'firebase',
isLoggedIn: true,
hasApiKey: this.hasApiKey // Always true for firebase users, but good practice
};
}
return {
uid: this.currentUserId, // returns 'default_user'
email: 'contact@pickle.com',
displayName: 'Default User',
mode: 'local',
isLoggedIn: false,
hasApiKey: this.hasApiKey
};
}
}
const authService = new AuthService();
module.exports = authService;

View File

@ -1,158 +0,0 @@
const config = require('../config/config');
class DataService {
constructor() {
this.cache = new Map();
this.cacheTimeout = config.get('cacheTimeout');
this.enableCaching = config.get('enableCaching');
this.sqliteClient = null;
this.currentUserId = 'default_user';
this.isInitialized = false;
if (config.get('enableSQLiteStorage')) {
try {
this.sqliteClient = require('./sqliteClient');
console.log('[DataService] SQLite storage enabled.');
} catch (error) {
console.error('[DataService] Failed to load SQLite client:', error);
}
}
}
async initialize() {
if (this.isInitialized || !this.sqliteClient) {
return;
}
try {
await this.sqliteClient.connect();
this.isInitialized = true;
console.log('[DataService] Initialized successfully');
} catch (error) {
console.error('[DataService] Failed to initialize:', error);
throw error;
}
}
setCurrentUser(uid) {
if (this.currentUserId !== uid) {
console.log(`[DataService] Current user switched to: ${uid}`);
this.currentUserId = uid;
this.clearCache();
}
}
getCacheKey(operation, params = '') {
return `${this.currentUserId}:${operation}:${params}`;
}
getFromCache(key) {
if (!this.enableCaching) return null;
const cached = this.cache.get(key);
if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
return cached.data;
}
return null;
}
setCache(key, data) {
if (!this.enableCaching) return;
this.cache.set(key, { data, timestamp: Date.now() });
}
clearCache() {
this.cache.clear();
}
async findOrCreateUser(firebaseUser) {
if (!this.sqliteClient) {
console.log('[DataService] SQLite client not available, skipping user creation');
return firebaseUser;
}
try {
await this.initialize();
const existingUser = await this.sqliteClient.getUser(firebaseUser.uid);
if (!existingUser) {
console.log(`[DataService] Creating new user in local DB: ${firebaseUser.uid}`);
await this.sqliteClient.findOrCreateUser({
uid: firebaseUser.uid,
display_name: firebaseUser.displayName || firebaseUser.display_name,
email: firebaseUser.email
});
}
this.clearCache();
return firebaseUser;
} catch (error) {
console.error('[DataService] Failed to sync Firebase user to local DB:', error);
return firebaseUser;
}
}
async saveApiKey(apiKey) {
if (!this.sqliteClient) {
throw new Error("SQLite client not available.");
}
try {
await this.initialize();
const result = await this.sqliteClient.saveApiKey(apiKey, this.currentUserId);
this.clearCache();
return result;
} catch (error) {
console.error('[DataService] Failed to save API key to SQLite:', error);
throw error;
}
}
async checkApiKey() {
if (!this.sqliteClient) return { hasApiKey: false };
try {
await this.initialize();
const user = await this.sqliteClient.getUser(this.currentUserId);
return { hasApiKey: !!user?.api_key && user.api_key.length > 0 };
} catch (error) {
console.error('[DataService] Failed to check API key from SQLite:', error);
return { hasApiKey: false };
}
}
async getUserPresets() {
const cacheKey = this.getCacheKey('presets');
const cached = this.getFromCache(cacheKey);
if (cached) return cached;
if (!this.sqliteClient) return [];
try {
await this.initialize();
const presets = await this.sqliteClient.getPresets(this.currentUserId);
this.setCache(cacheKey, presets);
return presets;
} catch (error) {
console.error('[DataService] Failed to get presets from SQLite:', error);
return [];
}
}
async getPresetTemplates() {
const cacheKey = this.getCacheKey('preset_templates');
const cached = this.getFromCache(cacheKey);
if (cached) return cached;
if (!this.sqliteClient) return [];
try {
await this.initialize();
const templates = await this.sqliteClient.getPresetTemplates();
this.setCache(cacheKey, templates);
return templates;
} catch (error) {
console.error('[DataService] Failed to get preset templates from SQLite:', error);
return [];
}
}
}
const dataService = new DataService();
module.exports = dataService;

View File

@ -0,0 +1,103 @@
const { initializeApp } = require('firebase/app');
const { initializeAuth } = require('firebase/auth');
const Store = require('electron-store');
/**
* Firebase Auth expects the `persistence` option passed to `initializeAuth()` to be *classes*,
* not instances. It then calls `new PersistenceClass()` internally.
*
* The helper below returns such a class, pre-configured with an `electron-store` instance that
* will be shared across all constructed objects. This mirrors the pattern used by Firebase's own
* `browserLocalPersistence` implementation as well as community solutions for NodeJS.
*/
function createElectronStorePersistence(storeName = 'firebase-auth-session') {
// Create a single `electron-store` behind the scenes all Persistence instances will use it.
const sharedStore = new Store({ name: storeName });
return class ElectronStorePersistence {
constructor() {
this.store = sharedStore;
this.type = 'LOCAL';
}
/**
* Firebase calls this to check whether the persistence is usable in the current context.
*/
_isAvailable() {
return Promise.resolve(true);
}
async _set(key, value) {
this.store.set(key, value);
}
async _get(key) {
return this.store.get(key) ?? null;
}
async _remove(key) {
this.store.delete(key);
}
/**
* These are used by Firebase to react to external storage events (e.g. multi-tab).
* Electron apps are single-renderer per process, so we can safely provide no-op
* implementations.
*/
_addListener(_key, _listener) {
// no-op
}
_removeListener(_key, _listener) {
// no-op
}
};
}
const firebaseConfig = {
apiKey: 'AIzaSyAgtJrmsFWG1C7m9S55HyT1laICEzuUS2g',
authDomain: 'pickle-3651a.firebaseapp.com',
projectId: 'pickle-3651a',
storageBucket: 'pickle-3651a.firebasestorage.app',
messagingSenderId: '904706892885',
appId: '1:904706892885:web:0e42b3dda796674ead20dc',
measurementId: 'G-SQ0WM6S28T',
};
let firebaseApp = null;
let firebaseAuth = null;
function initializeFirebase() {
if (firebaseApp) {
console.log('[FirebaseClient] Firebase already initialized.');
return;
}
try {
firebaseApp = initializeApp(firebaseConfig);
// Build a *class* persistence provider and hand it to Firebase.
const ElectronStorePersistence = createElectronStorePersistence('firebase-auth-session');
firebaseAuth = initializeAuth(firebaseApp, {
// `initializeAuth` accepts a single class or an array we pass an array for future
// extensibility and to match Firebase examples.
persistence: [ElectronStorePersistence],
});
console.log('[FirebaseClient] Firebase initialized successfully with class-based electron-store persistence.');
} catch (error) {
console.error('[FirebaseClient] Firebase initialization failed:', error);
}
}
function getFirebaseAuth() {
if (!firebaseAuth) {
throw new Error("Firebase Auth has not been initialized. Call initializeFirebase() first.");
}
return firebaseAuth;
}
module.exports = {
initializeFirebase,
getFirebaseAuth,
};

View File

@ -34,6 +34,13 @@ class SQLiteClient {
});
}
getDb() {
if (!this.db) {
throw new Error("Database not connected. Call connect() first.");
}
return this.db;
}
async synchronizeSchema() {
console.log('[DB Sync] Starting schema synchronization...');
const tablesInDb = await this.getTablesFromDb();
@ -194,231 +201,19 @@ class SQLiteClient {
});
}
async findOrCreateUser(user) {
return new Promise((resolve, reject) => {
const { uid, display_name, email } = user;
const now = Math.floor(Date.now() / 1000);
const query = `
INSERT INTO users (uid, display_name, email, created_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(uid) DO UPDATE SET
display_name=excluded.display_name,
email=excluded.email
`;
this.db.run(query, [uid, display_name, email, now], (err) => {
if (err) {
console.error('Failed to find or create user in SQLite:', err);
return reject(err);
}
this.getUser(uid).then(resolve).catch(reject);
});
});
}
async getUser(uid) {
return new Promise((resolve, reject) => {
this.db.get('SELECT * FROM users WHERE uid = ?', [uid], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
}
async saveApiKey(apiKey, uid = this.defaultUserId, provider = 'openai') {
return new Promise((resolve, reject) => {
this.db.run(
'UPDATE users SET api_key = ?, provider = ? WHERE uid = ?',
[apiKey, provider, uid],
function(err) {
if (err) {
console.error('SQLite: Failed to save API key:', err);
reject(err);
} else {
console.log(`SQLite: API key saved for user ${uid} with provider ${provider}.`);
resolve({ changes: this.changes });
}
}
async markPermissionsAsCompleted() {
return this.query(
'INSERT OR REPLACE INTO system_settings (key, value) VALUES (?, ?)',
['permissions_completed', 'true']
);
});
}
async getPresets(uid) {
return new Promise((resolve, reject) => {
const query = `
SELECT * FROM prompt_presets
WHERE uid = ? OR is_default = 1
ORDER BY is_default DESC, title ASC
`;
this.db.all(query, [uid], (err, rows) => {
if (err) {
console.error('SQLite: Failed to get presets:', err);
reject(err);
} else {
resolve(rows);
}
});
});
}
async getPresetTemplates() {
return new Promise((resolve, reject) => {
const query = `
SELECT * FROM prompt_presets
WHERE is_default = 1
ORDER BY title ASC
`;
this.db.all(query, [], (err, rows) => {
if (err) {
console.error('SQLite: Failed to get preset templates:', err);
reject(err);
} else {
resolve(rows);
}
});
});
}
async getSession(id) {
return new Promise((resolve, reject) => {
this.db.get('SELECT * FROM sessions WHERE id = ?', [id], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
}
async updateSessionType(id, type) {
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const query = 'UPDATE sessions SET session_type = ?, updated_at = ? WHERE id = ?';
this.db.run(query, [type, now, id], function(err) {
if (err) {
reject(err);
} else {
resolve({ changes: this.changes });
}
});
});
}
async touchSession(id) {
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const query = 'UPDATE sessions SET updated_at = ? WHERE id = ?';
this.db.run(query, [now, id], function(err) {
if (err) reject(err);
else resolve({ changes: this.changes });
});
});
}
async createSession(uid, type = 'ask') {
return new Promise((resolve, reject) => {
const sessionId = require('crypto').randomUUID();
const now = Math.floor(Date.now() / 1000);
const query = `INSERT INTO sessions (id, uid, title, session_type, started_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`;
this.db.run(query, [sessionId, uid, `Session @ ${new Date().toLocaleTimeString()}`, type, now, now], function(err) {
if (err) {
console.error('SQLite: Failed to create session:', err);
reject(err);
} else {
console.log(`SQLite: Created session ${sessionId} for user ${uid} (type: ${type})`);
resolve(sessionId);
}
});
});
}
async endSession(sessionId) {
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const query = `UPDATE sessions SET ended_at = ?, updated_at = ? WHERE id = ?`;
this.db.run(query, [now, now, sessionId], function(err) {
if (err) reject(err);
else resolve({ changes: this.changes });
});
});
}
async addTranscript({ sessionId, speaker, text }) {
return new Promise((resolve, reject) => {
this.touchSession(sessionId).catch(err => console.error("Failed to touch session", err));
const transcriptId = require('crypto').randomUUID();
const now = Math.floor(Date.now() / 1000);
const query = `INSERT INTO transcripts (id, session_id, start_at, speaker, text, created_at) VALUES (?, ?, ?, ?, ?, ?)`;
this.db.run(query, [transcriptId, sessionId, now, speaker, text, now], function(err) {
if (err) reject(err);
else resolve({ id: transcriptId });
});
});
}
async addAiMessage({ sessionId, role, content, model = 'gpt-4.1' }) {
return new Promise((resolve, reject) => {
this.touchSession(sessionId).catch(err => console.error("Failed to touch session", err));
const messageId = require('crypto').randomUUID();
const now = Math.floor(Date.now() / 1000);
const query = `INSERT INTO ai_messages (id, session_id, sent_at, role, content, model, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`;
this.db.run(query, [messageId, sessionId, now, role, content, model, now], function(err) {
if (err) reject(err);
else resolve({ id: messageId });
});
});
}
async saveSummary({ sessionId, tldr, text, bullet_json, action_json, model = 'gpt-4.1' }) {
return new Promise((resolve, reject) => {
this.touchSession(sessionId).catch(err => console.error("Failed to touch session", err));
const now = Math.floor(Date.now() / 1000);
const query = `
INSERT INTO summaries (session_id, generated_at, model, text, tldr, bullet_json, action_json, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(session_id) DO UPDATE SET
generated_at=excluded.generated_at,
model=excluded.model,
text=excluded.text,
tldr=excluded.tldr,
bullet_json=excluded.bullet_json,
action_json=excluded.action_json,
updated_at=excluded.updated_at
`;
this.db.run(query, [sessionId, now, model, text, tldr, bullet_json, action_json, now], function(err) {
if (err) reject(err);
else resolve({ changes: this.changes });
});
});
}
async runMigrations() {
return new Promise((resolve, reject) => {
console.log('[DB Migration] Checking schema for `sessions` table...');
this.db.all("PRAGMA table_info(sessions)", (err, columns) => {
if (err) {
console.error('[DB Migration] Error checking sessions table schema:', err);
return reject(err);
}
const hasSessionTypeCol = columns.some(col => col.name === 'session_type');
if (!hasSessionTypeCol) {
console.log('[DB Migration] `session_type` column missing. Altering table...');
this.db.run("ALTER TABLE sessions ADD COLUMN session_type TEXT DEFAULT 'ask'", (alterErr) => {
if (alterErr) {
console.error('[DB Migration] Failed to add `session_type` column:', alterErr);
return reject(alterErr);
}
console.log('[DB Migration] `sessions` table updated successfully.');
resolve();
});
} else {
console.log('[DB Migration] Schema is up to date.');
resolve();
}
});
});
async checkPermissionsCompleted() {
const result = await this.query(
'SELECT value FROM system_settings WHERE key = ?',
['permissions_completed']
);
return result.length > 0 && result[0].value === 'true';
}
close() {

View File

@ -5,10 +5,11 @@ const os = require('os');
const util = require('util');
const execFile = util.promisify(require('child_process').execFile);
const sharp = require('sharp');
const sqliteClient = require('../common/services/sqliteClient');
const authService = require('../common/services/authService');
const systemSettingsRepository = require('../common/repositories/systemSettings');
const userRepository = require('../common/repositories/user');
const fetch = require('node-fetch');
let currentFirebaseUser = null;
let isContentProtectionOn = true;
let currentDisplayId = null;
@ -233,7 +234,7 @@ class WindowLayoutManager {
const PAD = 8;
/* ① 헤더 중심 X를 “디스플레이 기준 상대좌표”로 변환 */
/* ① 헤더 중심 X를 "디스플레이 기준 상대좌표"로 변환 */
const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2;
let askBounds = askVisible ? ask.getBounds() : null;
@ -1365,17 +1366,6 @@ function setupIpcHandlers(openaiSessionRef) {
app.quit();
});
ipcMain.handle('message-sending', async event => {
console.log('📨 Main: Received message-sending signal');
const askWindow = windowPool.get('ask');
if (askWindow && !askWindow.isDestroyed()) {
console.log('📤 Main: Sending hide-text-input to ask window');
askWindow.webContents.send('hide-text-input');
return { success: true };
}
return { success: false };
});
ipcMain.handle('is-window-visible', (event, windowName) => {
const window = windowPool.get(windowName);
if (window && !window.isDestroyed()) {
@ -1444,46 +1434,6 @@ function setupIpcHandlers(openaiSessionRef) {
}
});
ipcMain.handle('get-available-screens', async () => {
try {
const sources = await desktopCapturer.getSources({
types: ['screen'],
thumbnailSize: { width: 300, height: 200 },
});
const displays = screen.getAllDisplays();
return sources.map((source, index) => {
const display = displays[index] || displays[0];
return {
id: source.id,
name: source.name,
thumbnail: source.thumbnail.toDataURL(),
display: {
id: display.id,
bounds: display.bounds,
workArea: display.workArea,
scaleFactor: display.scaleFactor,
isPrimary: display.id === screen.getPrimaryDisplay().id,
},
};
});
} catch (error) {
console.error('Failed to get available screens:', error);
return [];
}
});
ipcMain.handle('set-capture-source', (event, sourceId) => {
selectedCaptureSourceId = sourceId;
console.log(`[Capture] Selected source: ${sourceId}`);
return { success: true };
});
ipcMain.handle('get-capture-source', () => {
return selectedCaptureSourceId;
});
ipcMain.on('update-keybinds', (event, newKeybinds) => {
updateGlobalShortcuts(newKeybinds);
});
@ -1581,12 +1531,6 @@ function setupIpcHandlers(openaiSessionRef) {
}
});
ipcMain.on('move-to-edge', (event, direction) => {
if (movementManager) {
movementManager.moveToEdge(direction);
}
});
ipcMain.handle('force-close-window', (event, windowName) => {
const window = windowPool.get(windowName);
if (window && !window.isDestroyed()) {
@ -1627,68 +1571,7 @@ function setupIpcHandlers(openaiSessionRef) {
});
ipcMain.handle('capture-screenshot', async (event, options = {}) => {
if (process.platform === 'darwin') {
try {
const tempPath = path.join(os.tmpdir(), `screenshot-${Date.now()}.jpg`);
await execFile('screencapture', ['-x', '-t', 'jpg', tempPath]);
const imageBuffer = await fs.promises.readFile(tempPath);
await fs.promises.unlink(tempPath);
const resizedBuffer = await sharp(imageBuffer)
// .resize({ height: 1080 })
.resize({ height: 384 })
.jpeg({ quality: 80 })
.toBuffer();
const base64 = resizedBuffer.toString('base64');
const metadata = await sharp(resizedBuffer).metadata();
lastScreenshot = {
base64,
width: metadata.width,
height: metadata.height,
timestamp: Date.now(),
};
return { success: true, base64, width: metadata.width, height: metadata.height };
} catch (error) {
console.error('Failed to capture and resize screenshot:', error);
return { success: false, error: error.message };
}
}
try {
const sources = await desktopCapturer.getSources({
types: ['screen'],
thumbnailSize: {
width: 1920,
height: 1080,
},
});
if (sources.length === 0) {
throw new Error('No screen sources available');
}
const source = sources[0];
const buffer = source.thumbnail.toJPEG(70);
const base64 = buffer.toString('base64');
const size = source.thumbnail.getSize();
return {
success: true,
base64,
width: size.width,
height: size.height,
};
} catch (error) {
console.error('Failed to capture screenshot using desktopCapturer:', error);
return {
success: false,
error: error.message,
};
}
return captureScreenshot(options);
});
ipcMain.handle('get-current-screenshot', async event => {
@ -1715,132 +1598,18 @@ function setupIpcHandlers(openaiSessionRef) {
}
});
ipcMain.handle('firebase-auth-state-changed', (event, user) => {
console.log('[WindowManager] Firebase auth state changed:', user ? user.email : 'null');
const previousUser = currentFirebaseUser;
// 🛡️ Guard: ignore duplicate events where auth state did not actually change
const sameUser = user && previousUser && user.uid && previousUser.uid && user.uid === previousUser.uid;
const bothNull = !user && !previousUser;
if (sameUser || bothNull) {
// No real state change ➜ skip further processing
console.log('[WindowManager] No real state change, skipping further processing');
return;
}
currentFirebaseUser = user;
if (user && user.email) {
(async () => {
try {
const existingKey = getStoredApiKey();
if (existingKey) {
console.log('[WindowManager] Virtual key already exists, skipping fetch');
return;
}
if (!user.idToken) {
console.warn('[WindowManager] No ID token available, cannot fetch virtual key');
return;
}
console.log('[WindowManager] Fetching virtual key via onAuthStateChanged');
const vKey = await getVirtualKeyByEmail(user.email, user.idToken);
console.log('[WindowManager] Virtual key fetched successfully');
setApiKey(vKey)
.then(() => {
windowPool.forEach(win => {
if (win && !win.isDestroyed()) {
win.webContents.send('api-key-updated');
}
});
})
.catch(err => console.error('[WindowManager] Failed to save virtual key:', err));
} catch (err) {
console.error('[WindowManager] Virtual key fetch failed:', err);
if (err.message.includes('token') || err.message.includes('Authentication')) {
windowPool.forEach(win => {
if (win && !win.isDestroyed()) {
win.webContents.send('auth-error', {
message: 'Authentication expired. Please login again.',
shouldLogout: true,
});
}
});
}
}
})();
}
// If the user logged out, also hide the settings window
if (!user && previousUser) {
// ADDED: Only trigger on actual state change from logged in to logged out
console.log('[WindowManager] User logged out, clearing API key and notifying renderers');
setApiKey(null)
.then(() => {
console.log('[WindowManager] API key cleared successfully after logout');
windowPool.forEach(win => {
if (win && !win.isDestroyed()) {
win.webContents.send('api-key-removed');
}
});
})
.catch(err => {
console.error('[WindowManager] setApiKey error:', err);
windowPool.forEach(win => {
if (win && !win.isDestroyed()) {
win.webContents.send('api-key-removed');
}
});
});
const settingsWindow = windowPool.get('settings');
if (settingsWindow && settingsWindow.isVisible()) {
settingsWindow.hide();
console.log('[WindowManager] Settings window hidden after logout.');
}
}
// Broadcast to all windows
windowPool.forEach(win => {
if (win && !win.isDestroyed()) {
win.webContents.send('firebase-user-updated', user);
}
});
});
ipcMain.handle('get-current-firebase-user', () => {
return currentFirebaseUser;
});
ipcMain.handle('firebase-logout', () => {
ipcMain.handle('firebase-logout', async () => {
console.log('[WindowManager] Received request to log out.');
setApiKey(null)
.then(() => {
console.log('[WindowManager] API key cleared successfully after logout');
windowPool.forEach(win => {
if (win && !win.isDestroyed()) {
win.webContents.send('api-key-removed');
}
});
})
.catch(err => {
console.error('[WindowManager] setApiKey error:', err);
windowPool.forEach(win => {
if (win && !win.isDestroyed()) {
win.webContents.send('api-key-removed');
}
});
});
const header = windowPool.get('header');
if (header && !header.isDestroyed()) {
console.log('[WindowManager] Header window exists, sending to renderer...');
header.webContents.send('request-firebase-logout');
await authService.signOut();
await setApiKey(null);
windowPool.forEach(win => {
if (win && !win.isDestroyed()) {
win.webContents.send('api-key-removed');
}
});
});
ipcMain.handle('check-system-permissions', async () => {
const { systemPreferences } = require('electron');
@ -1944,11 +1713,8 @@ function setupIpcHandlers(openaiSessionRef) {
ipcMain.handle('mark-permissions-completed', async () => {
try {
// Store in SQLite that permissions have been completed
await sqliteClient.query(
'INSERT OR REPLACE INTO system_settings (key, value) VALUES (?, ?)',
['permissions_completed', 'true']
);
// This is a system-level setting, not user-specific.
await systemSettingsRepository.markPermissionsAsCompleted();
console.log('[Permissions] Marked permissions as completed');
return { success: true };
} catch (error) {
@ -1959,11 +1725,7 @@ function setupIpcHandlers(openaiSessionRef) {
ipcMain.handle('check-permissions-completed', async () => {
try {
const result = await sqliteClient.query(
'SELECT value FROM system_settings WHERE key = ?',
['permissions_completed']
);
const completed = result.length > 0 && result[0].value === 'true';
const completed = await systemSettingsRepository.checkPermissionsCompleted();
console.log('[Permissions] Permissions completed status:', completed);
return completed;
} catch (error) {
@ -1971,21 +1733,27 @@ function setupIpcHandlers(openaiSessionRef) {
return false;
}
});
ipcMain.handle('close-ask-window-if-empty', async () => {
const askWindow = windowPool.get('ask');
if (askWindow && !askWindow.isFocused()) {
askWindow.hide();
}
});
}
let storedApiKey = null;
let storedProvider = 'openai';
async function setApiKey(apiKey, provider = 'openai') {
storedApiKey = apiKey;
storedProvider = provider;
console.log('[WindowManager] API key and provider stored (and will be persisted to DB)');
console.log('[WindowManager] Persisting API key and provider to DB');
try {
await sqliteClient.saveApiKey(apiKey, sqliteClient.defaultUserId, provider);
await userRepository.saveApiKey(apiKey, authService.getCurrentUserId(), provider);
console.log('[WindowManager] API key and provider saved to SQLite');
// Notify authService that the key status may have changed
await authService.updateApiKeyStatus();
} catch (err) {
console.error('[WindowManager] Failed to save API key to SQLite:', err);
}
@ -2004,55 +1772,25 @@ async function setApiKey(apiKey, provider = 'openai') {
});
}
async function loadApiKeyFromDb() {
try {
const user = await sqliteClient.getUser(sqliteClient.defaultUserId);
if (user && user.api_key) {
console.log('[WindowManager] API key and provider loaded from SQLite for default user.');
storedApiKey = user.api_key;
storedProvider = user.provider || 'openai';
return user.api_key;
}
return null;
} catch (error) {
console.error('[WindowManager] Failed to load API key from SQLite:', error);
return null;
}
async function getStoredApiKey() {
const userId = authService.getCurrentUserId();
if (!userId) return null;
const user = await userRepository.getById(userId);
return user?.api_key || null;
}
function getCurrentFirebaseUser() {
return currentFirebaseUser;
}
function isFirebaseLoggedIn() {
return !!currentFirebaseUser;
}
function setCurrentFirebaseUser(user) {
currentFirebaseUser = user;
console.log('[WindowManager] Firebase user updated:', user ? user.email : 'null');
}
function getStoredApiKey() {
return storedApiKey;
}
function getStoredProvider() {
return storedProvider || 'openai';
async function getStoredProvider() {
const userId = authService.getCurrentUserId();
if (!userId) return 'openai';
const user = await userRepository.getById(userId);
return user?.provider || 'openai';
}
function setupApiKeyIPC() {
const { ipcMain } = require('electron');
ipcMain.handle('get-stored-api-key', async () => {
if (storedApiKey === null) {
const dbKey = await loadApiKeyFromDb();
if (dbKey) {
await setApiKey(dbKey, storedProvider);
}
}
return storedApiKey;
});
// Both handlers now do the same thing: fetch the key from the source of truth.
ipcMain.handle('get-stored-api-key', getStoredApiKey);
ipcMain.handle('api-key-validated', async (event, data) => {
console.log('[WindowManager] API key validation completed, saving...');
@ -2091,20 +1829,7 @@ function setupApiKeyIPC() {
return { success: true };
});
ipcMain.handle('get-current-api-key', async () => {
if (storedApiKey === null) {
const dbKey = await loadApiKeyFromDb();
if (dbKey) {
await setApiKey(dbKey, storedProvider);
}
}
return storedApiKey;
});
ipcMain.handle('get-ai-provider', async () => {
console.log('[WindowManager] AI provider requested from renderer');
return storedProvider || 'openai';
});
ipcMain.handle('get-ai-provider', getStoredProvider);
console.log('[WindowManager] API key related IPC handlers registered (SQLite-backed)');
}
@ -2470,42 +2195,40 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, openaiSessionRef) {
// ... other handlers like open-external, etc. can be added from the old file if needed
}
function clearApiKey() {
setApiKey(null);
}
async function getVirtualKeyByEmail(email, idToken) {
if (!idToken) {
throw new Error('Firebase ID token is required for virtual key request');
}
const resp = await fetch('https://serverless-api-sf3o.vercel.app/api/virtual_key', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${idToken}`,
},
body: JSON.stringify({ email: email.trim().toLowerCase() }),
redirect: 'follow',
});
const json = await resp.json().catch(() => ({}));
if (!resp.ok) {
console.error('[VK] API request failed:', json.message || 'Unknown error');
throw new Error(json.message || `HTTP ${resp.status}: Virtual key request failed`);
}
const vKey = json?.data?.virtualKey || json?.data?.virtual_key || json?.data?.newVKey?.slug;
if (!vKey) throw new Error('virtual key missing in response');
return vKey;
}
// Helper function to avoid code duplication
async function captureScreenshotInternal(options = {}) {
async function captureScreenshot(options = {}) {
if (process.platform === 'darwin') {
try {
const quality = options.quality || 'medium';
const tempPath = path.join(os.tmpdir(), `screenshot-${Date.now()}.jpg`);
await execFile('screencapture', ['-x', '-t', 'jpg', tempPath]);
const imageBuffer = await fs.promises.readFile(tempPath);
await fs.promises.unlink(tempPath);
const resizedBuffer = await sharp(imageBuffer)
// .resize({ height: 1080 })
.resize({ height: 384 })
.jpeg({ quality: 80 })
.toBuffer();
const base64 = resizedBuffer.toString('base64');
const metadata = await sharp(resizedBuffer).metadata();
lastScreenshot = {
base64,
width: metadata.width,
height: metadata.height,
timestamp: Date.now(),
};
return { success: true, base64, width: metadata.width, height: metadata.height };
} catch (error) {
console.error('Failed to capture and resize screenshot:', error);
return { success: false, error: error.message };
}
}
try {
const sources = await desktopCapturer.getSources({
types: ['screen'],
thumbnailSize: {
@ -2517,28 +2240,10 @@ async function captureScreenshotInternal(options = {}) {
if (sources.length === 0) {
throw new Error('No screen sources available');
}
const source = sources[0];
const thumbnail = source.thumbnail;
let jpegQuality;
switch (quality) {
case 'high':
jpegQuality = 90;
break;
case 'low':
jpegQuality = 50;
break;
case 'medium':
default:
jpegQuality = 70;
break;
}
const buffer = thumbnail.toJPEG(jpegQuality);
const buffer = source.thumbnail.toJPEG(70);
const base64 = buffer.toString('base64');
const size = thumbnail.getSize();
const size = source.thumbnail.getSize();
return {
success: true,
@ -2547,7 +2252,11 @@ async function captureScreenshotInternal(options = {}) {
height: size.height,
};
} catch (error) {
throw error;
console.error('Failed to capture screenshot using desktopCapturer:', error);
return {
success: false,
error: error.message,
};
}
}
@ -2558,9 +2267,5 @@ module.exports = {
setApiKey,
getStoredApiKey,
getStoredProvider,
clearApiKey,
getCurrentFirebaseUser,
isFirebaseLoggedIn,
setCurrentFirebaseUser,
getVirtualKeyByEmail,
captureScreenshot,
};

View File

@ -703,9 +703,10 @@ export class AskView extends LitElement {
handleWindowBlur() {
if (!this.currentResponse && !this.isLoading && !this.isStreaming) {
const askWindow = window.require('electron').remote.getCurrentWindow();
if (!askWindow.isFocused()) {
this.closeIfNoContent();
// If there's no active content, ask the main process to close this window.
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.invoke('close-ask-window-if-empty');
}
}
}
@ -793,13 +794,10 @@ export class AskView extends LitElement {
this.processAssistantQuestion(question);
};
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.on('ask-global-send', this.handleGlobalSendRequest);
ipcRenderer.on('toggle-text-input', this.handleToggleTextInput);
ipcRenderer.on('clear-ask-content', this.clearResponseContent);
ipcRenderer.on('receive-question-from-assistant', this.handleQuestionFromAssistant);
ipcRenderer.on('hide-text-input', () => {
console.log('📤 Hide text input signal received');
@ -865,7 +863,6 @@ export class AskView extends LitElement {
const { ipcRenderer } = window.require('electron');
ipcRenderer.removeListener('ask-global-send', this.handleGlobalSendRequest);
ipcRenderer.removeListener('toggle-text-input', this.handleToggleTextInput);
ipcRenderer.removeListener('clear-ask-content', this.clearResponseContent);
ipcRenderer.removeListener('clear-ask-response', () => {});
ipcRenderer.removeListener('hide-text-input', () => {});
ipcRenderer.removeListener('window-hide-animation', () => {});
@ -1054,8 +1051,6 @@ export class AskView extends LitElement {
}, 1500);
}
renderMarkdown(content) {
if (!content) return '';
@ -1125,7 +1120,9 @@ export class AskView extends LitElement {
this.requestUpdate();
this.renderContent();
window.pickleGlass.sendMessage(question).catch(error => {
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.invoke('ask:sendMessage', question).catch(error => {
console.error('Error processing assistant question:', error);
this.isLoading = false;
this.isStreaming = false;
@ -1133,6 +1130,7 @@ export class AskView extends LitElement {
this.renderContent();
});
}
}
async handleCopy() {
if (this.copyState === 'copied') return;
@ -1221,7 +1219,9 @@ export class AskView extends LitElement {
this.requestUpdate();
this.renderContent();
window.pickleGlass.sendMessage(text).catch(error => {
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.invoke('ask:sendMessage', text).catch(error => {
console.error('Error sending text:', error);
this.isLoading = false;
this.isStreaming = false;
@ -1229,8 +1229,14 @@ export class AskView extends LitElement {
this.renderContent();
});
}
}
handleTextKeydown(e) {
// Fix for IME composition issue: Ignore Enter key presses while composing.
if (e.isComposing) {
return;
}
const isPlainEnter = e.key === 'Enter' && !e.shiftKey && !e.metaKey && !e.ctrlKey;
const isModifierEnter = e.key === 'Enter' && (e.metaKey || e.ctrlKey);

View File

@ -0,0 +1,305 @@
const { ipcMain, BrowserWindow } = require('electron');
const { makeStreamingChatCompletionWithPortkey } = require('../../common/services/aiProviderService');
const { getConversationHistory } = require('../listen/liveSummaryService');
const { getStoredApiKey, getStoredProvider, windowPool, captureScreenshot } = require('../../electron/windowManager');
const authService = require('../../common/services/authService');
const sessionRepository = require('../../common/repositories/session');
const askRepository = require('./repositories');
const PICKLE_GLASS_SYSTEM_PROMPT = `<core_identity>
You are Pickle-Glass, developed and created by Pickle-Glass, and you are the user's live-meeting co-pilot.
</core_identity>
<objective>
Your goal is to help the user at the current moment in the conversation (the end of the transcript). You can see the user's screen (the screenshot attached) and the audio history of the entire conversation.
Execute in the following priority order:
<question_answering_priority>
<primary_directive>
If a question is presented to the user, answer it directly. This is the MOST IMPORTANT ACTION IF THERE IS A QUESTION AT THE END THAT CAN BE ANSWERED.
</primary_directive>
<question_response_structure>
Always start with the direct answer, then provide supporting details following the response format:
- **Short headline answer** (6 words) - the actual answer to the question
- **Main points** (1-2 bullets with 15 words each) - core supporting details
- **Sub-details** - examples, metrics, specifics under each main point
- **Extended explanation** - additional context and details as needed
</question_response_structure>
<intent_detection_guidelines>
Real transcripts have errors, unclear speech, and incomplete sentences. Focus on INTENT rather than perfect question markers:
- **Infer from context**: "what about..." "how did you..." "can you..." "tell me..." even if garbled
- **Incomplete questions**: "so the performance..." "and scaling wise..." "what's your approach to..."
- **Implied questions**: "I'm curious about X" "I'd love to hear about Y" "walk me through Z"
- **Transcription errors**: "what's your" "what's you" or "how do you" "how you" or "can you" "can u"
</intent_detection_guidelines>
<question_answering_priority_rules>
If the end of the transcript suggests someone is asking for information, explanation, or clarification - ANSWER IT. Don't get distracted by earlier content.
</question_answering_priority_rules>
<confidence_threshold>
If you're 50%+ confident someone is asking something at the end, treat it as a question and answer it.
</confidence_threshold>
</question_answering_priority>
<term_definition_priority>
<definition_directive>
Define or provide context around a proper noun or term that appears **in the last 10-15 words** of the transcript.
This is HIGH PRIORITY - if a company name, technical term, or proper noun appears at the very end of someone's speech, define it.
</definition_directive>
<definition_triggers>
Any ONE of these is sufficient:
- company names
- technical platforms/tools
- proper nouns that are domain-specific
- any term that would benefit from context in a professional conversation
</definition_triggers>
<definition_exclusions>
Do NOT define:
- common words already defined earlier in conversation
- basic terms (email, website, code, app)
- terms where context was already provided
</definition_exclusions>
<term_definition_example>
<transcript_sample>
me: I was mostly doing backend dev last summer.
them: Oh nice, what tech stack were you using?
me: A lot of internal tools, but also some Azure.
them: Yeah I've heard Azure is huge over there.
me: Yeah, I used to work at Microsoft last summer but now I...
</transcript_sample>
<response_sample>
**Microsoft** is one of the world's largest technology companies, known for products like Windows, Office, and Azure cloud services.
- **Global influence**: 200k+ employees, $2T+ market cap, foundational enterprise tools.
- Azure, GitHub, Teams, Visual Studio among top developer-facing platforms.
- **Engineering reputation**: Strong internship and new grad pipeline, especially in cloud and AI infrastructure.
</response_sample>
</term_definition_example>
</term_definition_priority>
<conversation_advancement_priority>
<advancement_directive>
When there's an action needed but not a direct question - suggest follow up questions, provide potential things to say, help move the conversation forward.
</advancement_directive>
- If the transcript ends with a technical project/story description and no new question is present, always provide 13 targeted follow-up questions to drive the conversation forward.
- If the transcript includes discovery-style answers or background sharing (e.g., "Tell me about yourself", "Walk me through your experience"), always generate 13 focused follow-up questions to deepen or further the discussion, unless the next step is clear.
- Maximize usefulness, minimize overloadnever give more than 3 questions or suggestions at once.
<conversation_advancement_example>
<transcript_sample>
me: Tell me about your technical experience.
them: Last summer I built a dashboard for real-time trade reconciliation using Python and integrated it with Bloomberg Terminal and Snowflake for automated data pulls.
</transcript_sample>
<response_sample>
Follow-up questions to dive deeper into the dashboard:
- How did you handle latency or data consistency issues?
- What made the Bloomberg integration challenging?
- Did you measure the impact on operational efficiency?
</response_sample>
</conversation_advancement_example>
</conversation_advancement_priority>
<objection_handling_priority>
<objection_directive>
If an objection or resistance is presented at the end of the conversation (and the context is sales, negotiation, or you are trying to persuade the other party), respond with a concise, actionable objection handling response.
- Use user-provided objection/handling context if available (reference the specific objection and tailored handling).
- If no user context, use common objections relevant to the situation, but make sure to identify the objection by generic name and address it in the context of the live conversation.
- State the objection in the format: **Objection: [Generic Objection Name]** (e.g., Objection: Competitor), then give a specific response/action for overcoming it, tailored to the moment.
- Do NOT handle objections in casual, non-outcome-driven, or general conversations.
- Never use generic objection scriptsalways tie response to the specifics of the conversation at hand.
</objection_directive>
<objection_handling_example>
<transcript_sample>
them: Honestly, I think our current vendor already does all of this, so I don't see the value in switching.
</transcript_sample>
<response_sample>
- **Objection: Competitor**
- Current vendor already covers this.
- Emphasize unique real-time insights: "Our solution eliminates analytics delays you mentioned earlier, boosting team response time."
</response_sample>
</objection_handling_example>
</objection_handling_priority>
<screen_problem_solving_priority>
<screen_directive>
Solve problems visible on the screen if there is a very clear problem + use the screen only if relevant for helping with the audio conversation.
</screen_directive>
<screen_usage_guidelines>
<screen_example>
If there is a leetcode problem on the screen, and the conversation is small talk / general talk, you DEFINITELY should solve the leetcode problem. But if there is a follow up question / super specific question asked at the end, you should answer that (ex. What's the runtime complexity), using the screen as additional context.
</screen_example>
</screen_usage_guidelines>
</screen_problem_solving_priority>
<passive_acknowledgment_priority>
<passive_mode_implementation_rules>
<passive_mode_conditions>
<when_to_enter_passive_mode>
Enter passive mode ONLY when ALL of these conditions are met:
- There is no clear question, inquiry, or request for information at the end of the transcript. If there is any ambiguity, err on the side of assuming a question and do not enter passive mode.
- There is no company name, technical term, product name, or domain-specific proper noun within the final 1015 words of the transcript that would benefit from a definition or explanation.
- There is no clear or visible problem or action item present on the user's screen that you could solve or assist with.
- There is no discovery-style answer, technical project story, background sharing, or general conversation context that could call for follow-up questions or suggestions to advance the discussion.
- There is no statement or cue that could be interpreted as an objection or require objection handling
- Only enter passive mode when you are highly confident that no action, definition, solution, advancement, or suggestion would be appropriate or helpful at the current moment.
</when_to_enter_passive_mode>
<passive_mode_behavior>
**Still show intelligence** by:
- Saying "Not sure what you need help with right now"
- Referencing visible screen elements or audio patterns ONLY if truly relevant
- Never giving random summaries unless explicitly asked
</passive_acknowledgment_priority>
</passive_mode_implementation_rules>
</objective>
User-provided context (defer to this information over your general knowledge / if there is specific script/desired responses prioritize this over previous instructions)
Make sure to **reference context** fully if it is provided (ex. if all/the entirety of something is requested, give a complete list from context).
----------
{{CONVERSATION_HISTORY}}`;
function formatConversationForPrompt(conversationTexts) {
if (!conversationTexts || conversationTexts.length === 0) return 'No conversation history available.';
return conversationTexts.slice(-30).join('\n');
}
async function sendMessage(userPrompt) {
if (!userPrompt || userPrompt.trim().length === 0) {
console.warn('[AskService] Cannot process empty message');
return { success: false, error: 'Empty message' };
}
const askWindow = windowPool.get('ask');
if (askWindow && !askWindow.isDestroyed()) {
askWindow.webContents.send('hide-text-input');
}
try {
console.log(`[AskService] 🤖 Processing message: ${userPrompt.substring(0, 50)}...`);
const screenshotResult = await captureScreenshot({ quality: 'medium' });
const screenshotBase64 = screenshotResult.success ? screenshotResult.base64 : null;
const conversationHistoryRaw = getConversationHistory();
const conversationHistory = formatConversationForPrompt(conversationHistoryRaw);
const systemPrompt = PICKLE_GLASS_SYSTEM_PROMPT.replace('{{CONVERSATION_HISTORY}}', conversationHistory);
const API_KEY = await getStoredApiKey();
if (!API_KEY) {
throw new Error('No API key found');
}
const messages = [
{ role: 'system', content: systemPrompt },
{
role: 'user',
content: [
{ type: 'text', text: `User Request: ${userPrompt.trim()}` },
],
},
];
if (screenshotBase64) {
messages[1].content.push({
type: 'image_url',
image_url: { url: `data:image/jpeg;base64,${screenshotBase64}` },
});
}
const provider = await getStoredProvider();
const { isLoggedIn } = authService.getCurrentUser();
const usePortkey = isLoggedIn && provider === 'openai';
console.log(`[AskService] 🚀 Sending request to ${provider} AI...`);
const response = await makeStreamingChatCompletionWithPortkey({
apiKey: API_KEY,
provider: provider,
messages: messages,
temperature: 0.7,
maxTokens: 2048,
model: provider === 'openai' ? 'gpt-4.1' : 'gemini-2.5-flash',
usePortkey: usePortkey,
portkeyVirtualKey: usePortkey ? API_KEY : null
});
// --- Stream Processing ---
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullResponse = '';
const askWin = windowPool.get('ask');
if (!askWin || askWin.isDestroyed()) {
console.error("[AskService] Ask window is not available to send stream to.");
reader.cancel();
return;
}
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6);
if (data === '[DONE]') {
askWin.webContents.send('ask-response-stream-end');
// Save to DB
try {
const uid = authService.getCurrentUserId();
if (!uid) throw new Error("User not logged in, cannot save message.");
const sessionId = await sessionRepository.getOrCreateActive(uid, 'ask');
await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
await askRepository.addAiMessage({ sessionId, role: 'assistant', content: fullResponse });
console.log(`[AskService] DB: Saved ask/answer pair to session ${sessionId}`);
} catch(dbError) {
console.error("[AskService] DB: Failed to save ask/answer pair:", dbError);
}
return { success: true, response: fullResponse };
}
try {
const json = JSON.parse(data);
const token = json.choices[0]?.delta?.content || '';
if (token) {
fullResponse += token;
askWin.webContents.send('ask-response-chunk', { token });
}
} catch (error) {
// Ignore parsing errors for now
}
}
}
}
} catch (error) {
console.error('[AskService] Error processing message:', error);
return { success: false, error: error.message };
}
}
function initialize() {
ipcMain.handle('ask:sendMessage', async (event, userPrompt) => {
return sendMessage(userPrompt);
});
console.log('[AskService] Initialized and ready.');
}
module.exports = {
initialize,
};

View File

@ -0,0 +1,18 @@
const sqliteRepository = require('./sqlite.repository');
// const firebaseRepository = require('./firebase.repository'); // Future implementation
const authService = require('../../../common/services/authService');
function getRepository() {
// In the future, we can check the user's login status from authService
// const user = authService.getCurrentUser();
// if (user.isLoggedIn) {
// return firebaseRepository;
// }
return sqliteRepository;
}
// Directly export functions for ease of use, decided by the strategy
module.exports = {
addAiMessage: (...args) => getRepository().addAiMessage(...args),
getAllAiMessagesBySessionId: (...args) => getRepository().getAllAiMessagesBySessionId(...args),
};

View File

@ -0,0 +1,35 @@
const sqliteClient = require('../../../common/services/sqliteClient');
function addAiMessage({ sessionId, role, content, model = 'gpt-4.1' }) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const messageId = require('crypto').randomUUID();
const now = Math.floor(Date.now() / 1000);
const query = `INSERT INTO ai_messages (id, session_id, sent_at, role, content, model, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`;
db.run(query, [messageId, sessionId, now, role, content, model, now], function(err) {
if (err) {
console.error('SQLite: Failed to add AI message:', err);
reject(err);
}
else {
resolve({ id: messageId });
}
});
});
}
function getAllAiMessagesBySessionId(sessionId) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = "SELECT * FROM ai_messages WHERE session_id = ? ORDER BY sent_at ASC";
db.all(query, [sessionId], (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
});
}
module.exports = {
addAiMessage,
getAllAiMessagesBySessionId
};

View File

@ -338,26 +338,17 @@ export class CustomizeView extends LitElement {
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.on('firebase-user-updated', (event, user) => {
this.firebaseUser = user;
if (!user) {
this.apiKey = null;
this._userStateListener = (event, userState) => {
console.log('[CustomizeView] Received user-state-changed:', userState);
if (userState && userState.isLoggedIn) {
this.firebaseUser = userState;
} else {
this.firebaseUser = null;
}
this.getApiKeyFromStorage(); // Also update API key display
this.requestUpdate();
});
ipcRenderer.on('user-changed', (event, firebaseUser) => {
console.log('[CustomizeView] Received user-changed:', firebaseUser);
this.firebaseUser = {
uid: firebaseUser.uid,
email: firebaseUser.email,
name: firebaseUser.displayName,
photoURL: firebaseUser.photoURL,
};
this.requestUpdate();
});
ipcRenderer.on('user-state-changed', this._userStateListener);
ipcRenderer.on('api-key-validated', (event, newApiKey) => {
console.log('[CustomizeView] Received api-key-validated, updating state.');
@ -376,12 +367,7 @@ export class CustomizeView extends LitElement {
this.requestUpdate();
});
this.loadInitialFirebaseUser();
ipcRenderer.invoke('get-current-api-key').then(key => {
this.apiKey = key;
this.requestUpdate();
});
this.loadInitialUser();
}
}
@ -393,8 +379,9 @@ export class CustomizeView extends LitElement {
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.removeAllListeners('firebase-user-updated');
ipcRenderer.removeAllListeners('user-changed');
if (this._userStateListener) {
ipcRenderer.removeListener('user-state-changed', this._userStateListener);
}
ipcRenderer.removeAllListeners('api-key-validated');
ipcRenderer.removeAllListeners('api-key-updated');
ipcRenderer.removeAllListeners('api-key-removed');
@ -1073,37 +1060,20 @@ export class CustomizeView extends LitElement {
}
}
async loadInitialFirebaseUser() {
if (!window.require) {
console.log('[CustomizeView] Electron not available');
return;
}
async loadInitialUser() {
if (!window.require) return;
const { ipcRenderer } = window.require('electron');
try {
console.log('[CustomizeView] Loading initial Firebase user...');
for (let i = 0; i < 3; i++) {
const user = await ipcRenderer.invoke('get-current-firebase-user');
console.log(`[CustomizeView] Attempt ${i + 1} - Firebase user:`, user);
if (user) {
this.firebaseUser = user;
this.requestUpdate();
console.log('[CustomizeView] Firebase user loaded successfully:', user.email);
return;
}
await new Promise(resolve => setTimeout(resolve, 100));
}
console.log('[CustomizeView] No Firebase user found after 3 attempts');
console.log('[CustomizeView] Loading initial user state...');
const userState = await ipcRenderer.invoke('get-current-user');
if (userState && userState.isLoggedIn) {
this.firebaseUser = userState;
} else {
this.firebaseUser = null;
}
this.requestUpdate();
} catch (error) {
console.error('[CustomizeView] Failed to load Firebase user:', error);
console.error('[CustomizeView] Failed to load initial user:', error);
this.firebaseUser = null;
this.requestUpdate();
}
@ -1112,7 +1082,7 @@ export class CustomizeView extends LitElement {
getApiKeyFromStorage() {
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.invoke('get-current-api-key').then(key => {
ipcRenderer.invoke('get-stored-api-key').then(key => {
this.apiKey = key;
this.requestUpdate();
}).catch(error => {

View File

@ -1,179 +0,0 @@
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
export class SetupView extends LitElement {
static styles = css`
* {
font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
cursor: default;
user-select: none;
}
.welcome {
color: white;
font-size: 12px;
font-family: 'Helvetica Neue', sans-serif;
font-weight: 500;
margin-bottom: 8px;
margin-top: auto;
}
.input-group {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.input-group input {
flex: 1;
}
input {
background: rgba(255, 255, 255, 0.20);
border-radius: 8px;
outline: 1px rgba(255, 255, 255, 0.50) solid;
outline-offset: -1px;
backdrop-filter: blur(0.50px);
border: none;
color: white;
padding: 12px 16px;
width: 100%;
font-size: 14px;
font-family: 'Helvetica Neue', sans-serif;
font-weight: 400;
}
input:focus {
outline: none;
}
input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.start-button {
background: var(--start-button-background);
color: var(--start-button-color);
border: 1px solid var(--start-button-border);
padding: 8px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
}
.start-button:hover {
background: var(--start-button-hover-background);
border-color: var(--start-button-hover-border);
}
.start-button.initializing {
opacity: 0.5;
}
.start-button.initializing:hover {
background: var(--start-button-background);
border-color: var(--start-button-border);
}
.description {
color: var(--description-color);
font-size: 14px;
margin-bottom: 24px;
line-height: 1.5;
}
.link {
color: var(--link-color);
text-decoration: underline;
cursor: pointer;
}
:host {
height: 100%;
display: flex;
flex-direction: column;
width: 100%;
max-width: 500px;
}
`;
static properties = {
onStart: { type: Function },
onAPIKeyHelp: { type: Function },
isInitializing: { type: Boolean },
onLayoutModeChange: { type: Function },
};
constructor() {
super();
this.onStart = () => {};
this.onAPIKeyHelp = () => {};
this.isInitializing = false;
this.onLayoutModeChange = () => {};
}
connectedCallback() {
super.connectedCallback();
window.electron?.ipcRenderer?.on('session-initializing', (event, isInitializing) => {
this.isInitializing = isInitializing;
});
// Load and apply layout mode on startup
this.loadLayoutMode();
}
disconnectedCallback() {
super.disconnectedCallback();
window.electron?.ipcRenderer?.removeAllListeners('session-initializing');
}
handleInput(e) {
localStorage.setItem('apiKey', e.target.value);
}
handleStartClick() {
if (this.isInitializing) {
return;
}
this.onStart();
}
handleAPIKeyHelpClick() {
this.onAPIKeyHelp();
}
handleResetOnboarding() {
localStorage.removeItem('onboardingCompleted');
// Refresh the page to trigger onboarding
window.location.reload();
}
loadLayoutMode() {
const savedLayoutMode = localStorage.getItem('layoutMode');
if (savedLayoutMode && savedLayoutMode !== 'normal') {
// Notify parent component to apply the saved layout mode
this.onLayoutModeChange(savedLayoutMode);
}
}
render() {
return html`
<div class="welcome">Welcome</div>
<div class="input-group">
<input
type="password"
placeholder="Enter your openai API Key"
.value=${localStorage.getItem('apiKey') || ''}
@input=${this.handleInput}
/>
<button @click=${this.handleStartClick} class="start-button ${this.isInitializing ? 'initializing' : ''}">Start Session</button>
</div>
<p class="description">
dont have an api key?
<span @click=${this.handleAPIKeyHelpClick} class="link">get one here</span>
</p>
`;
}
}
customElements.define('setup-view', SetupView);

View File

@ -6,14 +6,17 @@ const { getSystemPrompt } = require('../../common/prompts/promptBuilder.js');
const { connectToGeminiSession } = require('../../common/services/googleGeminiClient.js');
const { connectToOpenAiSession, createOpenAiGenerativeClient, getOpenAiGenerativeModel } = require('../../common/services/openAiClient.js');
const { makeChatCompletionWithPortkey } = require('../../common/services/aiProviderService.js');
const sqliteClient = require('../../common/services/sqliteClient');
const dataService = require('../../common/services/dataService');
const authService = require('../../common/services/authService');
const sessionRepository = require('../../common/repositories/session');
const listenRepository = require('./repositories');
const { isFirebaseLoggedIn, getCurrentFirebaseUser, getStoredProvider } = require('../../electron/windowManager.js');
const { getStoredApiKey, getStoredProvider } = require('../../electron/windowManager');
function getApiKey() {
const { getStoredApiKey } = require('../../electron/windowManager.js');
const storedKey = getStoredApiKey();
const MAX_BUFFER_LENGTH_CHARS = 2000;
const COMPLETION_DEBOUNCE_MS = 2000;
async function getApiKey() {
const storedKey = await getStoredApiKey();
if (storedKey) {
console.log('[LiveSummaryService] Using stored API key');
@ -37,7 +40,6 @@ async function getAiProvider() {
return provider || 'openai';
} catch (error) {
// If we're in the main process, get it directly
const { getStoredProvider } = require('../../electron/windowManager.js');
return getStoredProvider ? getStoredProvider() : 'openai';
}
}
@ -71,8 +73,6 @@ let analysisHistory = [];
// completions that arrive within this window are concatenated and flushed as
// **one** final turn.
const COMPLETION_DEBOUNCE_MS = 2000;
let myCompletionBuffer = '';
let theirCompletionBuffer = '';
let myCompletionTimer = null;
@ -185,6 +185,9 @@ Please build upon this context while analyzing the new conversation segments.
const systemPrompt = basePrompt.replace('{{CONVERSATION_HISTORY}}', recentConversation);
try {
if (currentSessionId) {
await sessionRepository.touch(currentSessionId);
}
const messages = [
{
role: 'system',
@ -218,13 +221,13 @@ Keep all points concise and build upon previous analysis if provided.`,
console.log('🤖 Sending analysis request to OpenAI...');
const API_KEY = getApiKey();
const API_KEY = await getApiKey();
if (!API_KEY) {
throw new Error('No API key available');
}
const provider = getStoredProvider ? getStoredProvider() : 'openai';
const loggedIn = isFirebaseLoggedIn(); // true ➜ vKey, false ➜ apiKey
const loggedIn = authService.getCurrentUser().isLoggedIn; // true ➜ vKey, false ➜ apiKey
const usePortkey = loggedIn && provider === 'openai'; // Only use Portkey for OpenAI with Firebase
console.log(`[LiveSummary] provider: ${provider}, usePortkey: ${usePortkey}`);
@ -245,7 +248,7 @@ Keep all points concise and build upon previous analysis if provided.`,
const structuredData = parseResponseText(responseText, previousAnalysisResult);
if (currentSessionId) {
sqliteClient.saveSummary({
listenRepository.saveSummary({
sessionId: currentSessionId,
tldr: structuredData.summary.join('\n'),
bullet_json: JSON.stringify(structuredData.topic.bullets),
@ -455,51 +458,13 @@ function getCurrentSessionData() {
}
// Conversation management functions
async function getOrCreateActiveSession(requestedType = 'ask') {
// 1. Check for an existing, valid session
if (currentSessionId) {
const session = await sqliteClient.getSession(currentSessionId);
if (session && !session.ended_at) {
// Ask sessions can expire, Listen sessions can't (they are closed explicitly)
const isExpired = session.session_type === 'ask' &&
(Date.now() / 1000) - session.updated_at > SESSION_IDLE_TIMEOUT_SECONDS;
if (!isExpired) {
// Session is valid, potentially promote it
if (requestedType === 'listen' && session.session_type === 'ask') {
await sqliteClient.updateSessionType(currentSessionId, 'listen');
console.log(`[Session] Promoted session ${currentSessionId} to 'listen'.`);
} else {
await sqliteClient.touchSession(currentSessionId);
}
return currentSessionId;
} else {
console.log(`[Session] Ask session ${currentSessionId} expired. Closing it.`);
await sqliteClient.endSession(currentSessionId);
currentSessionId = null; // Important: clear the expired session ID
}
}
}
// 2. If no valid session, create a new one
console.log(`[Session] No active session found. Creating a new one with type: ${requestedType}`);
const uid = dataService.currentUserId;
currentSessionId = await sqliteClient.createSession(uid, requestedType);
// Clear old conversation data for the new session
conversationHistory = [];
myCurrentUtterance = '';
theirCurrentUtterance = '';
previousAnalysisResult = null;
analysisHistory = [];
return currentSessionId;
}
async function initializeNewSession() {
try {
currentSessionId = await getOrCreateActiveSession('listen');
const uid = authService.getCurrentUserId();
if (!uid) {
throw new Error("Cannot initialize session: user not logged in.");
}
currentSessionId = await sessionRepository.getOrCreateActive(uid, 'listen');
console.log(`[DB] New listen session ensured: ${currentSessionId}`);
conversationHistory = [];
@ -541,7 +506,8 @@ async function saveConversationTurn(speaker, transcription) {
if (transcription.trim() === '') return;
try {
await sqliteClient.addTranscript({
await sessionRepository.touch(currentSessionId);
await listenRepository.addTranscript({
sessionId: currentSessionId,
speaker: speaker,
text: transcription.trim(),
@ -573,14 +539,15 @@ async function initializeLiveSummarySession(language = 'en') {
return false;
}
const loggedIn = isFirebaseLoggedIn();
const userState = authService.getCurrentUser();
const loggedIn = userState.isLoggedIn;
const keyType = loggedIn ? 'vKey' : 'apiKey';
isInitializingSession = true;
sendToRenderer('session-initializing', true);
sendToRenderer('update-status', 'Initializing sessions...');
const API_KEY = getApiKey();
const API_KEY = await getApiKey();
if (!API_KEY) {
console.error('FATAL ERROR: API Key is not defined.');
sendToRenderer('update-status', 'API Key not configured.');
@ -886,7 +853,7 @@ async function closeSession() {
stopAnalysisInterval();
if (currentSessionId) {
await sqliteClient.endSession(currentSessionId);
await sessionRepository.end(currentSessionId);
console.log(`[DB] Session ${currentSessionId} ended.`);
}
@ -971,40 +938,10 @@ function setupLiveSummaryIpcHandlers() {
}
});
ipcMain.handle('get-conversation-history', async () => {
try {
const formattedHistory = formatConversationForPrompt(conversationHistory);
console.log(`📤 Sending conversation history to renderer: ${conversationHistory.length} texts`);
return { success: true, data: formattedHistory };
} catch (error) {
console.error('Error getting conversation history:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('close-session', async () => {
return await closeSession();
});
ipcMain.handle('get-current-session', async event => {
try {
return { success: true, data: getCurrentSessionData() };
} catch (error) {
console.error('Error getting current session:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('start-new-session', async event => {
try {
initializeNewSession();
return { success: true, sessionId: currentSessionId };
} catch (error) {
console.error('Error starting new session:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('update-google-search-setting', async (event, enabled) => {
try {
console.log('Google Search setting updated to:', enabled);
@ -1014,33 +951,10 @@ function setupLiveSummaryIpcHandlers() {
return { success: false, error: error.message };
}
});
}
ipcMain.handle('save-ask-message', async (event, { userPrompt, aiResponse }) => {
try {
const sessionId = await getOrCreateActiveSession('ask');
if (!sessionId) {
throw new Error('Could not get or create a session for the ASK message.');
}
await sqliteClient.addAiMessage({
sessionId: sessionId,
role: 'user',
content: userPrompt
});
await sqliteClient.addAiMessage({
sessionId: sessionId,
role: 'assistant',
content: aiResponse
});
console.log(`[DB] Saved ask/answer pair to session ${sessionId}`);
return { success: true };
} catch(error) {
console.error('[IPC] Failed to save ask message:', error);
return { success: false, error: error.message };
}
});
function getConversationHistory() {
return conversationHistory;
}
module.exports = {
@ -1055,4 +969,5 @@ module.exports = {
setupLiveSummaryIpcHandlers,
isSessionActive,
closeSession,
getConversationHistory,
};

View File

@ -36,169 +36,6 @@ let lastScreenshotBase64 = null; // Store the latest screenshot
let realtimeConversationHistory = [];
const PICKLE_GLASS_SYSTEM_PROMPT = `<core_identity>
You are Pickle-Glass, developed and created by Pickle-Glass, and you are the user's live-meeting co-pilot.
</core_identity>
<objective>
Your goal is to help the user at the current moment in the conversation (the end of the transcript). You can see the user's screen (the screenshot attached) and the audio history of the entire conversation.
Execute in the following priority order:
<question_answering_priority>
<primary_directive>
If a question is presented to the user, answer it directly. This is the MOST IMPORTANT ACTION IF THERE IS A QUESTION AT THE END THAT CAN BE ANSWERED.
</primary_directive>
<question_response_structure>
Always start with the direct answer, then provide supporting details following the response format:
- **Short headline answer** (6 words) - the actual answer to the question
- **Main points** (1-2 bullets with 15 words each) - core supporting details
- **Sub-details** - examples, metrics, specifics under each main point
- **Extended explanation** - additional context and details as needed
</question_response_structure>
<intent_detection_guidelines>
Real transcripts have errors, unclear speech, and incomplete sentences. Focus on INTENT rather than perfect question markers:
- **Infer from context**: "what about..." "how did you..." "can you..." "tell me..." even if garbled
- **Incomplete questions**: "so the performance..." "and scaling wise..." "what's your approach to..."
- **Implied questions**: "I'm curious about X" "I'd love to hear about Y" "walk me through Z"
- **Transcription errors**: "what's your" "what's you" or "how do you" "how you" or "can you" "can u"
</intent_detection_guidelines>
<question_answering_priority_rules>
If the end of the transcript suggests someone is asking for information, explanation, or clarification - ANSWER IT. Don't get distracted by earlier content.
</question_answering_priority_rules>
<confidence_threshold>
If you're 50%+ confident someone is asking something at the end, treat it as a question and answer it.
</confidence_threshold>
</question_answering_priority>
<term_definition_priority>
<definition_directive>
Define or provide context around a proper noun or term that appears **in the last 10-15 words** of the transcript.
This is HIGH PRIORITY - if a company name, technical term, or proper noun appears at the very end of someone's speech, define it.
</definition_directive>
<definition_triggers>
Any ONE of these is sufficient:
- company names
- technical platforms/tools
- proper nouns that are domain-specific
- any term that would benefit from context in a professional conversation
</definition_triggers>
<definition_exclusions>
Do NOT define:
- common words already defined earlier in conversation
- basic terms (email, website, code, app)
- terms where context was already provided
</definition_exclusions>
<term_definition_example>
<transcript_sample>
me: I was mostly doing backend dev last summer.
them: Oh nice, what tech stack were you using?
me: A lot of internal tools, but also some Azure.
them: Yeah I've heard Azure is huge over there.
me: Yeah, I used to work at Microsoft last summer but now I...
</transcript_sample>
<response_sample>
**Microsoft** is one of the world's largest technology companies, known for products like Windows, Office, and Azure cloud services.
- **Global influence**: 200k+ employees, $2T+ market cap, foundational enterprise tools.
- Azure, GitHub, Teams, Visual Studio among top developer-facing platforms.
- **Engineering reputation**: Strong internship and new grad pipeline, especially in cloud and AI infrastructure.
</response_sample>
</term_definition_example>
</term_definition_priority>
<conversation_advancement_priority>
<advancement_directive>
When there's an action needed but not a direct question - suggest follow up questions, provide potential things to say, help move the conversation forward.
</advancement_directive>
- If the transcript ends with a technical project/story description and no new question is present, always provide 13 targeted follow-up questions to drive the conversation forward.
- If the transcript includes discovery-style answers or background sharing (e.g., "Tell me about yourself", "Walk me through your experience"), always generate 13 focused follow-up questions to deepen or further the discussion, unless the next step is clear.
- Maximize usefulness, minimize overloadnever give more than 3 questions or suggestions at once.
<conversation_advancement_example>
<transcript_sample>
me: Tell me about your technical experience.
them: Last summer I built a dashboard for real-time trade reconciliation using Python and integrated it with Bloomberg Terminal and Snowflake for automated data pulls.
</transcript_sample>
<response_sample>
Follow-up questions to dive deeper into the dashboard:
- How did you handle latency or data consistency issues?
- What made the Bloomberg integration challenging?
- Did you measure the impact on operational efficiency?
</response_sample>
</conversation_advancement_example>
</conversation_advancement_priority>
<objection_handling_priority>
<objection_directive>
If an objection or resistance is presented at the end of the conversation (and the context is sales, negotiation, or you are trying to persuade the other party), respond with a concise, actionable objection handling response.
- Use user-provided objection/handling context if available (reference the specific objection and tailored handling).
- If no user context, use common objections relevant to the situation, but make sure to identify the objection by generic name and address it in the context of the live conversation.
- State the objection in the format: **Objection: [Generic Objection Name]** (e.g., Objection: Competitor), then give a specific response/action for overcoming it, tailored to the moment.
- Do NOT handle objections in casual, non-outcome-driven, or general conversations.
- Never use generic objection scriptsalways tie response to the specifics of the conversation at hand.
</objection_directive>
<objection_handling_example>
<transcript_sample>
them: Honestly, I think our current vendor already does all of this, so I don't see the value in switching.
</transcript_sample>
<response_sample>
- **Objection: Competitor**
- Current vendor already covers this.
- Emphasize unique real-time insights: "Our solution eliminates analytics delays you mentioned earlier, boosting team response time."
</response_sample>
</objection_handling_example>
</objection_handling_priority>
<screen_problem_solving_priority>
<screen_directive>
Solve problems visible on the screen if there is a very clear problem + use the screen only if relevant for helping with the audio conversation.
</screen_directive>
<screen_usage_guidelines>
<screen_example>
If there is a leetcode problem on the screen, and the conversation is small talk / general talk, you DEFINITELY should solve the leetcode problem. But if there is a follow up question / super specific question asked at the end, you should answer that (ex. What's the runtime complexity), using the screen as additional context.
</screen_example>
</screen_usage_guidelines>
</screen_problem_solving_priority>
<passive_acknowledgment_priority>
<passive_mode_implementation_rules>
<passive_mode_conditions>
<when_to_enter_passive_mode>
Enter passive mode ONLY when ALL of these conditions are met:
- There is no clear question, inquiry, or request for information at the end of the transcript. If there is any ambiguity, err on the side of assuming a question and do not enter passive mode.
- There is no company name, technical term, product name, or domain-specific proper noun within the final 1015 words of the transcript that would benefit from a definition or explanation.
- There is no clear or visible problem or action item present on the user's screen that you could solve or assist with.
- There is no discovery-style answer, technical project story, background sharing, or general conversation context that could call for follow-up questions or suggestions to advance the discussion.
- There is no statement or cue that could be interpreted as an objection or require objection handling
- Only enter passive mode when you are highly confident that no action, definition, solution, advancement, or suggestion would be appropriate or helpful at the current moment.
</when_to_enter_passive_mode>
<passive_mode_behavior>
**Still show intelligence** by:
- Saying "Not sure what you need help with right now"
- Referencing visible screen elements or audio patterns ONLY if truly relevant
- Never giving random summaries unless explicitly asked
</passive_acknowledgment_priority>
</passive_mode_implementation_rules>
</objective>
User-provided context (defer to this information over your general knowledge / if there is specific script/desired responses prioritize this over previous instructions)
Make sure to **reference context** fully if it is provided (ex. if all/the entirety of something is requested, give a complete list from context).
----------
{{CONVERSATION_HISTORY}}`;
function base64ToFloat32Array(base64) {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
@ -218,8 +55,8 @@ function base64ToFloat32Array(base64) {
}
async function queryLoginState() {
const user = await ipcRenderer.invoke('get-current-firebase-user');
return { user, isLoggedIn: !!user };
const userState = await ipcRenderer.invoke('get-current-user');
return userState;
}
class SimpleAEC {
@ -869,12 +706,6 @@ function stopCapture() {
}
}
// Listen for screenshot updates from main process
ipcRenderer.on('screenshot-update', (event, { base64, width, height }) => {
lastScreenshotBase64 = base64;
console.log(`📸 Received screenshot update: ${width}x${height}`);
});
async function getCurrentScreenshot() {
try {
// First try to get a fresh screenshot from main process
@ -915,222 +746,10 @@ function formatRealtimeConversationHistory() {
return realtimeConversationHistory.slice(-30).join('\n');
}
async function sendMessage(userPrompt, options = {}) {
if (!userPrompt || userPrompt.trim().length === 0) {
console.warn('Cannot process empty message');
return { success: false, error: 'Empty message' };
}
if (window.require) {
const { ipcRenderer } = window.require('electron');
const isAskVisible = await ipcRenderer.invoke('is-window-visible', 'ask');
if (isAskVisible) {
ipcRenderer.send('clear-ask-response');
}
await ipcRenderer.invoke('message-sending');
}
try {
console.log(`🤖 Processing message: ${userPrompt.substring(0, 50)}...`);
// 1. Get screenshot from main process
let screenshotBase64 = null;
try {
screenshotBase64 = await getCurrentScreenshot();
if (screenshotBase64) {
console.log('📸 Screenshot obtained for message request');
} else {
console.warn('No screenshot available for message request');
}
} catch (error) {
console.warn('Failed to get screenshot:', error);
}
const conversationHistory = formatRealtimeConversationHistory();
console.log(`📝 Using conversation history: ${realtimeConversationHistory.length} texts`);
const systemPrompt = PICKLE_GLASS_SYSTEM_PROMPT.replace('{{CONVERSATION_HISTORY}}', conversationHistory);
let API_KEY = localStorage.getItem('openai_api_key');
if (!API_KEY && window.require) {
try {
const { ipcRenderer } = window.require('electron');
API_KEY = await ipcRenderer.invoke('get-stored-api-key');
} catch (error) {
console.error('Failed to get API key via IPC:', error);
}
}
if (!API_KEY) {
API_KEY = process.env.OPENAI_API_KEY;
}
if (!API_KEY) {
throw new Error('No API key found in storage, IPC, or environment');
}
console.log('[Renderer] Using API key for message request');
const messages = [
{
role: 'system',
content: systemPrompt,
},
{
role: 'user',
content: [
{
type: 'text',
text: `User Request: ${userPrompt.trim()}`,
},
],
},
];
if (screenshotBase64) {
messages[1].content.push({
type: 'image_url',
image_url: {
url: `data:image/jpeg;base64,${screenshotBase64}`,
},
});
console.log('📷 Screenshot included in message request');
}
const { isLoggedIn } = await queryLoginState();
const provider = await ipcRenderer.invoke('get-ai-provider');
const usePortkey = isLoggedIn && provider === 'openai';
console.log(`🚀 Sending request to ${provider} AI...`);
const response = await makeStreamingChatCompletionWithPortkey({
apiKey: API_KEY,
provider: provider,
messages: messages,
temperature: 0.7,
maxTokens: 2048,
model: provider === 'openai' ? 'gpt-4.1' : 'gemini-2.5-flash',
usePortkey: usePortkey,
portkeyVirtualKey: usePortkey ? API_KEY : null
});
// --- 스트리밍 응답 처리 ---
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullResponse = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6);
if (data === '[DONE]') {
// 스트리밍 종료 신호
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('ask-response-stream-end');
// Save the full conversation to DB
ipcRenderer.invoke('save-ask-message', {
userPrompt: userPrompt.trim(),
aiResponse: fullResponse
}).then(result => {
if (result.success) {
console.log('Ask/answer pair saved successfully.');
} else {
console.error('Failed to save ask/answer pair:', result.error);
}
});
}
return { success: true, response: fullResponse };
}
try {
const json = JSON.parse(data);
const token = json.choices[0]?.delta?.content || '';
if (token) {
fullResponse += token;
// 💡 렌더러 프로세스에 토큰 청크 전송
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('ask-response-chunk', { token });
}
}
} catch (error) {
console.error('Error parsing stream data chunk:', error, 'Chunk:', data);
}
}
}
}
// 이 부분은 스트리밍이 끝나면 사실상 도달하지 않음
return { success: true, response: fullResponse };
} catch (error) {
console.error('Error processing message:', error);
const errorMessage = `Error: ${error.message}`;
return { success: false, error: error.message, response: errorMessage };
}
}
const apiClient = window.require ? window.require('../common/services/apiClient') : undefined;
async function initConversationStorage() {
try {
const isOnline = await apiClient.checkConnection();
console.log('API 연결 상태:', isOnline);
return isOnline;
} catch (error) {
console.error('API 연결 실패:', error);
return false;
}
}
async function getConversationSession(sessionId) {
try {
if (!apiClient) {
throw new Error('API client not available');
}
const response = await apiClient.client.get(`/api/conversations/${sessionId}`);
return response.data;
} catch (error) {
console.error('대화 세션 조회 실패:', error);
throw error;
}
}
async function getAllConversationSessions() {
try {
if (!apiClient) {
throw new Error('API client not available');
}
const response = await apiClient.client.get('/api/conversations');
return response.data;
} catch (error) {
console.error('전체 대화 세션 조회 실패:', error);
throw error;
}
}
// Initialize conversation storage when renderer loads
initConversationStorage().catch(console.error);
window.pickleGlass = {
initializeopenai,
startCapture,
stopCapture,
sendMessage,
// Conversation history functions
getAllConversationSessions,
getConversationSession,
initConversationStorage,
isLinux: isLinux,
isMacOS: isMacOS,
e: pickleGlassElement,

View File

@ -0,0 +1,20 @@
const sqliteRepository = require('./sqlite.repository');
// const firebaseRepository = require('./firebase.repository'); // Future implementation
const authService = require('../../../common/services/authService');
function getRepository() {
// In the future, we can check the user's login status from authService
// const user = authService.getCurrentUser();
// if (user.isLoggedIn) {
// return firebaseRepository;
// }
return sqliteRepository;
}
// Directly export functions for ease of use, decided by the strategy
module.exports = {
addTranscript: (...args) => getRepository().addTranscript(...args),
saveSummary: (...args) => getRepository().saveSummary(...args),
getAllTranscriptsBySessionId: (...args) => getRepository().getAllTranscriptsBySessionId(...args),
getSummaryBySessionId: (...args) => getRepository().getSummaryBySessionId(...args),
};

View File

@ -0,0 +1,66 @@
const sqliteClient = require('../../../common/services/sqliteClient');
function addTranscript({ sessionId, speaker, text }) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const transcriptId = require('crypto').randomUUID();
const now = Math.floor(Date.now() / 1000);
const query = `INSERT INTO transcripts (id, session_id, start_at, speaker, text, created_at) VALUES (?, ?, ?, ?, ?, ?)`;
db.run(query, [transcriptId, sessionId, now, speaker, text, now], function(err) {
if (err) reject(err);
else resolve({ id: transcriptId });
});
});
}
function saveSummary({ sessionId, tldr, text, bullet_json, action_json, model = 'gpt-4.1' }) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const query = `
INSERT INTO summaries (session_id, generated_at, model, text, tldr, bullet_json, action_json, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(session_id) DO UPDATE SET
generated_at=excluded.generated_at,
model=excluded.model,
text=excluded.text,
tldr=excluded.tldr,
bullet_json=excluded.bullet_json,
action_json=excluded.action_json,
updated_at=excluded.updated_at
`;
db.run(query, [sessionId, now, model, text, tldr, bullet_json, action_json, now], function(err) {
if (err) reject(err);
else resolve({ changes: this.changes });
});
});
}
function getAllTranscriptsBySessionId(sessionId) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = "SELECT * FROM transcripts WHERE session_id = ? ORDER BY start_at ASC";
db.all(query, [sessionId], (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
});
}
function getSummaryBySessionId(sessionId) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = "SELECT * FROM summaries WHERE session_id = ?";
db.get(query, [sessionId], (err, row) => {
if (err) reject(err);
else resolve(row || null);
});
});
}
module.exports = {
addTranscript,
saveSummary,
getAllTranscriptsBySessionId,
getSummaryBySessionId
};

View File

@ -1,372 +0,0 @@
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
export class OnboardingView extends LitElement {
static styles = css`
* {
font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
cursor: default;
user-select: none;
}
:host {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
position: relative;
overflow: hidden;
}
.slide {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding: 20px;
text-align: left;
border-radius: 16px;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 50px;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
opacity: 0;
transform: translateX(100%);
}
.slide.active {
opacity: 1;
transform: translateX(0);
}
.slide.prev {
transform: translateX(-100%);
}
.slide-1 {
background: linear-gradient(135deg, #2d1b69 0%, #11998e 100%);
color: white;
}
.slide-2 {
background: linear-gradient(135deg, #8e2de2 0%, #4a00e0 100%);
color: white;
}
.slide-3 {
background: linear-gradient(135deg, #2c5aa0 0%, #1a237e 100%);
color: white;
}
.slide-4 {
background: linear-gradient(135deg, #fc466b 0%, #3f5efb 100%);
color: white;
}
.slide-5 {
background: linear-gradient(135deg, #833ab4 0%, #fd1d1d 100%);
color: white;
}
.slide-title {
font-size: 24px;
font-weight: 700;
margin-bottom: 12px;
margin-top: 10px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.slide-content {
font-size: 14px;
line-height: 1.5;
max-width: 100%;
margin-bottom: 20px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.context-textarea {
width: 100%;
max-width: 100%;
height: 80px;
padding: 12px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
color: white;
font-size: 14px;
resize: vertical;
box-sizing: border-box;
}
.context-textarea::placeholder {
color: rgba(255, 255, 255, 0.7);
}
.context-textarea:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.15);
}
.navigation {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
}
.nav-button {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.nav-button:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
}
.nav-button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.nav-button:disabled:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
}
.progress-dots {
display: flex;
gap: 12px;
align-items: center;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
}
.dot.active {
background: white;
transform: scale(1.2);
}
.emoji {
position: absolute;
top: 15px;
right: 15px;
font-size: 40px;
transform: rotate(15deg);
z-index: 1;
}
.feature-list {
text-align: left;
max-width: 100%;
}
.feature-item {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
}
.feature-icon {
font-size: 16px;
margin-right: 8px;
}
`;
static properties = {
currentSlide: { type: Number },
contextText: { type: String },
onComplete: { type: Function },
onClose: { type: Function },
};
constructor() {
super();
this.currentSlide = 0;
this.contextText = '';
this.onComplete = () => {};
this.onClose = () => {};
}
nextSlide() {
if (this.currentSlide < 4) {
this.currentSlide++;
} else {
this.completeOnboarding();
}
}
prevSlide() {
if (this.currentSlide > 0) {
this.currentSlide--;
}
}
handleContextInput(e) {
this.contextText = e.target.value;
}
completeOnboarding() {
// Save the context text to localStorage as custom prompt
if (this.contextText.trim()) {
localStorage.setItem('customPrompt', this.contextText.trim());
}
// Mark onboarding as completed
localStorage.setItem('onboardingCompleted', 'true');
// Call the completion callback
this.onComplete();
}
renderSlide1() {
return html`
<div class="slide slide-1 ${this.currentSlide === 0 ? 'active' : ''}">
<div class="emoji">👋</div>
<div class="slide-title">Welcome to Pickle Glass!</div>
<div class="slide-content">
Pickle Glass hears what you hear and sees what you see, then generates AI-powered suggestions without any user input needed.
</div>
</div>
`;
}
renderSlide2() {
return html`
<div class="slide slide-2 ${this.currentSlide === 1 ? 'active' : ''}">
<div class="emoji">🔒</div>
<div class="slide-title">Completely Private</div>
<div class="slide-content">
Your secret weapon is completely invisible! It won't show up on screen sharing apps, keeping your assistance completely private
during interviews and meetings.
</div>
</div>
`;
}
renderSlide3() {
return html`
<div class="slide slide-3 ${this.currentSlide === 2 ? 'active' : ''}">
<div class="emoji">📝</div>
<div class="slide-title">Tell Us Your Context</div>
<div class="slide-content">
Help the AI understand your situation better by sharing your context - like your resume, the job description, or interview
details.
</div>
<textarea
class="context-textarea"
placeholder="Paste your resume, job description, interview context, or any relevant information here..."
.value=${this.contextText}
@input=${this.handleContextInput}
></textarea>
</div>
`;
}
renderSlide4() {
return html`
<div class="slide slide-4 ${this.currentSlide === 3 ? 'active' : ''}">
<div class="emoji"></div>
<div class="slide-title">Explore More Features</div>
<div class="feature-list">
<div class="feature-item">
<span class="feature-icon">🎨</span>
Customize settings and AI profiles
</div>
<div class="feature-item">
<span class="feature-icon">📚</span>
View your conversation history
</div>
<div class="feature-item">
<span class="feature-icon">🔧</span>
Adjust screenshot intervals and quality
</div>
</div>
</div>
`;
}
renderSlide5() {
return html`
<div class="slide slide-5 ${this.currentSlide === 4 ? 'active' : ''}">
<div class="emoji">🎉</div>
<div class="slide-title">You're All Set!</div>
<div class="slide-content">
Pickle Glass is completely free to use. Just add your openai API key and start getting AI-powered assistance in your interviews
and meetings!
</div>
</div>
`;
}
render() {
return html`
${this.renderSlide1()} ${this.renderSlide2()} ${this.renderSlide3()} ${this.renderSlide4()} ${this.renderSlide5()}
<div class="navigation">
<button class="nav-button" @click=${this.prevSlide} ?disabled=${this.currentSlide === 0}>
<svg
width="16px"
height="16px"
stroke-width="1.7"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
color="#ffffff"
>
<path d="M15 6L9 12L15 18" stroke="#ffffff" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</button>
<div class="progress-dots">
${[0, 1, 2, 3, 4].map(index => html` <div class="dot ${index === this.currentSlide ? 'active' : ''}"></div> `)}
</div>
<button class="nav-button" @click=${this.nextSlide}>
${this.currentSlide === 4
? 'Get Started'
: html`
<svg
width="16px"
height="16px"
stroke-width="1.7"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
color="#ffffff"
>
<path d="M9 6L15 12L9 18" stroke="#ffffff" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
`}
</button>
</div>
`;
}
}
customElements.define('onboarding-view', OnboardingView);

View File

@ -14,14 +14,19 @@ if (require('electron-squirrel-startup')) {
const { app, BrowserWindow, shell, ipcMain, dialog } = require('electron');
const { createWindows } = require('./electron/windowManager.js');
const { setupLiveSummaryIpcHandlers, stopMacOSAudioCapture } = require('./features/listen/liveSummaryService.js');
const { initializeFirebase } = require('./common/services/firebaseClient');
const databaseInitializer = require('./common/services/databaseInitializer');
const dataService = require('./common/services/dataService');
const authService = require('./common/services/authService');
const path = require('node:path');
const { Deeplink } = require('electron-deeplink');
const express = require('express');
const fetch = require('node-fetch');
const { autoUpdater } = require('electron-updater');
const { EventEmitter } = require('events');
const askService = require('./features/ask/askService');
const sessionRepository = require('./common/repositories/session');
const eventBridge = new EventEmitter();
let WEB_PORT = 3000;
const openaiSessionRef = { current: null };
@ -91,19 +96,27 @@ app.whenReady().then(async () => {
});
}
const dbInitSuccess = await databaseInitializer.initialize();
if (!dbInitSuccess) {
console.error('>>> [index.js] Database initialization failed - some features may not work');
} else {
initializeFirebase();
databaseInitializer.initialize()
.then(() => {
console.log('>>> [index.js] Database initialized successfully');
}
// Clean up any zombie sessions from previous runs first
sessionRepository.endAllActiveSessions();
authService.initialize();
setupLiveSummaryIpcHandlers(openaiSessionRef);
askService.initialize();
setupGeneralIpcHandlers();
})
.catch(err => {
console.error('>>> [index.js] Database initialization failed - some features may not work', err);
});
WEB_PORT = await startWebStack();
console.log('Web front-end listening on', WEB_PORT);
setupLiveSummaryIpcHandlers(openaiSessionRef);
setupGeneralIpcHandlers();
createMainWindows();
initAutoUpdater();
@ -116,8 +129,10 @@ app.on('window-all-closed', () => {
}
});
app.on('before-quit', () => {
app.on('before-quit', async () => {
console.log('[Shutdown] App is about to quit.');
stopMacOSAudioCapture();
await sessionRepository.endAllActiveSessions();
databaseInitializer.close();
});
@ -145,19 +160,12 @@ app.on('open-url', (event, url) => {
app.setAsDefaultProtocolClient('pickleglass');
function setupGeneralIpcHandlers() {
ipcMain.handle('open-external', async (event, url) => {
try {
await shell.openExternal(url);
return { success: true };
} catch (error) {
console.error('Error opening external URL:', error);
return { success: false, error: error.message };
}
});
const userRepository = require('./common/repositories/user');
const presetRepository = require('./common/repositories/preset');
ipcMain.handle('save-api-key', async (event, apiKey) => {
try {
await dataService.saveApiKey(apiKey);
await userRepository.saveApiKey(apiKey, authService.getCurrentUserId());
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send('api-key-updated');
});
@ -168,21 +176,12 @@ function setupGeneralIpcHandlers() {
}
});
ipcMain.handle('check-api-key', async () => {
return await dataService.checkApiKey();
});
ipcMain.handle('get-user-presets', async () => {
return await dataService.getUserPresets();
return await presetRepository.getPresets(authService.getCurrentUserId());
});
ipcMain.handle('get-preset-templates', async () => {
return await dataService.getPresetTemplates();
});
ipcMain.on('set-current-user', (event, uid) => {
console.log(`[IPC] set-current-user: ${uid}`);
dataService.setCurrentUser(uid);
return await presetRepository.getPresetTemplates();
});
ipcMain.handle('start-firebase-auth', async () => {
@ -197,63 +196,119 @@ function setupGeneralIpcHandlers() {
}
});
ipcMain.on('firebase-auth-success', async (event, firebaseUser) => {
console.log('[IPC] firebase-auth-success:', firebaseUser.uid);
try {
await dataService.findOrCreateUser(firebaseUser);
dataService.setCurrentUser(firebaseUser.uid);
BrowserWindow.getAllWindows().forEach(win => {
if (win !== event.sender.getOwnerBrowserWindow()) {
win.webContents.send('user-changed', firebaseUser);
}
});
} catch (error) {
console.error('[IPC] Failed to handle firebase-auth-success:', error);
}
});
ipcMain.handle('get-api-url', () => {
return process.env.pickleglass_API_URL || 'http://localhost:9001';
});
ipcMain.handle('get-web-url', () => {
return process.env.pickleglass_WEB_URL || 'http://localhost:3000';
});
ipcMain.on('get-api-url-sync', (event) => {
event.returnValue = process.env.pickleglass_API_URL || 'http://localhost:9001';
});
ipcMain.handle('get-database-status', async () => {
return await databaseInitializer.getStatus();
});
ipcMain.handle('reset-database', async () => {
return await databaseInitializer.reset();
});
ipcMain.handle('get-current-user', async () => {
try {
const user = await dataService.sqliteClient.getUser(dataService.currentUserId);
if (user) {
return {
id: user.uid,
name: user.display_name,
isAuthenticated: user.uid !== 'default_user'
};
}
throw new Error('User not found in DataService');
} catch (error) {
console.error('Failed to get current user via DataService:', error);
return {
id: 'default_user',
name: 'Default User',
isAuthenticated: false
};
}
return authService.getCurrentUser();
});
// --- Web UI Data Handlers (New) ---
setupWebDataHandlers();
}
function setupWebDataHandlers() {
const sessionRepository = require('./common/repositories/session');
const listenRepository = require('./features/listen/repositories');
const askRepository = require('./features/ask/repositories');
const userRepository = require('./common/repositories/user');
const presetRepository = require('./common/repositories/preset');
const handleRequest = async (channel, responseChannel, payload) => {
let result;
const currentUserId = authService.getCurrentUserId();
try {
switch (channel) {
// SESSION
case 'get-sessions':
result = await sessionRepository.getAllByUserId(currentUserId);
break;
case 'get-session-details':
const session = await sessionRepository.getById(payload);
if (!session) {
result = null;
break;
}
const transcripts = await listenRepository.getAllTranscriptsBySessionId(payload);
const ai_messages = await askRepository.getAllAiMessagesBySessionId(payload);
const summary = await listenRepository.getSummaryBySessionId(payload);
result = { session, transcripts, ai_messages, summary };
break;
case 'delete-session':
result = await sessionRepository.deleteWithRelatedData(payload);
break;
case 'create-session':
const id = await sessionRepository.create(currentUserId, 'ask');
if (payload.title) {
await sessionRepository.updateTitle(id, payload.title);
}
result = { id };
break;
// USER
case 'get-user-profile':
result = await userRepository.getById(currentUserId);
break;
case 'update-user-profile':
result = await userRepository.update({ uid: currentUserId, ...payload });
break;
case 'find-or-create-user':
result = await userRepository.findOrCreate(payload);
break;
case 'save-api-key':
result = await userRepository.saveApiKey(payload, currentUserId);
break;
case 'check-api-key-status':
const user = await userRepository.getById(currentUserId);
result = { hasApiKey: !!user?.api_key && user.api_key.length > 0 };
break;
case 'delete-account':
result = await userRepository.deleteById(currentUserId);
break;
// PRESET
case 'get-presets':
result = await presetRepository.getPresets(currentUserId);
break;
case 'create-preset':
result = await presetRepository.create({ ...payload, uid: currentUserId });
break;
case 'update-preset':
result = await presetRepository.update(payload.id, payload.data, currentUserId);
break;
case 'delete-preset':
result = await presetRepository.delete(payload, currentUserId);
break;
// BATCH
case 'get-batch-data':
const includes = payload ? payload.split(',').map(item => item.trim()) : ['profile', 'presets', 'sessions'];
const batchResult = {};
if (includes.includes('profile')) {
batchResult.profile = await userRepository.getById(currentUserId);
}
if (includes.includes('presets')) {
batchResult.presets = await presetRepository.getPresets(currentUserId);
}
if (includes.includes('sessions')) {
batchResult.sessions = await sessionRepository.getAllByUserId(currentUserId);
}
result = batchResult;
break;
default:
throw new Error(`Unknown web data channel: ${channel}`);
}
eventBridge.emit(responseChannel, { success: true, data: result });
} catch (error) {
console.error(`Error handling web data request for ${channel}:`, error);
eventBridge.emit(responseChannel, { success: false, error: error.message });
}
};
eventBridge.on('web-data-request', handleRequest);
}
async function handleCustomUrl(url) {
@ -293,18 +348,12 @@ async function handleCustomUrl(url) {
}
async function handleFirebaseAuthCallback(params) {
const userRepository = require('./common/repositories/user');
const { token: idToken } = params;
if (!idToken) {
console.error('[Auth] Firebase auth callback is missing ID token.');
const { windowPool } = require('./electron/windowManager');
const header = windowPool.get('header');
if (header) {
header.webContents.send('login-successful', {
error: 'authentication_failed',
message: 'ID token not provided in deep link.'
});
}
// No need to send IPC, the UI won't transition without a successful auth state change.
return;
}
@ -320,8 +369,6 @@ async function handleFirebaseAuthCallback(params) {
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || 'Failed to exchange token.');
}
@ -336,71 +383,32 @@ async function handleFirebaseAuthCallback(params) {
photoURL: user.picture
};
await dataService.findOrCreateUser(firebaseUser);
dataService.setCurrentUser(user.uid);
// 1. Sync user data to local DB
await userRepository.findOrCreate(firebaseUser);
console.log('[Auth] User data synced with local DB.');
// if (firebaseUser.email && idToken) {
// try {
// const { getVirtualKeyByEmail, setApiKey } = require('./electron/windowManager');
// console.log('[Auth] Fetching virtual key for:', firebaseUser.email);
// const vKey = await getVirtualKeyByEmail(firebaseUser.email, idToken);
// console.log('[Auth] Virtual key fetched successfully');
// await setApiKey(vKey);
// console.log('[Auth] Virtual key saved successfully');
// const { setCurrentFirebaseUser } = require('./electron/windowManager');
// setCurrentFirebaseUser(firebaseUser);
// const { windowPool } = require('./electron/windowManager');
// windowPool.forEach(win => {
// if (win && !win.isDestroyed()) {
// win.webContents.send('api-key-updated');
// win.webContents.send('firebase-user-updated', firebaseUser);
// }
// });
// } catch (error) {
// console.error('[Auth] Virtual key fetch failed:', error);
// }
// }
// 2. Sign in using the authService in the main process
await authService.signInWithCustomToken(customToken);
console.log('[Auth] Main process sign-in initiated. Waiting for onAuthStateChanged...');
// 3. Focus the app window
const { windowPool } = require('./electron/windowManager');
const header = windowPool.get('header');
if (header) {
if (header.isMinimized()) header.restore();
header.focus();
console.log('[Auth] Sending custom token to renderer for sign-in.');
header.webContents.send('login-successful', {
customToken: customToken,
user: firebaseUser,
success: true
});
BrowserWindow.getAllWindows().forEach(win => {
if (win !== header) {
win.webContents.send('user-changed', firebaseUser);
}
});
console.log('[Auth] Firebase authentication completed successfully');
} else {
console.error('[Auth] Header window not found after getting custom token.');
console.error('[Auth] Header window not found after auth callback.');
}
} catch (error) {
console.error('[Auth] Error during custom token exchange:', error);
console.error('[Auth] Error during custom token exchange or sign-in:', error);
// The UI will not change, and the user can try again.
// Optionally, send a generic error event to the renderer.
const { windowPool } = require('./electron/windowManager');
const header = windowPool.get('header');
if (header) {
header.webContents.send('login-successful', {
error: 'authentication_failed',
message: error.message
});
header.webContents.send('auth-failed', { message: error.message });
}
}
}
@ -462,7 +470,7 @@ async function startWebStack() {
});
const createBackendApp = require('../pickleglass_web/backend_node');
const nodeApi = createBackendApp();
const nodeApi = createBackendApp(eventBridge);
const staticDir = app.isPackaged
? path.join(process.resourcesPath, 'out')

View File

@ -1,41 +1,2 @@
// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('ipcRenderer', {
send: (channel, data) => {
const validChannels = ['set-current-user', 'firebase-auth-success'];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
sendSync: (channel, data) => {
const validChannels = ['get-api-url-sync'];
if (validChannels.includes(channel)) {
return ipcRenderer.sendSync(channel, data);
}
},
on: (channel, func) => {
const validChannels = ['api-key-updated'];
if (validChannels.includes(channel)) {
const newCallback = (_, ...args) => func(...args);
ipcRenderer.on(channel, newCallback);
return () => {
ipcRenderer.removeListener(channel, newCallback);
};
}
},
invoke: (channel, ...args) => {
const validChannels = ['save-api-key'];
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, ...args);
}
},
removeAllListeners: (channel) => {
const validChannels = ['api-key-updated'];
if (validChannels.includes(channel)) {
ipcRenderer.removeAllListeners(channel);
}
},
});