From 05557c2e6819653733e43bbe643be9c521302c86 Mon Sep 17 00:00:00 2001 From: samtiz Date: Sat, 5 Jul 2025 22:00:36 +0900 Subject: [PATCH 1/2] fix session data local saving issue --- pickleglass_web/app/activity/details/page.tsx | 80 ++++-- pickleglass_web/app/activity/page.tsx | 4 +- pickleglass_web/backend_node/db.js | 74 +---- .../backend_node/routes/conversations.js | 2 +- pickleglass_web/utils/api.ts | 3 + pickleglass_web/utils/firestore.ts | 1 + src/app/PickleGlassApp.js | 12 +- src/common/config/schema.js | 76 +++++ src/common/services/databaseInitializer.js | 14 +- src/common/services/sqliteClient.js | 264 ++++++++++++------ src/features/customize/CustomizeView.js | 67 +++-- src/features/listen/liveSummaryService.js | 107 +++++-- src/features/listen/renderer.js | 53 +--- 13 files changed, 475 insertions(+), 282 deletions(-) create mode 100644 src/common/config/schema.js diff --git a/pickleglass_web/app/activity/details/page.tsx b/pickleglass_web/app/activity/details/page.tsx index f2069b5..78b022c 100644 --- a/pickleglass_web/app/activity/details/page.tsx +++ b/pickleglass_web/app/activity/details/page.tsx @@ -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 (
@@ -108,8 +103,8 @@ function SessionDetailsContent() {
-
-
+
+

{sessionDetails.session.title || `Conversation on ${new Date(sessionDetails.session.started_at * 1000).toLocaleDateString()}`} @@ -117,6 +112,9 @@ function SessionDetailsContent() {
{new Date(sessionDetails.session.started_at * 1000).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })} {new Date(sessionDetails.session.started_at * 1000).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })} + + {sessionDetails.session.session_type} +

