Merge branch 'main' into pr-90

This commit is contained in:
sanio 2025-07-09 18:54:06 +09:00
commit a4e61c73e5
10 changed files with 312 additions and 51 deletions

View File

@ -6,7 +6,8 @@ const LATEST_SCHEMA = {
{ name: 'email', type: 'TEXT NOT NULL' },
{ name: 'created_at', type: 'INTEGER' },
{ name: 'api_key', type: 'TEXT' },
{ name: 'provider', type: 'TEXT DEFAULT \'openai\'' }
{ name: 'provider', type: 'TEXT DEFAULT \'openai\'' },
{ name: 'auto_update_enabled', type: 'INTEGER DEFAULT 1' }
]
},
sessions: {

View File

@ -37,56 +37,64 @@ class AuthService {
this.currentUserMode = 'local'; // 'local' or 'firebase'
this.currentUser = null;
this.isInitialized = false;
this.initializationPromise = null;
}
initialize() {
if (this.isInitialized) return;
if (this.isInitialized) return this.initializationPromise;
const auth = getFirebaseAuth();
onAuthStateChanged(auth, async (user) => {
const previousUser = this.currentUser;
this.initializationPromise = new Promise((resolve) => {
const auth = getFirebaseAuth();
onAuthStateChanged(auth, async (user) => {
const previousUser = this.currentUser;
if (user) {
// User signed IN
console.log(`[AuthService] Firebase user signed in:`, user.uid);
this.currentUser = user;
this.currentUserId = user.uid;
this.currentUserMode = 'firebase';
if (user) {
// User signed IN
console.log(`[AuthService] Firebase user signed in:`, user.uid);
this.currentUser = user;
this.currentUserId = user.uid;
this.currentUserMode = 'firebase';
// Start background task to fetch and save virtual key
(async () => {
try {
const idToken = await user.getIdToken(true);
const virtualKey = await getVirtualKeyByEmail(user.email, idToken);
// Start background task to fetch and save virtual key
(async () => {
try {
const idToken = await user.getIdToken(true);
const virtualKey = await getVirtualKeyByEmail(user.email, idToken);
if (global.modelStateService) {
global.modelStateService.setFirebaseVirtualKey(virtualKey);
if (global.modelStateService) {
global.modelStateService.setFirebaseVirtualKey(virtualKey);
}
console.log(`[AuthService] BG: Virtual key for ${user.email} has been processed.`);
} catch (error) {
console.error('[AuthService] BG: Failed to fetch or save virtual key:', error);
}
console.log(`[AuthService] BG: Virtual key for ${user.email} has been processed.`);
})();
} catch (error) {
console.error('[AuthService] BG: Failed to fetch or save virtual key:', error);
}
})();
} else {
// User signed OUT
console.log(`[AuthService] No Firebase user.`);
if (previousUser) {
console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`);
if (global.modelStateService) {
global.modelStateService.setFirebaseVirtualKey(null);
} else {
// User signed OUT
console.log(`[AuthService] No Firebase user.`);
if (previousUser) {
console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`);
if (global.modelStateService) {
global.modelStateService.setFirebaseVirtualKey(null);
}
}
this.currentUser = null;
this.currentUserId = 'default_user';
this.currentUserMode = 'local';
}
this.currentUser = null;
this.currentUserId = 'default_user';
this.currentUserMode = 'local';
}
this.broadcastUserState();
this.broadcastUserState();
if (!this.isInitialized) {
this.isInitialized = true;
console.log('[AuthService] Initialized and resolved initialization promise.');
resolve();
}
});
});
this.isInitialized = true;
console.log('[AuthService] Initialized and attached to Firebase Auth state.');
return this.initializationPromise;
}
async signInWithCustomToken(token) {

View File

@ -122,6 +122,11 @@ class SmoothMovementManager {
console.log(`[MovementManager] Moving ${direction} from (${targetX}, ${targetY})`);
const windowSize = {
width: currentBounds.width,
height: currentBounds.height
};
switch (direction) {
case 'left': targetX -= this.stepSize; break;
case 'right': targetX += this.stepSize; break;
@ -165,7 +170,7 @@ class SmoothMovementManager {
this.animateToPosition(header, clampedX, clampedY);
}
animateToPosition(header, targetX, targetY) {
animateToPosition(header, targetX, targetY, windowSize) {
if (!this._isWindowValid(header)) return;
this.isAnimating = true;
@ -193,7 +198,13 @@ class SmoothMovementManager {
}
if (!this._isWindowValid(header)) return;
header.setPosition(Math.round(currentX), Math.round(currentY));
const { width, height } = windowSize || header.getBounds();
header.setBounds({
x: Math.round(currentX),
y: Math.round(currentY),
width,
height
});
if (progress < 1) {
this.animationFrameId = setTimeout(animate, 8);
@ -219,20 +230,40 @@ class SmoothMovementManager {
const display = this.getCurrentDisplay(header);
const { width, height } = display.workAreaSize;
const { x: workAreaX, y: workAreaY } = display.workArea;
const headerBounds = header.getBounds();
const currentBounds = header.getBounds();
const windowSize = {
width: currentBounds.width,
height: currentBounds.height
};
let targetX = currentBounds.x;
let targetY = currentBounds.y;
switch (direction) {
case 'left': targetX = workAreaX; break;
case 'right': targetX = workAreaX + width - headerBounds.width; break;
case 'up': targetY = workAreaY; break;
case 'down': targetY = workAreaY + height - headerBounds.height; break;
case 'left':
targetX = workAreaX;
break;
case 'right':
targetX = workAreaX + width - windowSize.width;
break;
case 'up':
targetY = workAreaY;
break;
case 'down':
targetY = workAreaY + height - windowSize.height;
break;
}
this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
this.animateToPosition(header, targetX, targetY);
header.setBounds({
x: Math.round(targetX),
y: Math.round(targetY),
width: windowSize.width,
height: windowSize.height
});
this.headerPosition = { x: targetX, y: targetY };
this.updateLayout();
}
destroy() {

View File

@ -98,6 +98,12 @@ export class AskView extends LitElement {
user-select: none;
}
/* Allow text selection in assistant responses */
.response-container, .response-container * {
user-select: text !important;
cursor: text !important;
}
.response-container pre {
background: rgba(0, 0, 0, 0.4) !important;
border-radius: 8px !important;

View File

@ -84,6 +84,87 @@ export class AssistantView extends LitElement {
user-select: none;
}
/* Allow text selection in insights responses */
.insights-container, .insights-container *, .markdown-content {
user-select: text !important;
cursor: text !important;
}
/* highlight.js 스타일 추가 */
.insights-container pre {
background: rgba(0, 0, 0, 0.4) !important;
border-radius: 8px !important;
padding: 12px !important;
margin: 8px 0 !important;
overflow-x: auto !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
white-space: pre !important;
word-wrap: normal !important;
word-break: normal !important;
}
.insights-container code {
font-family: 'Monaco', 'Menlo', 'Consolas', monospace !important;
font-size: 11px !important;
background: transparent !important;
white-space: pre !important;
word-wrap: normal !important;
word-break: normal !important;
}
.insights-container pre code {
white-space: pre !important;
word-wrap: normal !important;
word-break: normal !important;
display: block !important;
}
.insights-container p code {
background: rgba(255, 255, 255, 0.1) !important;
padding: 2px 4px !important;
border-radius: 3px !important;
color: #ffd700 !important;
}
.hljs-keyword {
color: #ff79c6 !important;
}
.hljs-string {
color: #f1fa8c !important;
}
.hljs-comment {
color: #6272a4 !important;
}
.hljs-number {
color: #bd93f9 !important;
}
.hljs-function {
color: #50fa7b !important;
}
.hljs-title {
color: #50fa7b !important;
}
.hljs-variable {
color: #8be9fd !important;
}
.hljs-built_in {
color: #ffb86c !important;
}
.hljs-attr {
color: #50fa7b !important;
}
.hljs-tag {
color: #ff79c6 !important;
}
.assistant-container {
display: flex;
flex-direction: column;

View File

@ -456,6 +456,8 @@ export class SettingsView extends LitElement {
presets: { type: Array, state: true },
selectedPreset: { type: Object, state: true },
showPresets: { type: Boolean, state: true },
autoUpdateEnabled: { type: Boolean, state: true },
autoUpdateLoading: { type: Boolean, state: true },
};
//////// after_modelStateService ////////
@ -479,10 +481,48 @@ export class SettingsView extends LitElement {
this.selectedPreset = null;
this.showPresets = false;
this.handleUsePicklesKey = this.handleUsePicklesKey.bind(this)
this.autoUpdateEnabled = true;
this.autoUpdateLoading = true;
this.loadInitialData();
//////// after_modelStateService ////////
}
async loadAutoUpdateSetting() {
if (!window.require) return;
const { ipcRenderer } = window.require('electron');
this.autoUpdateLoading = true;
try {
const enabled = await ipcRenderer.invoke('settings:get-auto-update');
this.autoUpdateEnabled = enabled;
console.log('Auto-update setting loaded:', enabled);
} catch (e) {
console.error('Error loading auto-update setting:', e);
this.autoUpdateEnabled = true; // fallback
}
this.autoUpdateLoading = false;
this.requestUpdate();
}
async handleToggleAutoUpdate() {
if (!window.require || this.autoUpdateLoading) return;
const { ipcRenderer } = window.require('electron');
this.autoUpdateLoading = true;
this.requestUpdate();
try {
const newValue = !this.autoUpdateEnabled;
const result = await ipcRenderer.invoke('settings:set-auto-update', newValue);
if (result && result.success) {
this.autoUpdateEnabled = newValue;
} else {
console.error('Failed to update auto-update setting');
}
} catch (e) {
console.error('Error toggling auto-update:', e);
}
this.autoUpdateLoading = false;
this.requestUpdate();
}
//////// after_modelStateService ////////
async loadInitialData() {
if (!window.require) return;
@ -617,6 +657,7 @@ export class SettingsView extends LitElement {
this.setupEventListeners();
this.setupIpcListeners();
this.setupWindowResize();
this.loadAutoUpdateSetting();
}
disconnectedCallback() {
@ -648,6 +689,7 @@ export class SettingsView extends LitElement {
} else {
this.firebaseUser = null;
}
this.loadAutoUpdateSetting();
this.requestUpdate();
};
@ -1161,6 +1203,9 @@ export class SettingsView extends LitElement {
<button class="settings-button full-width" @click=${this.handlePersonalize}>
<span>Personalize / Meeting Notes</span>
</button>
<button class="settings-button full-width" @click=${this.handleToggleAutoUpdate} ?disabled=${this.autoUpdateLoading}>
<span>Automatic Updates: ${this.autoUpdateEnabled ? 'On' : 'Off'}</span>
</button>
<div class="move-buttons">
<button class="settings-button half-width" @click=${this.handleMoveLeft}>

View File

@ -18,4 +18,6 @@ module.exports = {
createPreset: (...args) => getRepository().createPreset(...args),
updatePreset: (...args) => getRepository().updatePreset(...args),
deletePreset: (...args) => getRepository().deletePreset(...args),
getAutoUpdate: (...args) => getRepository().getAutoUpdate(...args),
setAutoUpdate: (...args) => getRepository().setAutoUpdate(...args),
};

View File

@ -90,10 +90,57 @@ function deletePreset(id, uid) {
}
}
function getAutoUpdate(uid) {
const db = sqliteClient.getDb();
const targetUid = uid;
try {
const row = db.prepare('SELECT auto_update_enabled FROM users WHERE uid = ?').get(targetUid);
if (row) {
console.log('SQLite: Auto update setting found:', row.auto_update_enabled);
return row.auto_update_enabled !== 0;
} else {
// User doesn't exist, create them with default settings
const now = Math.floor(Date.now() / 1000);
const stmt = db.prepare(
'INSERT OR REPLACE INTO users (uid, display_name, email, created_at, auto_update_enabled) VALUES (?, ?, ?, ?, ?)');
stmt.run(targetUid, 'User', 'user@example.com', now, 1);
return true; // default to enabled
}
} catch (error) {
console.error('SQLite: Error getting auto_update_enabled setting:', error);
return true; // fallback to enabled
}
}
function setAutoUpdate(uid, isEnabled) {
const db = sqliteClient.getDb();
const targetUid = uid || sqliteClient.defaultUserId;
try {
const result = db.prepare('UPDATE users SET auto_update_enabled = ? WHERE uid = ?').run(isEnabled ? 1 : 0, targetUid);
// If no rows were updated, the user might not exist, so create them
if (result.changes === 0) {
const now = Math.floor(Date.now() / 1000);
const stmt = db.prepare('INSERT OR REPLACE INTO users (uid, display_name, email, created_at, auto_update_enabled) VALUES (?, ?, ?, ?, ?)');
stmt.run(targetUid, 'User', 'user@example.com', now, isEnabled ? 1 : 0);
}
return { success: true };
} catch (error) {
console.error('SQLite: Error setting auto-update:', error);
throw error;
}
}
module.exports = {
getPresets,
getPresetTemplates,
createPreset,
updatePreset,
deletePreset
deletePreset,
getAutoUpdate,
setAutoUpdate
};

View File

@ -383,6 +383,29 @@ async function updateContentProtection(enabled) {
}
}
async function getAutoUpdateSetting() {
try {
const uid = authService.getCurrentUserId();
// This can be awaited if the repository returns a promise.
// Assuming it's synchronous for now based on original structure.
return settingsRepository.getAutoUpdate(uid);
} catch (error) {
console.error('[SettingsService] Error getting auto update setting:', error);
return true; // Fallback to enabled
}
}
async function setAutoUpdateSetting(isEnabled) {
try {
const uid = authService.getCurrentUserId();
await settingsRepository.setAutoUpdate(uid, isEnabled);
return { success: true };
} catch (error) {
console.error('[SettingsService] Error setting auto update setting:', error);
return { success: false, error: error.message };
}
}
function initialize() {
// cleanup
windowNotificationManager.cleanup();
@ -429,6 +452,15 @@ function initialize() {
return await updateContentProtection(enabled);
});
ipcMain.handle('settings:get-auto-update', async () => {
return await getAutoUpdateSetting();
});
ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => {
console.log('[SettingsService] Setting auto update setting:', isEnabled);
return await setAutoUpdateSetting(isEnabled);
});
console.log('[SettingsService] Initialized and ready.');
}
@ -459,4 +491,5 @@ module.exports = {
saveApiKey,
removeApiKey,
updateContentProtection,
getAutoUpdateSetting,
};

View File

@ -26,6 +26,7 @@ const askService = require('./features/ask/askService');
const settingsService = require('./features/settings/settingsService');
const sessionRepository = require('./common/repositories/session');
const ModelStateService = require('./common/services/modelStateService');
const sqliteClient = require('./common/services/sqliteClient');
const eventBridge = new EventEmitter();
let WEB_PORT = 3000;
@ -189,7 +190,7 @@ app.whenReady().then(async () => {
// Clean up zombie sessions from previous runs first
sessionRepository.endAllActiveSessions();
authService.initialize();
await authService.initialize();
//////// after_modelStateService ////////
modelStateService.initialize();
@ -215,6 +216,7 @@ app.whenReady().then(async () => {
);
}
// initAutoUpdater should be called after auth is initialized
initAutoUpdater();
// Process any pending deep link after everything is initialized
@ -651,8 +653,13 @@ async function startWebStack() {
}
// Auto-update initialization
function initAutoUpdater() {
async function initAutoUpdater() {
try {
const autoUpdateEnabled = await settingsService.getAutoUpdateSetting();
if (!autoUpdateEnabled) {
console.log('[AutoUpdater] Skipped because auto-updates are disabled in settings');
return;
}
// Skip auto-updater in development mode
if (!app.isPackaged) {
console.log('[AutoUpdater] Skipped in development (app is not packaged)');