Spaces:
Paused
Paused
Savestate: Add background interval sync and instant push triggers for multi-device sync
Browse files- index.html +1 -1
- src/services/sync-service.js +28 -4
- src/views/chat/chat-view.js +15 -0
index.html
CHANGED
|
@@ -51,7 +51,7 @@
|
|
| 51 |
</head>
|
| 52 |
<body class="dark-theme in-chat-view">
|
| 53 |
|
| 54 |
-
<div id="live-version-badge" style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); font-size: 10px; color: var(--text-secondary); opacity: 0.6; z-index: 99999; pointer-events: none; font-weight: 500; font-family: monospace; letter-spacing: 1px; background: rgba(0,0,0,0.2); padding: 2px 8px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.05);">BUILD 1.4.
|
| 55 |
|
| 56 |
<!-- SVG Filters for Liquid Glass Refraction -->
|
| 57 |
<svg width="0" height="0" style="position: absolute; pointer-events: none;">
|
|
|
|
| 51 |
</head>
|
| 52 |
<body class="dark-theme in-chat-view">
|
| 53 |
|
| 54 |
+
<div id="live-version-badge" style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); font-size: 10px; color: var(--text-secondary); opacity: 0.6; z-index: 99999; pointer-events: none; font-weight: 500; font-family: monospace; letter-spacing: 1px; background: rgba(0,0,0,0.2); padding: 2px 8px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.05);">BUILD 1.4.6</div>
|
| 55 |
|
| 56 |
<!-- SVG Filters for Liquid Glass Refraction -->
|
| 57 |
<svg width="0" height="0" style="position: absolute; pointer-events: none;">
|
src/services/sync-service.js
CHANGED
|
@@ -23,6 +23,13 @@ class SyncManager {
|
|
| 23 |
// Called once on app startup to trigger initial sync if already logged in
|
| 24 |
if (authService.user && authService.isCryptoUnlocked()) {
|
| 25 |
this.sync();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
}
|
| 28 |
|
|
@@ -33,11 +40,17 @@ class SyncManager {
|
|
| 33 |
|
| 34 |
console.log('[Sync] Starting Sync...');
|
| 35 |
try {
|
| 36 |
-
|
| 37 |
-
await this.
|
| 38 |
-
await this.
|
|
|
|
| 39 |
|
| 40 |
console.log('[Sync] Sync completed successfully.');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
this.lastSyncTime = Date.now();
|
| 42 |
} catch (e) {
|
| 43 |
console.error('[Sync] Error during sync:', e);
|
|
@@ -92,16 +105,21 @@ class SyncManager {
|
|
| 92 |
}, { onConflict: 'user_id, key' });
|
| 93 |
}
|
| 94 |
|
|
|
|
| 95 |
// Pull any setting from remote that isn't locally set (e.g. from another device)
|
| 96 |
if (remoteSettings) {
|
| 97 |
for (const row of remoteSettings) {
|
| 98 |
const localVal = db.getSetting(row.key);
|
| 99 |
if (!localVal) {
|
| 100 |
const decrypted = await cryptoService.decrypt(row.value);
|
| 101 |
-
if (decrypted)
|
|
|
|
|
|
|
|
|
|
| 102 |
}
|
| 103 |
}
|
| 104 |
}
|
|
|
|
| 105 |
}
|
| 106 |
|
| 107 |
async syncConversations(userId) {
|
|
@@ -140,6 +158,7 @@ class SyncManager {
|
|
| 140 |
}
|
| 141 |
}
|
| 142 |
|
|
|
|
| 143 |
// Pull from remote if remote is newer or doesn't exist locally
|
| 144 |
if (remoteConvs) {
|
| 145 |
for (const remote of remoteConvs) {
|
|
@@ -163,9 +182,11 @@ class SyncManager {
|
|
| 163 |
[title, emoji, preview, remote.model_used, remote.updated_at, remote.id]
|
| 164 |
);
|
| 165 |
}
|
|
|
|
| 166 |
}
|
| 167 |
}
|
| 168 |
}
|
|
|
|
| 169 |
}
|
| 170 |
|
| 171 |
async syncMessages(userId) {
|
|
@@ -198,6 +219,7 @@ class SyncManager {
|
|
| 198 |
}
|
| 199 |
}
|
| 200 |
|
|
|
|
| 201 |
// Pull remote to local
|
| 202 |
for (const remote of (remoteMsgs || [])) {
|
| 203 |
if (!localIds.has(remote.id)) {
|
|
@@ -213,9 +235,11 @@ class SyncManager {
|
|
| 213 |
"INSERT INTO messages (id, conversation_id, role, content, model_used, style_used, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
| 214 |
[remoteFull.id, remoteFull.conversation_id, remoteFull.role, content, remoteFull.model_used, remoteFull.style_used, remoteFull.created_at]
|
| 215 |
);
|
|
|
|
| 216 |
}
|
| 217 |
}
|
| 218 |
}
|
|
|
|
| 219 |
}
|
| 220 |
|
| 221 |
}
|
|
|
|
| 23 |
// Called once on app startup to trigger initial sync if already logged in
|
| 24 |
if (authService.user && authService.isCryptoUnlocked()) {
|
| 25 |
this.sync();
|
| 26 |
+
|
| 27 |
+
// Auto-Sync every 15 seconds
|
| 28 |
+
setInterval(() => {
|
| 29 |
+
if (authService.user && authService.isCryptoUnlocked()) {
|
| 30 |
+
this.sync();
|
| 31 |
+
}
|
| 32 |
+
}, 15000);
|
| 33 |
}
|
| 34 |
}
|
| 35 |
|
|
|
|
| 40 |
|
| 41 |
console.log('[Sync] Starting Sync...');
|
| 42 |
try {
|
| 43 |
+
let hasUpdates = false;
|
| 44 |
+
if (await this.syncSettings(userId)) hasUpdates = true;
|
| 45 |
+
if (await this.syncConversations(userId)) hasUpdates = true;
|
| 46 |
+
if (await this.syncMessages(userId)) hasUpdates = true;
|
| 47 |
|
| 48 |
console.log('[Sync] Sync completed successfully.');
|
| 49 |
+
|
| 50 |
+
if (hasUpdates) {
|
| 51 |
+
console.log('[Sync] New remote changes found, dispatching event.');
|
| 52 |
+
window.dispatchEvent(new CustomEvent('cloud_sync_updated'));
|
| 53 |
+
}
|
| 54 |
this.lastSyncTime = Date.now();
|
| 55 |
} catch (e) {
|
| 56 |
console.error('[Sync] Error during sync:', e);
|
|
|
|
| 105 |
}, { onConflict: 'user_id, key' });
|
| 106 |
}
|
| 107 |
|
| 108 |
+
let updated = false;
|
| 109 |
// Pull any setting from remote that isn't locally set (e.g. from another device)
|
| 110 |
if (remoteSettings) {
|
| 111 |
for (const row of remoteSettings) {
|
| 112 |
const localVal = db.getSetting(row.key);
|
| 113 |
if (!localVal) {
|
| 114 |
const decrypted = await cryptoService.decrypt(row.value);
|
| 115 |
+
if (decrypted) {
|
| 116 |
+
db.setSetting(row.key, decrypted);
|
| 117 |
+
updated = true;
|
| 118 |
+
}
|
| 119 |
}
|
| 120 |
}
|
| 121 |
}
|
| 122 |
+
return updated;
|
| 123 |
}
|
| 124 |
|
| 125 |
async syncConversations(userId) {
|
|
|
|
| 158 |
}
|
| 159 |
}
|
| 160 |
|
| 161 |
+
let updated = false;
|
| 162 |
// Pull from remote if remote is newer or doesn't exist locally
|
| 163 |
if (remoteConvs) {
|
| 164 |
for (const remote of remoteConvs) {
|
|
|
|
| 182 |
[title, emoji, preview, remote.model_used, remote.updated_at, remote.id]
|
| 183 |
);
|
| 184 |
}
|
| 185 |
+
updated = true;
|
| 186 |
}
|
| 187 |
}
|
| 188 |
}
|
| 189 |
+
return updated;
|
| 190 |
}
|
| 191 |
|
| 192 |
async syncMessages(userId) {
|
|
|
|
| 219 |
}
|
| 220 |
}
|
| 221 |
|
| 222 |
+
let updated = false;
|
| 223 |
// Pull remote to local
|
| 224 |
for (const remote of (remoteMsgs || [])) {
|
| 225 |
if (!localIds.has(remote.id)) {
|
|
|
|
| 235 |
"INSERT INTO messages (id, conversation_id, role, content, model_used, style_used, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
| 236 |
[remoteFull.id, remoteFull.conversation_id, remoteFull.role, content, remoteFull.model_used, remoteFull.style_used, remoteFull.created_at]
|
| 237 |
);
|
| 238 |
+
updated = true;
|
| 239 |
}
|
| 240 |
}
|
| 241 |
}
|
| 242 |
+
return updated;
|
| 243 |
}
|
| 244 |
|
| 245 |
}
|
src/views/chat/chat-view.js
CHANGED
|
@@ -5,6 +5,7 @@ import { PresentationRenderer } from '../../utils/presentation-renderer.js';
|
|
| 5 |
import { deepResearchLoader } from '../../utils/deep-research-loader.js';
|
| 6 |
import { presentationLoader } from '../../utils/presentation-loader.js';
|
| 7 |
import { getApiUrl } from '../../config.js';
|
|
|
|
| 8 |
|
| 9 |
window.youtubeSearchCache = window.youtubeSearchCache || {};
|
| 10 |
|
|
@@ -159,6 +160,14 @@ export class ChatView {
|
|
| 159 |
this.setupTimelineObserver();
|
| 160 |
this.setupAnnotations();
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
// Inject dynamic styles to bypass aggressive browser CSS caching
|
| 163 |
if (!document.getElementById('dynamic-source-styles')) {
|
| 164 |
const style = document.createElement('style');
|
|
@@ -4180,6 +4189,9 @@ body { margin: 0; padding: 0; background: #000; overflow: hidden; display: flex;
|
|
| 4180 |
|
| 4181 |
this.appendMessageElement('user', text, true);
|
| 4182 |
this.buildTimeline();
|
|
|
|
|
|
|
|
|
|
| 4183 |
|
| 4184 |
// Auto-update title if it's the first message
|
| 4185 |
const msgs = db.getMessages(this.currentConversationId);
|
|
@@ -4346,6 +4358,9 @@ body { margin: 0; padding: 0; background: #000; overflow: hidden; display: flex;
|
|
| 4346 |
db.addMessage(aiMsgId, this.currentConversationId, 'assistant', processedResponseText, model, 'default');
|
| 4347 |
this.appendMessageElement('assistant', processedResponseText, false, true, text);
|
| 4348 |
|
|
|
|
|
|
|
|
|
|
| 4349 |
// 4. Dynamic AI Title Generation (Only after the very first Q&A pair)
|
| 4350 |
const currentMsgs = db.getMessages(this.currentConversationId);
|
| 4351 |
if (currentMsgs.length === 2) { // 1 User msg + 1 AI msg
|
|
|
|
| 5 |
import { deepResearchLoader } from '../../utils/deep-research-loader.js';
|
| 6 |
import { presentationLoader } from '../../utils/presentation-loader.js';
|
| 7 |
import { getApiUrl } from '../../config.js';
|
| 8 |
+
import { syncManager } from '../../services/sync-service.js';
|
| 9 |
|
| 10 |
window.youtubeSearchCache = window.youtubeSearchCache || {};
|
| 11 |
|
|
|
|
| 160 |
this.setupTimelineObserver();
|
| 161 |
this.setupAnnotations();
|
| 162 |
|
| 163 |
+
// Listen for cloud sync updates to refresh sidebar
|
| 164 |
+
window.addEventListener('cloud_sync_updated', () => {
|
| 165 |
+
if (typeof this.loadHistory === 'function') {
|
| 166 |
+
this.loadHistory();
|
| 167 |
+
}
|
| 168 |
+
});
|
| 169 |
+
|
| 170 |
+
|
| 171 |
// Inject dynamic styles to bypass aggressive browser CSS caching
|
| 172 |
if (!document.getElementById('dynamic-source-styles')) {
|
| 173 |
const style = document.createElement('style');
|
|
|
|
| 4189 |
|
| 4190 |
this.appendMessageElement('user', text, true);
|
| 4191 |
this.buildTimeline();
|
| 4192 |
+
|
| 4193 |
+
// Trigger sync instantly so the message uploads to cloud
|
| 4194 |
+
syncManager.sync();
|
| 4195 |
|
| 4196 |
// Auto-update title if it's the first message
|
| 4197 |
const msgs = db.getMessages(this.currentConversationId);
|
|
|
|
| 4358 |
db.addMessage(aiMsgId, this.currentConversationId, 'assistant', processedResponseText, model, 'default');
|
| 4359 |
this.appendMessageElement('assistant', processedResponseText, false, true, text);
|
| 4360 |
|
| 4361 |
+
// Trigger sync for AI response
|
| 4362 |
+
syncManager.sync();
|
| 4363 |
+
|
| 4364 |
// 4. Dynamic AI Title Generation (Only after the very first Q&A pair)
|
| 4365 |
const currentMsgs = db.getMessages(this.currentConversationId);
|
| 4366 |
if (currentMsgs.length === 2) { // 1 User msg + 1 AI msg
|