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);