fix logout disabled bug
This commit is contained in:
		
						commit
						30d43eb5fe
					
				
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								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.
 | 
			
		||||
 | 
			
		||||
@ -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}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    ))}
 | 
			
		||||
                    {combinedConversation.length === 0 && <p>No notes recorded for this session.</p>}
 | 
			
		||||
                </Section>
 | 
			
		||||
                {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>
 | 
			
		||||
                            ))}
 | 
			
		||||
                        </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>
 | 
			
		||||
                    )}
 | 
			
		||||
                </Section>
 | 
			
		||||
                {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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
              ))}
 | 
			
		||||
 | 
			
		||||
@ -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],
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -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 };
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ export interface FirestoreUserProfile {
 | 
			
		||||
 | 
			
		||||
export interface FirestoreSession {
 | 
			
		||||
  title: string;
 | 
			
		||||
  session_type: string;
 | 
			
		||||
  startedAt: Timestamp;
 | 
			
		||||
  endedAt?: Timestamp;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										77
									
								
								src/common/config/schema.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/common/config/schema.js
									
									
									
									
									
										Normal 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; 
 | 
			
		||||
@ -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...');
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
            this.db.exec(schema, (err) => {
 | 
			
		||||
    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('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();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                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);
 | 
			
		||||
                    }
 | 
			
		||||
                    this.initDefaultData().then(resolve).catch(reject);
 | 
			
		||||
                    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) => {
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
                    // 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()) {
 | 
			
		||||
 | 
			
		||||
@ -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' },
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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,12 +535,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;
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user