fix logout disabled bug

This commit is contained in:
sanio 2025-07-05 22:47:42 +09:00
commit 30d43eb5fe
17 changed files with 508 additions and 300 deletions

View File

@ -92,6 +92,19 @@ You can visit [here](https://platform.openai.com/api-keys) to get your OpenAI AP
We love contributions! Feel free to open issues for bugs or feature requests.
## 🛠 Current Issues & Improvements
| Status | Issue | Description |
|--------|--------------------------------|---------------------------------------------------|
| 🚧 WIP | AEC Improvement | Transcription is not working occasionally |
| 🚧 WIP | Code Refactoring | Refactoring the entire codebase for better maintainability. |
| 🚧 WIP | Firebase Data Storage Issue | Session & ask should be saved in firebase |
| 🚧 WIP | Login Issue | Currently breaking when switching between local and sign-in mode |
| 🚧 WIP | Liquid Glass | Liquid Glass UI for MacOS 26 |
| 🚧 WIP | Permission Issue | Mic & system audio & display capture permission sometimes not working|
## About Pickle
**Our mission is to build a living digital clone for everyone.** Glass is part of Step 1—a trusted pipeline that transforms your daily data into a scalable clone. Visit [pickle.com](https://pickle.com) to learn more.

View File

@ -89,12 +89,7 @@ function SessionDetailsContent() {
)
}
const combinedConversation = [
...sessionDetails.transcripts.map(t => ({ ...t, type: 'transcript' as const, created_at: t.start_at })),
...sessionDetails.ai_messages.map(m => ({ ...m, type: 'ai_message' as const, created_at: m.sent_at }))
].sort((a, b) => (a.created_at || 0) - (b.created_at || 0));
const audioTranscripts = sessionDetails.transcripts.filter(t => t.speaker !== 'Me');
const askMessages = sessionDetails.ai_messages || [];
return (
<div className="min-h-screen bg-[#FDFCF9] text-gray-800">
@ -108,8 +103,8 @@ function SessionDetailsContent() {
</Link>
</div>
<div className="bg-white p-8 rounded-xl">
<div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="bg-white p-8 rounded-xl shadow-md border border-gray-100">
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-900 mb-2">
{sessionDetails.session.title || `Conversation on ${new Date(sessionDetails.session.started_at * 1000).toLocaleDateString()}`}
@ -117,6 +112,9 @@ function SessionDetailsContent() {
<div className="flex items-center text-sm text-gray-500 space-x-4">
<span>{new Date(sessionDetails.session.started_at * 1000).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}</span>
<span>{new Date(sessionDetails.session.started_at * 1000).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })}</span>
<span className={`capitalize px-2 py-0.5 rounded-full text-xs font-medium ${sessionDetails.session.session_type === 'listen' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'}`}>
{sessionDetails.session.session_type}
</span>
</div>
</div>
<button
@ -130,29 +128,57 @@ function SessionDetailsContent() {
{sessionDetails.summary && (
<Section title="Summary">
<p className="italic">"{sessionDetails.summary.tldr}"</p>
<p className="text-lg italic text-gray-600 mb-4">"{sessionDetails.summary.tldr}"</p>
{sessionDetails.summary.bullet_json && JSON.parse(sessionDetails.summary.bullet_json).length > 0 &&
<div className="mt-4">
<h3 className="font-semibold text-gray-700 mb-2">Key Points:</h3>
<ul className="list-disc list-inside space-y-1 text-gray-600">
{JSON.parse(sessionDetails.summary.bullet_json).map((point: string, index: number) => (
<li key={index}>{point}</li>
))}
</ul>
</div>
}
{sessionDetails.summary.action_json && JSON.parse(sessionDetails.summary.action_json).length > 0 &&
<div className="mt-4">
<h3 className="font-semibold text-gray-700 mb-2">Action Items:</h3>
<ul className="list-disc list-inside space-y-1 text-gray-600">
{JSON.parse(sessionDetails.summary.action_json).map((action: string, index: number) => (
<li key={index}>{action}</li>
))}
</ul>
</div>
}
</Section>
)}
<Section title="Notes">
{combinedConversation.map((item) => (
<p key={item.id}>
<span className="font-semibold">{(item.type === 'transcript' && item.speaker === 'Me') || (item.type === 'ai_message' && item.role === 'user') ? 'You: ' : 'AI: '}</span>
{item.type === 'transcript' ? item.text : item.content}
{sessionDetails.transcripts && sessionDetails.transcripts.length > 0 && (
<Section title="Listen: Transcript">
<div className="space-y-3">
{sessionDetails.transcripts.map((item) => (
<p key={item.id} className="text-gray-700">
<span className="font-semibold capitalize">{item.speaker}: </span>
{item.text}
</p>
))}
{combinedConversation.length === 0 && <p>No notes recorded for this session.</p>}
</div>
</Section>
<Section title="Audio transcript content">
{audioTranscripts.length > 0 ? (
<ul className="list-disc list-inside space-y-1">
{audioTranscripts.map(t => <li key={t.id}>{t.text}</li>)}
</ul>
) : (
<p>No audio transcript available for this session.</p>
)}
{askMessages.length > 0 && (
<Section title="Ask: Q&A">
<div className="space-y-4">
{askMessages.map((item) => (
<div key={item.id} className={`p-3 rounded-lg ${item.role === 'user' ? 'bg-gray-100' : 'bg-blue-50'}`}>
<p className="font-semibold capitalize text-sm text-gray-600 mb-1">{item.role === 'user' ? 'You' : 'AI'}</p>
<p className="text-gray-800 whitespace-pre-wrap">{item.content}</p>
</div>
))}
</div>
</Section>
)}
</div>
</div>
</div>

View File

@ -101,8 +101,8 @@ export default function ActivityPage() {
{deletingId === session.id ? 'Deleting...' : 'Delete'}
</button>
</div>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
Conversation
<span className={`capitalize inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${session.session_type === 'listen' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'}`}>
{session.session_type || 'ask'}
</span>
</div>
))}

View File

@ -7,78 +7,8 @@ const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.exec(`
-- users
CREATE TABLE IF NOT EXISTS users (
uid TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
email TEXT NOT NULL,
created_at INTEGER,
api_key TEXT
);
-- sessions
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
uid TEXT NOT NULL,
title TEXT,
started_at INTEGER,
ended_at INTEGER,
sync_state TEXT DEFAULT 'clean',
updated_at INTEGER
);
-- transcripts
CREATE TABLE IF NOT EXISTS transcripts (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
start_at INTEGER,
end_at INTEGER,
speaker TEXT,
text TEXT,
lang TEXT,
created_at INTEGER,
sync_state TEXT DEFAULT 'clean'
);
-- ai_messages
CREATE TABLE IF NOT EXISTS ai_messages (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
sent_at INTEGER,
role TEXT,
content TEXT,
tokens INTEGER,
model TEXT,
created_at INTEGER,
sync_state TEXT DEFAULT 'clean'
);
-- summaries
CREATE TABLE IF NOT EXISTS summaries (
session_id TEXT PRIMARY KEY,
generated_at INTEGER,
model TEXT,
text TEXT,
tldr TEXT,
bullet_json TEXT,
action_json TEXT,
tokens_used INTEGER,
updated_at INTEGER,
sync_state TEXT DEFAULT 'clean'
);
-- prompt_presets
CREATE TABLE IF NOT EXISTS prompt_presets (
id TEXT PRIMARY KEY,
uid TEXT NOT NULL,
title TEXT NOT NULL,
prompt TEXT NOT NULL,
is_default INTEGER NOT NULL,
created_at INTEGER,
sync_state TEXT DEFAULT 'clean'
);
`);
// The schema is now managed by the main Electron process on startup.
// This file can assume the schema is correct and up-to-date.
const defaultPresets = [
['school', 'School', 'You are a school and lecture assistant. Your goal is to help the user, a student, understand academic material and answer questions.\n\nWhenever a question appears on the user\'s screen or is asked aloud, you provide a direct, step-by-step answer, showing all necessary reasoning or calculations.\n\nIf the user is watching a lecture or working through new material, you offer concise explanations of key concepts and clarify definitions as they come up.', 1],

View File

@ -7,7 +7,7 @@ const validator = require('validator');
router.get('/', (req, res) => {
try {
const sessions = db.prepare(
"SELECT id, uid, title, started_at, ended_at, sync_state, updated_at FROM sessions WHERE uid = ? ORDER BY started_at DESC"
"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);
res.json(sessions);
} catch (error) {

View File

@ -24,6 +24,7 @@ export interface Session {
id: string;
uid: string;
title: string;
session_type: string;
started_at: number;
ended_at?: number;
sync_state: 'clean' | 'dirty';
@ -102,6 +103,7 @@ const convertFirestoreSession = (session: { id: string } & FirestoreSession, uid
id: session.id,
uid,
title: session.title,
session_type: session.session_type,
started_at: timestampToUnix(session.startedAt),
ended_at: session.endedAt ? timestampToUnix(session.endedAt) : undefined,
sync_state: 'clean',
@ -387,6 +389,7 @@ export const createSession = async (title?: string): Promise<{ id: string }> =>
const uid = firebaseAuth.currentUser!.uid;
const sessionId = await FirestoreSessionService.createSession(uid, {
title: title || 'New Session',
session_type: 'ask',
endedAt: undefined
});
return { id: sessionId };

View File

@ -24,6 +24,7 @@ export interface FirestoreUserProfile {
export interface FirestoreSession {
title: string;
session_type: string;
startedAt: Timestamp;
endedAt?: Timestamp;
}

View File

@ -109,7 +109,7 @@ export class ApiKeyHeader extends LitElement {
font-weight: 500; /* Medium */
margin: 0;
text-align: center;
flex-shrink: 0; /* 제목이 줄어들지 않도록 고정 */
flex-shrink: 0;
}
.form-content {
@ -117,14 +117,14 @@ export class ApiKeyHeader extends LitElement {
flex-direction: column;
align-items: center;
width: 100%;
margin-top: auto; /* 이 속성이 제목과 폼 사이의 공간을 만듭니다. */
margin-top: auto;
}
.error-message {
color: rgba(239, 68, 68, 0.9);
font-weight: 500;
font-size: 11px;
height: 14px; /* Reserve space to prevent layout shift */
height: 14px;
text-align: center;
margin-bottom: 4px;
}

View File

@ -113,6 +113,7 @@ class HeaderTransitionManager {
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);

View File

@ -51,7 +51,17 @@ export class PickleGlassApp extends LitElement {
this.currentView = urlParams.get('view') || 'listen';
this.currentResponseIndex = -1;
this.selectedProfile = localStorage.getItem('selectedProfile') || 'interview';
this.selectedLanguage = localStorage.getItem('selectedLanguage') || 'en-US';
// Language format migration for legacy users
let lang = localStorage.getItem('selectedLanguage') || 'en';
if (lang.includes('-')) {
const newLang = lang.split('-')[0];
console.warn(`[Migration] Correcting language format from "${lang}" to "${newLang}".`);
localStorage.setItem('selectedLanguage', newLang);
lang = newLang;
}
this.selectedLanguage = lang;
this.selectedScreenshotInterval = localStorage.getItem('selectedScreenshotInterval') || '5';
this.selectedImageQuality = localStorage.getItem('selectedImageQuality') || 'medium';
this._isClickThrough = false;

View File

@ -0,0 +1,77 @@
const LATEST_SCHEMA = {
users: {
columns: [
{ name: 'uid', type: 'TEXT PRIMARY KEY' },
{ name: 'display_name', type: 'TEXT NOT NULL' },
{ name: 'email', type: 'TEXT NOT NULL' },
{ name: 'created_at', type: 'INTEGER' },
{ name: 'api_key', type: 'TEXT' },
{ name: 'provider', type: 'TEXT DEFAULT \'openai\'' }
]
},
sessions: {
columns: [
{ name: 'id', type: 'TEXT PRIMARY KEY' },
{ name: 'uid', type: 'TEXT NOT NULL' },
{ name: 'title', type: 'TEXT' },
{ name: 'session_type', type: 'TEXT DEFAULT \'ask\'' },
{ name: 'started_at', type: 'INTEGER' },
{ name: 'ended_at', type: 'INTEGER' },
{ name: 'sync_state', type: 'TEXT DEFAULT \'clean\'' },
{ name: 'updated_at', type: 'INTEGER' }
]
},
transcripts: {
columns: [
{ name: 'id', type: 'TEXT PRIMARY KEY' },
{ name: 'session_id', type: 'TEXT NOT NULL' },
{ name: 'start_at', type: 'INTEGER' },
{ name: 'end_at', type: 'INTEGER' },
{ name: 'speaker', type: 'TEXT' },
{ name: 'text', type: 'TEXT' },
{ name: 'lang', type: 'TEXT' },
{ name: 'created_at', type: 'INTEGER' },
{ name: 'sync_state', type: 'TEXT DEFAULT \'clean\'' }
]
},
ai_messages: {
columns: [
{ name: 'id', type: 'TEXT PRIMARY KEY' },
{ name: 'session_id', type: 'TEXT NOT NULL' },
{ name: 'sent_at', type: 'INTEGER' },
{ name: 'role', type: 'TEXT' },
{ name: 'content', type: 'TEXT' },
{ name: 'tokens', type: 'INTEGER' },
{ name: 'model', type: 'TEXT' },
{ name: 'created_at', type: 'INTEGER' },
{ name: 'sync_state', type: 'TEXT DEFAULT \'clean\'' }
]
},
summaries: {
columns: [
{ name: 'session_id', type: 'TEXT PRIMARY KEY' },
{ name: 'generated_at', type: 'INTEGER' },
{ name: 'model', type: 'TEXT' },
{ name: 'text', type: 'TEXT' },
{ name: 'tldr', type: 'TEXT' },
{ name: 'bullet_json', type: 'TEXT' },
{ name: 'action_json', type: 'TEXT' },
{ name: 'tokens_used', type: 'INTEGER' },
{ name: 'updated_at', type: 'INTEGER' },
{ name: 'sync_state', type: 'TEXT DEFAULT \'clean\'' }
]
},
prompt_presets: {
columns: [
{ name: 'id', type: 'TEXT PRIMARY KEY' },
{ name: 'uid', type: 'TEXT NOT NULL' },
{ name: 'title', type: 'TEXT NOT NULL' },
{ name: 'prompt', type: 'TEXT NOT NULL' },
{ name: 'is_default', type: 'INTEGER NOT NULL' },
{ name: 'created_at', type: 'INTEGER' },
{ name: 'sync_state', type: 'TEXT DEFAULT \'clean\'' }
]
}
};
module.exports = LATEST_SCHEMA;

View File

@ -54,13 +54,11 @@ class DatabaseInitializer {
await sqliteClient.connect(this.dbPath); // DB 경로를 인자로 전달
// 연결 후 테이블 및 기본 데이터 초기화
// This single call will now synchronize the schema and then init default data.
await sqliteClient.initTables();
const user = await sqliteClient.getUser(sqliteClient.defaultUserId);
if (!user) {
await sqliteClient.initDefaultData();
console.log('[DB] Default data initialized.');
}
// Clean up any orphaned sessions from previous versions
await sqliteClient.cleanupEmptySessions();
this.isInitialized = true;
console.log('[DB] Database initialized successfully');
@ -141,6 +139,10 @@ class DatabaseInitializer {
try {
console.log('[DatabaseInitializer] Validating database integrity...');
// The synchronizeSchema function handles table and column creation now.
// We just need to ensure default data is present.
await sqliteClient.synchronizeSchema();
const defaultUser = await sqliteClient.getUser(sqliteClient.defaultUserId);
if (!defaultUser) {
console.log('[DatabaseInitializer] Default user not found - creating...');

View File

@ -1,5 +1,6 @@
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const LATEST_SCHEMA = require('../config/schema');
class SQLiteClient {
constructor() {
@ -33,96 +34,123 @@ class SQLiteClient {
});
}
async initTables() {
async synchronizeSchema() {
console.log('[DB Sync] Starting schema synchronization...');
const tablesInDb = await this.getTablesFromDb();
for (const tableName of Object.keys(LATEST_SCHEMA)) {
const tableSchema = LATEST_SCHEMA[tableName];
if (!tablesInDb.includes(tableName)) {
// Table doesn't exist, create it
await this.createTable(tableName, tableSchema);
} else {
// Table exists, check for missing columns
await this.updateTable(tableName, tableSchema);
}
}
console.log('[DB Sync] Schema synchronization finished.');
}
async getTablesFromDb() {
return new Promise((resolve, reject) => {
const schema = `
PRAGMA journal_mode = WAL;
this.db.all("SELECT name FROM sqlite_master WHERE type='table'", (err, tables) => {
if (err) return reject(err);
resolve(tables.map(t => t.name));
});
});
}
CREATE TABLE IF NOT EXISTS users (
uid TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
email TEXT NOT NULL,
created_at INTEGER,
api_key TEXT,
provider TEXT DEFAULT 'openai'
);
async createTable(tableName, tableSchema) {
return new Promise((resolve, reject) => {
const columnDefs = tableSchema.columns.map(col => `"${col.name}" ${col.type}`).join(', ');
const query = `CREATE TABLE IF NOT EXISTS "${tableName}" (${columnDefs})`;
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
uid TEXT NOT NULL,
title TEXT,
started_at INTEGER,
ended_at INTEGER,
sync_state TEXT DEFAULT 'clean',
updated_at INTEGER
);
console.log(`[DB Sync] Creating table: ${tableName}`);
this.db.run(query, (err) => {
if (err) return reject(err);
resolve();
});
});
}
CREATE TABLE IF NOT EXISTS transcripts (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
start_at INTEGER,
end_at INTEGER,
speaker TEXT,
text TEXT,
lang TEXT,
created_at INTEGER,
sync_state TEXT DEFAULT 'clean'
);
async updateTable(tableName, tableSchema) {
return new Promise((resolve, reject) => {
this.db.all(`PRAGMA table_info("${tableName}")`, async (err, existingColumns) => {
if (err) return reject(err);
CREATE TABLE IF NOT EXISTS ai_messages (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
sent_at INTEGER,
role TEXT,
content TEXT,
tokens INTEGER,
model TEXT,
created_at INTEGER,
sync_state TEXT DEFAULT 'clean'
);
const existingColumnNames = existingColumns.map(c => c.name);
const columnsToAdd = tableSchema.columns.filter(col => !existingColumnNames.includes(col.name));
CREATE TABLE IF NOT EXISTS summaries (
session_id TEXT PRIMARY KEY,
generated_at INTEGER,
model TEXT,
text TEXT,
tldr TEXT,
bullet_json TEXT,
action_json TEXT,
tokens_used INTEGER,
updated_at INTEGER,
sync_state TEXT DEFAULT 'clean'
);
if (columnsToAdd.length > 0) {
console.log(`[DB Sync] Updating table: ${tableName}. Adding columns: ${columnsToAdd.map(c=>c.name).join(', ')}`);
for (const column of columnsToAdd) {
const addColumnQuery = `ALTER TABLE "${tableName}" ADD COLUMN "${column.name}" ${column.type}`;
try {
await this.runQuery(addColumnQuery);
} catch (alterErr) {
return reject(alterErr);
}
}
}
resolve();
});
});
}
CREATE TABLE IF NOT EXISTS prompt_presets (
id TEXT PRIMARY KEY,
uid TEXT NOT NULL,
title TEXT NOT NULL,
prompt TEXT NOT NULL,
is_default INTEGER NOT NULL,
created_at INTEGER,
sync_state TEXT DEFAULT 'clean'
);
async runQuery(query, params = []) {
return new Promise((resolve, reject) => {
this.db.run(query, params, function(err) {
if (err) return reject(err);
resolve(this);
});
});
}
async cleanupEmptySessions() {
console.log('[DB Cleanup] Checking for empty sessions...');
const query = `
SELECT s.id FROM sessions s
LEFT JOIN transcripts t ON s.id = t.session_id
LEFT JOIN ai_messages a ON s.id = a.session_id
LEFT JOIN summaries su ON s.id = su.session_id
WHERE t.id IS NULL AND a.id IS NULL AND su.session_id IS NULL
`;
this.db.exec(schema, (err) => {
return new Promise((resolve, reject) => {
this.db.all(query, [], (err, rows) => {
if (err) {
console.error('Failed to create tables:', err);
console.error('[DB Cleanup] Error finding empty sessions:', err);
return reject(err);
}
console.log('All tables are ready.');
// Add provider column to existing databases
this.db.run("ALTER TABLE users ADD COLUMN provider TEXT DEFAULT 'openai'", (alterErr) => {
if (alterErr && !alterErr.message.includes('duplicate column')) {
console.log('Note: Could not add provider column (may already exist)');
if (rows.length === 0) {
console.log('[DB Cleanup] No empty sessions found.');
return resolve();
}
this.initDefaultData().then(resolve).catch(reject);
const idsToDelete = rows.map(r => r.id);
const placeholders = idsToDelete.map(() => '?').join(',');
const deleteQuery = `DELETE FROM sessions WHERE id IN (${placeholders})`;
console.log(`[DB Cleanup] Found ${idsToDelete.length} empty sessions. Deleting...`);
this.db.run(deleteQuery, idsToDelete, function(deleteErr) {
if (deleteErr) {
console.error('[DB Cleanup] Error deleting empty sessions:', deleteErr);
return reject(deleteErr);
}
console.log(`[DB Cleanup] Successfully deleted ${this.changes} empty sessions.`);
resolve();
});
});
});
}
async initTables() {
await this.synchronizeSchema();
await this.initDefaultData();
}
async initDefaultData() {
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
@ -252,18 +280,52 @@ class SQLiteClient {
});
}
async createSession(uid) {
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, started_at, updated_at) VALUES (?, ?, ?, ?, ?)`;
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()}`, now, now], function(err) {
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}`);
console.log(`SQLite: Created session ${sessionId} for user ${uid} (type: ${type})`);
resolve(sessionId);
}
});
@ -283,6 +345,7 @@ class SQLiteClient {
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 (?, ?, ?, ?, ?, ?)`;
@ -295,6 +358,7 @@ class SQLiteClient {
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 (?, ?, ?, ?, ?, ?, ?)`;
@ -307,6 +371,7 @@ class SQLiteClient {
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)
@ -327,6 +392,35 @@ class SQLiteClient {
});
}
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();
}
});
});
}
close() {
if (this.db) {
this.db.close((err) => {

View File

@ -107,6 +107,10 @@ function createFeatureWindows(header) {
}
function destroyFeatureWindows() {
if (settingsHideTimer) {
clearTimeout(settingsHideTimer);
settingsHideTimer = null;
}
featureWindows.forEach(name=>{
const win = windowPool.get(name);
if (win && !win.isDestroyed()) win.destroy();
@ -1328,8 +1332,12 @@ function setupIpcHandlers(openaiSessionRef) {
clearTimeout(settingsHideTimer);
}
settingsHideTimer = setTimeout(() => {
// window.setAlwaysOnTop(false);
// window.hide();
if (window && !window.isDestroyed()) {
window.setAlwaysOnTop(false);
window.hide();
}
settingsHideTimer = null;
}, 200);
} else {
@ -1809,23 +1817,23 @@ function setupIpcHandlers(openaiSessionRef) {
ipcMain.handle('firebase-logout', () => {
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');
// }
// });
// });
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()) {

View File

@ -270,7 +270,17 @@ export class CustomizeView extends LitElement {
super();
this.selectedProfile = localStorage.getItem('selectedProfile') || 'school';
this.selectedLanguage = localStorage.getItem('selectedLanguage') || 'en-US';
// Language format migration for legacy users
let lang = localStorage.getItem('selectedLanguage') || 'en';
if (lang.includes('-')) {
const newLang = lang.split('-')[0];
console.warn(`[Migration] Correcting language format from "${lang}" to "${newLang}".`);
localStorage.setItem('selectedLanguage', newLang);
lang = newLang;
}
this.selectedLanguage = lang;
this.selectedScreenshotInterval = localStorage.getItem('selectedScreenshotInterval') || '5000';
this.selectedImageQuality = localStorage.getItem('selectedImageQuality') || '0.8';
this.layoutMode = localStorage.getItem('layoutMode') || 'stacked';
@ -428,36 +438,31 @@ export class CustomizeView extends LitElement {
getLanguages() {
return [
{ value: 'en-US', name: 'English (US)' },
{ value: 'en-GB', name: 'English (UK)' },
{ value: 'en-AU', name: 'English (Australia)' },
{ value: 'en-IN', name: 'English (India)' },
{ value: 'de-DE', name: 'German (Germany)' },
{ value: 'es-US', name: 'Spanish (United States)' },
{ value: 'es-ES', name: 'Spanish (Spain)' },
{ value: 'fr-FR', name: 'French (France)' },
{ value: 'fr-CA', name: 'French (Canada)' },
{ value: 'hi-IN', name: 'Hindi (India)' },
{ value: 'pt-BR', name: 'Portuguese (Brazil)' },
{ value: 'ar-XA', name: 'Arabic (Generic)' },
{ value: 'id-ID', name: 'Indonesian (Indonesia)' },
{ value: 'it-IT', name: 'Italian (Italy)' },
{ value: 'ja-JP', name: 'Japanese (Japan)' },
{ value: 'tr-TR', name: 'Turkish (Turkey)' },
{ value: 'vi-VN', name: 'Vietnamese (Vietnam)' },
{ value: 'bn-IN', name: 'Bengali (India)' },
{ value: 'gu-IN', name: 'Gujarati (India)' },
{ value: 'kn-IN', name: 'Kannada (India)' },
{ value: 'ml-IN', name: 'Malayalam (India)' },
{ value: 'mr-IN', name: 'Marathi (India)' },
{ value: 'ta-IN', name: 'Tamil (India)' },
{ value: 'te-IN', name: 'Telugu (India)' },
{ value: 'nl-NL', name: 'Dutch (Netherlands)' },
{ value: 'ko-KR', name: 'Korean (South Korea)' },
{ value: 'cmn-CN', name: 'Mandarin Chinese (China)' },
{ value: 'pl-PL', name: 'Polish (Poland)' },
{ value: 'ru-RU', name: 'Russian (Russia)' },
{ value: 'th-TH', name: 'Thai (Thailand)' },
{ value: 'en', name: 'English' },
{ value: 'de', name: 'German' },
{ value: 'es', name: 'Spanish' },
{ value: 'fr', name: 'French' },
{ value: 'hi', name: 'Hindi' },
{ value: 'pt', name: 'Portuguese' },
{ value: 'ar', name: 'Arabic' },
{ value: 'id', name: 'Indonesian' },
{ value: 'it', name: 'Italian' },
{ value: 'ja', name: 'Japanese' },
{ value: 'tr', name: 'Turkish' },
{ value: 'vi', name: 'Vietnamese' },
{ value: 'bn', name: 'Bengali' },
{ value: 'gu', name: 'Gujarati' },
{ value: 'kn', name: 'Kannada' },
{ value: 'ml', name: 'Malayalam' },
{ value: 'mr', name: 'Marathi' },
{ value: 'ta', name: 'Tamil' },
{ value: 'te', name: 'Telugu' },
{ value: 'nl', name: 'Dutch' },
{ value: 'ko', name: 'Korean' },
{ value: 'zh', name: 'Chinese' },
{ value: 'pl', name: 'Polish' },
{ value: 'ru', name: 'Russian' },
{ value: 'th', name: 'Thai' },
];
}

View File

@ -57,6 +57,8 @@ let myInactivityTimer = null;
let theirInactivityTimer = null;
const INACTIVITY_TIMEOUT = 3000;
const SESSION_IDLE_TIMEOUT_SECONDS = 30 * 60; // 30 minutes
let previousAnalysisResult = null;
let analysisHistory = [];
@ -242,6 +244,16 @@ Keep all points concise and build upon previous analysis if provided.`,
console.log(`✅ Analysis response received: ${responseText}`);
const structuredData = parseResponseText(responseText, previousAnalysisResult);
if (currentSessionId) {
sqliteClient.saveSummary({
sessionId: currentSessionId,
tldr: structuredData.summary.join('\n'),
bullet_json: JSON.stringify(structuredData.topic.bullets),
action_json: JSON.stringify(structuredData.actions),
model: 'gpt-4.1'
}).catch(err => console.error('[DB] Failed to save summary:', err));
}
// 분석 결과 저장
previousAnalysisResult = structuredData;
analysisHistory.push({
@ -443,11 +455,52 @@ 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 {
const uid = dataService.currentUserId; // Get current user (local or firebase)
currentSessionId = await sqliteClient.createSession(uid);
console.log(`[DB] New session started in DB: ${currentSessionId}`);
currentSessionId = await getOrCreateActiveSession('listen');
console.log(`[DB] New listen session ensured: ${currentSessionId}`);
conversationHistory = [];
myCurrentUtterance = '';
@ -482,13 +535,9 @@ async function initializeNewSession() {
async function saveConversationTurn(speaker, transcription) {
if (!currentSessionId) {
console.log('No active session, initializing a new one first.');
const success = await initializeNewSession();
if (!success) {
console.error('Could not save turn because session initialization failed.');
console.error('[DB] Cannot save turn, no active session ID.');
return;
}
}
if (transcription.trim() === '') return;
try {
@ -511,14 +560,6 @@ async function saveConversationTurn(speaker, transcription) {
timestamp: Date.now(),
transcription: transcription.trim(),
};
sendToRenderer('update-live-transcription', { turn: conversationTurn });
if (conversationHistory.length % 5 === 0) {
console.log(`🔄 Auto-saving conversation session ${currentSessionId} (${conversationHistory.length} turns)`);
sendToRenderer('save-conversation-session', {
sessionId: currentSessionId,
conversationHistory: conversationHistory,
});
}
} catch (error) {
console.error('Failed to save transcript to DB:', error);
}
@ -548,7 +589,7 @@ async function initializeLiveSummarySession(language = 'en') {
return false;
}
initializeNewSession();
await initializeNewSession();
const provider = await getAiProvider();
const isGemini = provider === 'gemini';
@ -884,7 +925,7 @@ function setupLiveSummaryIpcHandlers() {
ipcMain.handle('initialize-openai', async (event, profile = 'interview', language = 'en') => {
console.log(`Received initialize-openai request with profile: ${profile}, language: ${language}`);
const success = await initializeLiveSummarySession();
const success = await initializeLiveSummarySession(language);
return success;
});
@ -973,10 +1014,36 @@ 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 };
}
});
}
module.exports = {
initializeLiveSummarySession,
sendToRenderer,
initializeNewSession,
saveConversationTurn,

View File

@ -1035,6 +1035,18 @@ async function sendMessage(userPrompt, options = {}) {
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 };
}
@ -1079,26 +1091,6 @@ async function initConversationStorage() {
}
}
async function saveConversationSession(sessionId, conversationHistory) {
try {
if (!apiClient) {
throw new Error('API client not available');
}
const response = await apiClient.client.post('/api/conversations', {
sessionId,
conversationHistory,
userId: apiClient.userId,
});
console.log('대화 세션 저장 완료:', sessionId);
return response.data;
} catch (error) {
console.error('대화 세션 저장 실패:', error);
throw error;
}
}
async function getConversationSession(sessionId) {
try {
if (!apiClient) {
@ -1127,27 +1119,6 @@ async function getAllConversationSessions() {
}
}
// Listen for conversation data from main process
ipcRenderer.on('save-conversation-turn', async (event, data) => {
try {
await saveConversationSession(data.sessionId, data.fullHistory);
console.log('Conversation session saved:', data.sessionId);
} catch (error) {
console.error('Error saving conversation session:', error);
}
});
// Listen for session save request from main process
ipcRenderer.on('save-conversation-session', async (event, data) => {
try {
console.log(`📥 Received conversation session save request: ${data.sessionId}`);
await saveConversationSession(data.sessionId, data.conversationHistory);
console.log(`✅ Conversation session saved successfully: ${data.sessionId}`);
} catch (error) {
console.error('❌ Error saving conversation session:', error);
}
});
// Initialize conversation storage when renderer loads
initConversationStorage().catch(console.error);