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.
|
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
|
## 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.
|
**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 = [
|
const askMessages = sessionDetails.ai_messages || [];
|
||||||
...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');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#FDFCF9] text-gray-800">
|
<div className="min-h-screen bg-[#FDFCF9] text-gray-800">
|
||||||
@ -108,8 +103,8 @@ function SessionDetailsContent() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-8 rounded-xl">
|
<div className="bg-white p-8 rounded-xl shadow-md border border-gray-100">
|
||||||
<div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
||||||
{sessionDetails.session.title || `Conversation on ${new Date(sessionDetails.session.started_at * 1000).toLocaleDateString()}`}
|
{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">
|
<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).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>{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>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@ -130,29 +128,57 @@ function SessionDetailsContent() {
|
|||||||
|
|
||||||
{sessionDetails.summary && (
|
{sessionDetails.summary && (
|
||||||
<Section title="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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Section title="Notes">
|
{sessionDetails.transcripts && sessionDetails.transcripts.length > 0 && (
|
||||||
{combinedConversation.map((item) => (
|
<Section title="Listen: Transcript">
|
||||||
<p key={item.id}>
|
<div className="space-y-3">
|
||||||
<span className="font-semibold">{(item.type === 'transcript' && item.speaker === 'Me') || (item.type === 'ai_message' && item.role === 'user') ? 'You: ' : 'AI: '}</span>
|
{sessionDetails.transcripts.map((item) => (
|
||||||
{item.type === 'transcript' ? item.text : item.content}
|
<p key={item.id} className="text-gray-700">
|
||||||
</p>
|
<span className="font-semibold capitalize">{item.speaker}: </span>
|
||||||
))}
|
{item.text}
|
||||||
{combinedConversation.length === 0 && <p>No notes recorded for this session.</p>}
|
</p>
|
||||||
</Section>
|
))}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
|
||||||
<Section title="Audio transcript content">
|
{askMessages.length > 0 && (
|
||||||
{audioTranscripts.length > 0 ? (
|
<Section title="Ask: Q&A">
|
||||||
<ul className="list-disc list-inside space-y-1">
|
<div className="space-y-4">
|
||||||
{audioTranscripts.map(t => <li key={t.id}>{t.text}</li>)}
|
{askMessages.map((item) => (
|
||||||
</ul>
|
<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>No audio transcript available for this session.</p>
|
<p className="text-gray-800 whitespace-pre-wrap">{item.content}</p>
|
||||||
)}
|
</div>
|
||||||
</Section>
|
))}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -101,8 +101,8 @@ export default function ActivityPage() {
|
|||||||
{deletingId === session.id ? 'Deleting...' : 'Delete'}
|
{deletingId === session.id ? 'Deleting...' : 'Delete'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<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'}`}>
|
||||||
Conversation
|
{session.session_type || 'ask'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -7,78 +7,8 @@ const db = new Database(dbPath);
|
|||||||
|
|
||||||
db.pragma('journal_mode = WAL');
|
db.pragma('journal_mode = WAL');
|
||||||
|
|
||||||
db.exec(`
|
// The schema is now managed by the main Electron process on startup.
|
||||||
-- users
|
// This file can assume the schema is correct and up-to-date.
|
||||||
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'
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
const defaultPresets = [
|
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],
|
['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) => {
|
router.get('/', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const sessions = db.prepare(
|
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);
|
).all(req.uid);
|
||||||
res.json(sessions);
|
res.json(sessions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -24,6 +24,7 @@ export interface Session {
|
|||||||
id: string;
|
id: string;
|
||||||
uid: string;
|
uid: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
session_type: string;
|
||||||
started_at: number;
|
started_at: number;
|
||||||
ended_at?: number;
|
ended_at?: number;
|
||||||
sync_state: 'clean' | 'dirty';
|
sync_state: 'clean' | 'dirty';
|
||||||
@ -102,6 +103,7 @@ const convertFirestoreSession = (session: { id: string } & FirestoreSession, uid
|
|||||||
id: session.id,
|
id: session.id,
|
||||||
uid,
|
uid,
|
||||||
title: session.title,
|
title: session.title,
|
||||||
|
session_type: session.session_type,
|
||||||
started_at: timestampToUnix(session.startedAt),
|
started_at: timestampToUnix(session.startedAt),
|
||||||
ended_at: session.endedAt ? timestampToUnix(session.endedAt) : undefined,
|
ended_at: session.endedAt ? timestampToUnix(session.endedAt) : undefined,
|
||||||
sync_state: 'clean',
|
sync_state: 'clean',
|
||||||
@ -387,6 +389,7 @@ export const createSession = async (title?: string): Promise<{ id: string }> =>
|
|||||||
const uid = firebaseAuth.currentUser!.uid;
|
const uid = firebaseAuth.currentUser!.uid;
|
||||||
const sessionId = await FirestoreSessionService.createSession(uid, {
|
const sessionId = await FirestoreSessionService.createSession(uid, {
|
||||||
title: title || 'New Session',
|
title: title || 'New Session',
|
||||||
|
session_type: 'ask',
|
||||||
endedAt: undefined
|
endedAt: undefined
|
||||||
});
|
});
|
||||||
return { id: sessionId };
|
return { id: sessionId };
|
||||||
|
@ -24,6 +24,7 @@ export interface FirestoreUserProfile {
|
|||||||
|
|
||||||
export interface FirestoreSession {
|
export interface FirestoreSession {
|
||||||
title: string;
|
title: string;
|
||||||
|
session_type: string;
|
||||||
startedAt: Timestamp;
|
startedAt: Timestamp;
|
||||||
endedAt?: Timestamp;
|
endedAt?: Timestamp;
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
font-weight: 500; /* Medium */
|
font-weight: 500; /* Medium */
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
flex-shrink: 0; /* 제목이 줄어들지 않도록 고정 */
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-content {
|
.form-content {
|
||||||
@ -117,14 +117,14 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: auto; /* 이 속성이 제목과 폼 사이의 공간을 만듭니다. */
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
color: rgba(239, 68, 68, 0.9);
|
color: rgba(239, 68, 68, 0.9);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
height: 14px; /* Reserve space to prevent layout shift */
|
height: 14px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
@ -113,6 +113,7 @@ class HeaderTransitionManager {
|
|||||||
ipcRenderer.on('request-firebase-logout', async () => {
|
ipcRenderer.on('request-firebase-logout', async () => {
|
||||||
console.log('[HeaderController] Received request to sign out.');
|
console.log('[HeaderController] Received request to sign out.');
|
||||||
try {
|
try {
|
||||||
|
this.hasApiKey = false;
|
||||||
await signOut(auth);
|
await signOut(auth);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[HeaderController] Sign out failed', error);
|
console.error('[HeaderController] Sign out failed', error);
|
||||||
|
@ -51,7 +51,17 @@ export class PickleGlassApp extends LitElement {
|
|||||||
this.currentView = urlParams.get('view') || 'listen';
|
this.currentView = urlParams.get('view') || 'listen';
|
||||||
this.currentResponseIndex = -1;
|
this.currentResponseIndex = -1;
|
||||||
this.selectedProfile = localStorage.getItem('selectedProfile') || 'interview';
|
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.selectedScreenshotInterval = localStorage.getItem('selectedScreenshotInterval') || '5';
|
||||||
this.selectedImageQuality = localStorage.getItem('selectedImageQuality') || 'medium';
|
this.selectedImageQuality = localStorage.getItem('selectedImageQuality') || 'medium';
|
||||||
this._isClickThrough = false;
|
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 경로를 인자로 전달
|
await sqliteClient.connect(this.dbPath); // DB 경로를 인자로 전달
|
||||||
|
|
||||||
// 연결 후 테이블 및 기본 데이터 초기화
|
// This single call will now synchronize the schema and then init default data.
|
||||||
await sqliteClient.initTables();
|
await sqliteClient.initTables();
|
||||||
const user = await sqliteClient.getUser(sqliteClient.defaultUserId);
|
|
||||||
if (!user) {
|
// Clean up any orphaned sessions from previous versions
|
||||||
await sqliteClient.initDefaultData();
|
await sqliteClient.cleanupEmptySessions();
|
||||||
console.log('[DB] Default data initialized.');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
console.log('[DB] Database initialized successfully');
|
console.log('[DB] Database initialized successfully');
|
||||||
@ -141,6 +139,10 @@ class DatabaseInitializer {
|
|||||||
try {
|
try {
|
||||||
console.log('[DatabaseInitializer] Validating database integrity...');
|
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);
|
const defaultUser = await sqliteClient.getUser(sqliteClient.defaultUserId);
|
||||||
if (!defaultUser) {
|
if (!defaultUser) {
|
||||||
console.log('[DatabaseInitializer] Default user not found - creating...');
|
console.log('[DatabaseInitializer] Default user not found - creating...');
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const sqlite3 = require('sqlite3').verbose();
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const LATEST_SCHEMA = require('../config/schema');
|
||||||
|
|
||||||
class SQLiteClient {
|
class SQLiteClient {
|
||||||
constructor() {
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const schema = `
|
this.db.all("SELECT name FROM sqlite_master WHERE type='table'", (err, tables) => {
|
||||||
PRAGMA journal_mode = WAL;
|
if (err) return reject(err);
|
||||||
|
resolve(tables.map(t => t.name));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
async createTable(tableName, tableSchema) {
|
||||||
uid TEXT PRIMARY KEY,
|
return new Promise((resolve, reject) => {
|
||||||
display_name TEXT NOT NULL,
|
const columnDefs = tableSchema.columns.map(col => `"${col.name}" ${col.type}`).join(', ');
|
||||||
email TEXT NOT NULL,
|
const query = `CREATE TABLE IF NOT EXISTS "${tableName}" (${columnDefs})`;
|
||||||
created_at INTEGER,
|
|
||||||
api_key TEXT,
|
|
||||||
provider TEXT DEFAULT 'openai'
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
console.log(`[DB Sync] Creating table: ${tableName}`);
|
||||||
id TEXT PRIMARY KEY,
|
this.db.run(query, (err) => {
|
||||||
uid TEXT NOT NULL,
|
if (err) return reject(err);
|
||||||
title TEXT,
|
resolve();
|
||||||
started_at INTEGER,
|
});
|
||||||
ended_at INTEGER,
|
});
|
||||||
sync_state TEXT DEFAULT 'clean',
|
}
|
||||||
updated_at INTEGER
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS transcripts (
|
async updateTable(tableName, tableSchema) {
|
||||||
id TEXT PRIMARY KEY,
|
return new Promise((resolve, reject) => {
|
||||||
session_id TEXT NOT NULL,
|
this.db.all(`PRAGMA table_info("${tableName}")`, async (err, existingColumns) => {
|
||||||
start_at INTEGER,
|
if (err) return reject(err);
|
||||||
end_at INTEGER,
|
|
||||||
speaker TEXT,
|
|
||||||
text TEXT,
|
|
||||||
lang TEXT,
|
|
||||||
created_at INTEGER,
|
|
||||||
sync_state TEXT DEFAULT 'clean'
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ai_messages (
|
const existingColumnNames = existingColumns.map(c => c.name);
|
||||||
id TEXT PRIMARY KEY,
|
const columnsToAdd = tableSchema.columns.filter(col => !existingColumnNames.includes(col.name));
|
||||||
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 (
|
if (columnsToAdd.length > 0) {
|
||||||
session_id TEXT PRIMARY KEY,
|
console.log(`[DB Sync] Updating table: ${tableName}. Adding columns: ${columnsToAdd.map(c=>c.name).join(', ')}`);
|
||||||
generated_at INTEGER,
|
for (const column of columnsToAdd) {
|
||||||
model TEXT,
|
const addColumnQuery = `ALTER TABLE "${tableName}" ADD COLUMN "${column.name}" ${column.type}`;
|
||||||
text TEXT,
|
try {
|
||||||
tldr TEXT,
|
await this.runQuery(addColumnQuery);
|
||||||
bullet_json TEXT,
|
} catch (alterErr) {
|
||||||
action_json TEXT,
|
return reject(alterErr);
|
||||||
tokens_used INTEGER,
|
}
|
||||||
updated_at INTEGER,
|
}
|
||||||
sync_state TEXT DEFAULT 'clean'
|
}
|
||||||
);
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS prompt_presets (
|
async runQuery(query, params = []) {
|
||||||
id TEXT PRIMARY KEY,
|
return new Promise((resolve, reject) => {
|
||||||
uid TEXT NOT NULL,
|
this.db.run(query, params, function(err) {
|
||||||
title TEXT NOT NULL,
|
if (err) return reject(err);
|
||||||
prompt TEXT NOT NULL,
|
resolve(this);
|
||||||
is_default INTEGER NOT NULL,
|
});
|
||||||
created_at INTEGER,
|
});
|
||||||
sync_state TEXT DEFAULT 'clean'
|
}
|
||||||
);
|
|
||||||
`;
|
|
||||||
|
|
||||||
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) {
|
if (err) {
|
||||||
console.error('Failed to create tables:', err);
|
console.error('[DB Cleanup] Error finding empty sessions:', err);
|
||||||
return reject(err);
|
return reject(err);
|
||||||
}
|
}
|
||||||
console.log('All tables are ready.');
|
|
||||||
|
if (rows.length === 0) {
|
||||||
// Add provider column to existing databases
|
console.log('[DB Cleanup] No empty sessions found.');
|
||||||
this.db.run("ALTER TABLE users ADD COLUMN provider TEXT DEFAULT 'openai'", (alterErr) => {
|
return resolve();
|
||||||
if (alterErr && !alterErr.message.includes('duplicate column')) {
|
}
|
||||||
console.log('Note: Could not add provider column (may already exist)');
|
|
||||||
|
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() {
|
async initDefaultData() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const sessionId = require('crypto').randomUUID();
|
const sessionId = require('crypto').randomUUID();
|
||||||
const now = Math.floor(Date.now() / 1000);
|
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) {
|
if (err) {
|
||||||
console.error('SQLite: Failed to create session:', err);
|
console.error('SQLite: Failed to create session:', err);
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
console.log(`SQLite: Created session ${sessionId} for user ${uid}`);
|
console.log(`SQLite: Created session ${sessionId} for user ${uid} (type: ${type})`);
|
||||||
resolve(sessionId);
|
resolve(sessionId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -283,6 +345,7 @@ class SQLiteClient {
|
|||||||
|
|
||||||
async addTranscript({ sessionId, speaker, text }) {
|
async addTranscript({ sessionId, speaker, text }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
this.touchSession(sessionId).catch(err => console.error("Failed to touch session", err));
|
||||||
const transcriptId = require('crypto').randomUUID();
|
const transcriptId = require('crypto').randomUUID();
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const query = `INSERT INTO transcripts (id, session_id, start_at, speaker, text, created_at) VALUES (?, ?, ?, ?, ?, ?)`;
|
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' }) {
|
async addAiMessage({ sessionId, role, content, model = 'gpt-4.1' }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
this.touchSession(sessionId).catch(err => console.error("Failed to touch session", err));
|
||||||
const messageId = require('crypto').randomUUID();
|
const messageId = require('crypto').randomUUID();
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const query = `INSERT INTO ai_messages (id, session_id, sent_at, role, content, model, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`;
|
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' }) {
|
async saveSummary({ sessionId, tldr, text, bullet_json, action_json, model = 'gpt-4.1' }) {
|
||||||
return new Promise((resolve, reject) => {
|
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 now = Math.floor(Date.now() / 1000);
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO summaries (session_id, generated_at, model, text, tldr, bullet_json, action_json, updated_at)
|
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() {
|
close() {
|
||||||
if (this.db) {
|
if (this.db) {
|
||||||
this.db.close((err) => {
|
this.db.close((err) => {
|
||||||
|
@ -107,6 +107,10 @@ function createFeatureWindows(header) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function destroyFeatureWindows() {
|
function destroyFeatureWindows() {
|
||||||
|
if (settingsHideTimer) {
|
||||||
|
clearTimeout(settingsHideTimer);
|
||||||
|
settingsHideTimer = null;
|
||||||
|
}
|
||||||
featureWindows.forEach(name=>{
|
featureWindows.forEach(name=>{
|
||||||
const win = windowPool.get(name);
|
const win = windowPool.get(name);
|
||||||
if (win && !win.isDestroyed()) win.destroy();
|
if (win && !win.isDestroyed()) win.destroy();
|
||||||
@ -1328,8 +1332,12 @@ function setupIpcHandlers(openaiSessionRef) {
|
|||||||
clearTimeout(settingsHideTimer);
|
clearTimeout(settingsHideTimer);
|
||||||
}
|
}
|
||||||
settingsHideTimer = setTimeout(() => {
|
settingsHideTimer = setTimeout(() => {
|
||||||
window.setAlwaysOnTop(false);
|
// window.setAlwaysOnTop(false);
|
||||||
window.hide();
|
// window.hide();
|
||||||
|
if (window && !window.isDestroyed()) {
|
||||||
|
window.setAlwaysOnTop(false);
|
||||||
|
window.hide();
|
||||||
|
}
|
||||||
settingsHideTimer = null;
|
settingsHideTimer = null;
|
||||||
}, 200);
|
}, 200);
|
||||||
} else {
|
} else {
|
||||||
@ -1809,23 +1817,23 @@ function setupIpcHandlers(openaiSessionRef) {
|
|||||||
|
|
||||||
ipcMain.handle('firebase-logout', () => {
|
ipcMain.handle('firebase-logout', () => {
|
||||||
console.log('[WindowManager] Received request to log out.');
|
console.log('[WindowManager] Received request to log out.');
|
||||||
// setApiKey(null)
|
setApiKey(null)
|
||||||
// .then(() => {
|
.then(() => {
|
||||||
// console.log('[WindowManager] API key cleared successfully after logout');
|
console.log('[WindowManager] API key cleared successfully after logout');
|
||||||
// windowPool.forEach(win => {
|
windowPool.forEach(win => {
|
||||||
// if (win && !win.isDestroyed()) {
|
if (win && !win.isDestroyed()) {
|
||||||
// win.webContents.send('api-key-removed');
|
win.webContents.send('api-key-removed');
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
// })
|
})
|
||||||
// .catch(err => {
|
.catch(err => {
|
||||||
// console.error('[WindowManager] setApiKey error:', err);
|
console.error('[WindowManager] setApiKey error:', err);
|
||||||
// windowPool.forEach(win => {
|
windowPool.forEach(win => {
|
||||||
// if (win && !win.isDestroyed()) {
|
if (win && !win.isDestroyed()) {
|
||||||
// win.webContents.send('api-key-removed');
|
win.webContents.send('api-key-removed');
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
// });
|
});
|
||||||
|
|
||||||
const header = windowPool.get('header');
|
const header = windowPool.get('header');
|
||||||
if (header && !header.isDestroyed()) {
|
if (header && !header.isDestroyed()) {
|
||||||
|
@ -270,7 +270,17 @@ export class CustomizeView extends LitElement {
|
|||||||
super();
|
super();
|
||||||
|
|
||||||
this.selectedProfile = localStorage.getItem('selectedProfile') || 'school';
|
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.selectedScreenshotInterval = localStorage.getItem('selectedScreenshotInterval') || '5000';
|
||||||
this.selectedImageQuality = localStorage.getItem('selectedImageQuality') || '0.8';
|
this.selectedImageQuality = localStorage.getItem('selectedImageQuality') || '0.8';
|
||||||
this.layoutMode = localStorage.getItem('layoutMode') || 'stacked';
|
this.layoutMode = localStorage.getItem('layoutMode') || 'stacked';
|
||||||
@ -428,36 +438,31 @@ export class CustomizeView extends LitElement {
|
|||||||
|
|
||||||
getLanguages() {
|
getLanguages() {
|
||||||
return [
|
return [
|
||||||
{ value: 'en-US', name: 'English (US)' },
|
{ value: 'en', name: 'English' },
|
||||||
{ value: 'en-GB', name: 'English (UK)' },
|
{ value: 'de', name: 'German' },
|
||||||
{ value: 'en-AU', name: 'English (Australia)' },
|
{ value: 'es', name: 'Spanish' },
|
||||||
{ value: 'en-IN', name: 'English (India)' },
|
{ value: 'fr', name: 'French' },
|
||||||
{ value: 'de-DE', name: 'German (Germany)' },
|
{ value: 'hi', name: 'Hindi' },
|
||||||
{ value: 'es-US', name: 'Spanish (United States)' },
|
{ value: 'pt', name: 'Portuguese' },
|
||||||
{ value: 'es-ES', name: 'Spanish (Spain)' },
|
{ value: 'ar', name: 'Arabic' },
|
||||||
{ value: 'fr-FR', name: 'French (France)' },
|
{ value: 'id', name: 'Indonesian' },
|
||||||
{ value: 'fr-CA', name: 'French (Canada)' },
|
{ value: 'it', name: 'Italian' },
|
||||||
{ value: 'hi-IN', name: 'Hindi (India)' },
|
{ value: 'ja', name: 'Japanese' },
|
||||||
{ value: 'pt-BR', name: 'Portuguese (Brazil)' },
|
{ value: 'tr', name: 'Turkish' },
|
||||||
{ value: 'ar-XA', name: 'Arabic (Generic)' },
|
{ value: 'vi', name: 'Vietnamese' },
|
||||||
{ value: 'id-ID', name: 'Indonesian (Indonesia)' },
|
{ value: 'bn', name: 'Bengali' },
|
||||||
{ value: 'it-IT', name: 'Italian (Italy)' },
|
{ value: 'gu', name: 'Gujarati' },
|
||||||
{ value: 'ja-JP', name: 'Japanese (Japan)' },
|
{ value: 'kn', name: 'Kannada' },
|
||||||
{ value: 'tr-TR', name: 'Turkish (Turkey)' },
|
{ value: 'ml', name: 'Malayalam' },
|
||||||
{ value: 'vi-VN', name: 'Vietnamese (Vietnam)' },
|
{ value: 'mr', name: 'Marathi' },
|
||||||
{ value: 'bn-IN', name: 'Bengali (India)' },
|
{ value: 'ta', name: 'Tamil' },
|
||||||
{ value: 'gu-IN', name: 'Gujarati (India)' },
|
{ value: 'te', name: 'Telugu' },
|
||||||
{ value: 'kn-IN', name: 'Kannada (India)' },
|
{ value: 'nl', name: 'Dutch' },
|
||||||
{ value: 'ml-IN', name: 'Malayalam (India)' },
|
{ value: 'ko', name: 'Korean' },
|
||||||
{ value: 'mr-IN', name: 'Marathi (India)' },
|
{ value: 'zh', name: 'Chinese' },
|
||||||
{ value: 'ta-IN', name: 'Tamil (India)' },
|
{ value: 'pl', name: 'Polish' },
|
||||||
{ value: 'te-IN', name: 'Telugu (India)' },
|
{ value: 'ru', name: 'Russian' },
|
||||||
{ value: 'nl-NL', name: 'Dutch (Netherlands)' },
|
{ value: 'th', name: 'Thai' },
|
||||||
{ 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)' },
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +57,8 @@ let myInactivityTimer = null;
|
|||||||
let theirInactivityTimer = null;
|
let theirInactivityTimer = null;
|
||||||
const INACTIVITY_TIMEOUT = 3000;
|
const INACTIVITY_TIMEOUT = 3000;
|
||||||
|
|
||||||
|
const SESSION_IDLE_TIMEOUT_SECONDS = 30 * 60; // 30 minutes
|
||||||
|
|
||||||
let previousAnalysisResult = null;
|
let previousAnalysisResult = null;
|
||||||
let analysisHistory = [];
|
let analysisHistory = [];
|
||||||
|
|
||||||
@ -242,6 +244,16 @@ Keep all points concise and build upon previous analysis if provided.`,
|
|||||||
console.log(`✅ Analysis response received: ${responseText}`);
|
console.log(`✅ Analysis response received: ${responseText}`);
|
||||||
const structuredData = parseResponseText(responseText, previousAnalysisResult);
|
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;
|
previousAnalysisResult = structuredData;
|
||||||
analysisHistory.push({
|
analysisHistory.push({
|
||||||
@ -443,11 +455,52 @@ function getCurrentSessionData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Conversation management functions
|
// 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() {
|
async function initializeNewSession() {
|
||||||
try {
|
try {
|
||||||
const uid = dataService.currentUserId; // Get current user (local or firebase)
|
currentSessionId = await getOrCreateActiveSession('listen');
|
||||||
currentSessionId = await sqliteClient.createSession(uid);
|
console.log(`[DB] New listen session ensured: ${currentSessionId}`);
|
||||||
console.log(`[DB] New session started in DB: ${currentSessionId}`);
|
|
||||||
|
|
||||||
conversationHistory = [];
|
conversationHistory = [];
|
||||||
myCurrentUtterance = '';
|
myCurrentUtterance = '';
|
||||||
@ -482,12 +535,8 @@ async function initializeNewSession() {
|
|||||||
|
|
||||||
async function saveConversationTurn(speaker, transcription) {
|
async function saveConversationTurn(speaker, transcription) {
|
||||||
if (!currentSessionId) {
|
if (!currentSessionId) {
|
||||||
console.log('No active session, initializing a new one first.');
|
console.error('[DB] Cannot save turn, no active session ID.');
|
||||||
const success = await initializeNewSession();
|
return;
|
||||||
if (!success) {
|
|
||||||
console.error('Could not save turn because session initialization failed.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (transcription.trim() === '') return;
|
if (transcription.trim() === '') return;
|
||||||
|
|
||||||
@ -511,14 +560,6 @@ async function saveConversationTurn(speaker, transcription) {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
transcription: transcription.trim(),
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to save transcript to DB:', error);
|
console.error('Failed to save transcript to DB:', error);
|
||||||
}
|
}
|
||||||
@ -548,7 +589,7 @@ async function initializeLiveSummarySession(language = 'en') {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeNewSession();
|
await initializeNewSession();
|
||||||
|
|
||||||
const provider = await getAiProvider();
|
const provider = await getAiProvider();
|
||||||
const isGemini = provider === 'gemini';
|
const isGemini = provider === 'gemini';
|
||||||
@ -884,7 +925,7 @@ function setupLiveSummaryIpcHandlers() {
|
|||||||
|
|
||||||
ipcMain.handle('initialize-openai', async (event, profile = 'interview', language = 'en') => {
|
ipcMain.handle('initialize-openai', async (event, profile = 'interview', language = 'en') => {
|
||||||
console.log(`Received initialize-openai request with profile: ${profile}, language: ${language}`);
|
console.log(`Received initialize-openai request with profile: ${profile}, language: ${language}`);
|
||||||
const success = await initializeLiveSummarySession();
|
const success = await initializeLiveSummarySession(language);
|
||||||
return success;
|
return success;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -973,10 +1014,36 @@ function setupLiveSummaryIpcHandlers() {
|
|||||||
return { success: false, error: error.message };
|
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 = {
|
module.exports = {
|
||||||
initializeLiveSummarySession,
|
|
||||||
sendToRenderer,
|
sendToRenderer,
|
||||||
initializeNewSession,
|
initializeNewSession,
|
||||||
saveConversationTurn,
|
saveConversationTurn,
|
||||||
|
@ -1035,6 +1035,18 @@ async function sendMessage(userPrompt, options = {}) {
|
|||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require('electron');
|
||||||
ipcRenderer.send('ask-response-stream-end');
|
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 };
|
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) {
|
async function getConversationSession(sessionId) {
|
||||||
try {
|
try {
|
||||||
if (!apiClient) {
|
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
|
// Initialize conversation storage when renderer loads
|
||||||
initConversationStorage().catch(console.error);
|
initConversationStorage().catch(console.error);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user