diff --git a/pickleglass_web/app/activity/page.tsx b/pickleglass_web/app/activity/page.tsx index 663c17a..9d60578 100644 --- a/pickleglass_web/app/activity/page.tsx +++ b/pickleglass_web/app/activity/page.tsx @@ -101,8 +101,8 @@ export default function ActivityPage() { {deletingId === session.id ? 'Deleting...' : 'Delete'}
- - Conversation + + {session.session_type || 'ask'} ))} diff --git a/pickleglass_web/backend_node/db.js b/pickleglass_web/backend_node/db.js index 2fb78a0..d59636a 100644 --- a/pickleglass_web/backend_node/db.js +++ b/pickleglass_web/backend_node/db.js @@ -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], diff --git a/pickleglass_web/backend_node/routes/conversations.js b/pickleglass_web/backend_node/routes/conversations.js index cce7866..903b446 100644 --- a/pickleglass_web/backend_node/routes/conversations.js +++ b/pickleglass_web/backend_node/routes/conversations.js @@ -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) { diff --git a/pickleglass_web/utils/api.ts b/pickleglass_web/utils/api.ts index 78d0c41..aa80e87 100644 --- a/pickleglass_web/utils/api.ts +++ b/pickleglass_web/utils/api.ts @@ -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 }; diff --git a/pickleglass_web/utils/firestore.ts b/pickleglass_web/utils/firestore.ts index 7664b92..dd8d4db 100644 --- a/pickleglass_web/utils/firestore.ts +++ b/pickleglass_web/utils/firestore.ts @@ -24,6 +24,7 @@ export interface FirestoreUserProfile { export interface FirestoreSession { title: string; + session_type: string; startedAt: Timestamp; endedAt?: Timestamp; } diff --git a/src/app/PickleGlassApp.js b/src/app/PickleGlassApp.js index 8ce44d6..a2fd887 100644 --- a/src/app/PickleGlassApp.js +++ b/src/app/PickleGlassApp.js @@ -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; diff --git a/src/common/config/schema.js b/src/common/config/schema.js new file mode 100644 index 0000000..7e50323 --- /dev/null +++ b/src/common/config/schema.js @@ -0,0 +1,76 @@ +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' } + ] + }, + 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; \ No newline at end of file diff --git a/src/common/services/databaseInitializer.js b/src/common/services/databaseInitializer.js index 7484558..70cff75 100644 --- a/src/common/services/databaseInitializer.js +++ b/src/common/services/databaseInitializer.js @@ -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...'); diff --git a/src/common/services/sqliteClient.js b/src/common/services/sqliteClient.js index 8dda4fe..e1f4c86 100644 --- a/src/common/services/sqliteClient.js +++ b/src/common/services/sqliteClient.js @@ -1,5 +1,6 @@ const sqlite3 = require('sqlite3').verbose(); const path = require('path'); +const LATEST_SCHEMA = require('../config/schema'); class SQLiteClient { constructor() { @@ -33,88 +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; - - 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 - ); - - 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 - ); - - 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' - ); - - 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' - ); - - 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' - ); - - 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' - ); - `; - - this.db.exec(schema, (err) => { - if (err) { - console.error('Failed to create tables:', err); - return reject(err); - } - console.log('All tables are ready.'); - this.initDefaultData().then(resolve).catch(reject); + this.db.all("SELECT name FROM sqlite_master WHERE type='table'", (err, tables) => { + if (err) return reject(err); + resolve(tables.map(t => t.name)); }); }); } + 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})`; + + console.log(`[DB Sync] Creating table: ${tableName}`); + this.db.run(query, (err) => { + if (err) return reject(err); + resolve(); + }); + }); + } + + async updateTable(tableName, tableSchema) { + return new Promise((resolve, reject) => { + this.db.all(`PRAGMA table_info("${tableName}")`, async (err, existingColumns) => { + if (err) return reject(err); + + const existingColumnNames = existingColumns.map(c => c.name); + const columnsToAdd = tableSchema.columns.filter(col => !existingColumnNames.includes(col.name)); + + 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(); + }); + }); + } + + 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 + `; + + return new Promise((resolve, reject) => { + this.db.all(query, [], (err, rows) => { + if (err) { + console.error('[DB Cleanup] Error finding empty sessions:', err); + return reject(err); + } + + if (rows.length === 0) { + console.log('[DB Cleanup] No empty sessions found.'); + return resolve(); + } + + 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); @@ -244,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); } }); @@ -275,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 (?, ?, ?, ?, ?, ?)`; @@ -287,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 (?, ?, ?, ?, ?, ?, ?)`; @@ -299,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) @@ -319,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) => { diff --git a/src/features/customize/CustomizeView.js b/src/features/customize/CustomizeView.js index 831a685..fcebfc6 100644 --- a/src/features/customize/CustomizeView.js +++ b/src/features/customize/CustomizeView.js @@ -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' }, ]; } diff --git a/src/features/listen/liveSummaryService.js b/src/features/listen/liveSummaryService.js index 85cbbd1..b999793 100644 --- a/src/features/listen/liveSummaryService.js +++ b/src/features/listen/liveSummaryService.js @@ -43,6 +43,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 = []; @@ -244,6 +246,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({ @@ -445,11 +457,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 = ''; @@ -484,12 +537,8 @@ 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.'); - return; - } + console.error('[DB] Cannot save turn, no active session ID.'); + return; } if (transcription.trim() === '') return; @@ -513,14 +562,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); } @@ -551,7 +592,7 @@ async function initializeLiveSummarySession(language = 'en') { return false; } - initializeNewSession(); + await initializeNewSession(); try { const handleMyMessage = message => { @@ -859,7 +900,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; }); @@ -940,10 +981,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, diff --git a/src/features/listen/renderer.js b/src/features/listen/renderer.js index 7b17190..38571d5 100644 --- a/src/features/listen/renderer.js +++ b/src/features/listen/renderer.js @@ -1052,6 +1052,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 }; } @@ -1096,26 +1108,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) { @@ -1144,27 +1136,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); From 5d98e6152f6d31432f3bfce7ca68e692245fa0b9 Mon Sep 17 00:00:00 2001 From: entry-gi Date: Sat, 5 Jul 2025 22:16:36 +0900 Subject: [PATCH 2/2] Issues & improvements in readme --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 6e07f05..dcf7d05 100644 --- a/README.md +++ b/README.md @@ -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.