dreamloop 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,509 @@
1
+ <!doctype html>
2
+ <html lang="{{ lang }}">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>DreamLoop</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
8
+ </head>
9
+ <body>
10
+ <div class="app-frame">
11
+ <aside class="sidebar" aria-label="DreamLoop navigation">
12
+ <a class="brand-mark" href="/?lang={{ lang }}" aria-label="DreamLoop home">
13
+ <span class="brand-orbit"></span>
14
+ <strong>DreamLoop</strong>
15
+ </a>
16
+ <nav class="side-nav">
17
+ <a href="/?lang={{ lang }}" class="{{ 'active' if page == 'dashboard' else '' }}">{{ t.nav_dashboard }}</a>
18
+ <a href="/log?lang={{ lang }}" class="{{ 'active' if page == 'log' else '' }}">{{ t.nav_log }}</a>
19
+ <a href="/patterns?lang={{ lang }}" class="{{ 'active' if page == 'patterns' else '' }}">{{ t.nav_patterns }}</a>
20
+ <a href="/gallery?lang={{ lang }}" class="{{ 'active' if page == 'gallery' else '' }}">{{ t.nav_gallery }}</a>
21
+ <a href="/settings?lang={{ lang }}" class="{{ 'active' if page == 'settings' else '' }}">{{ t.nav_settings }}</a>
22
+ </nav>
23
+ <div class="side-note">
24
+ <span>{{ dreams|length }}</span>
25
+ <p>{{ t.sidebar_count }}</p>
26
+ </div>
27
+ </aside>
28
+
29
+ <main class="dashboard">
30
+ <header class="topbar">
31
+ <div>
32
+ {% if page == "dashboard" %}
33
+ <p class="eyebrow">{{ t.dashboard_eyebrow }}</p>
34
+ <h1>{{ t.dashboard_title }}</h1>
35
+ <p class="tagline">{{ t.dashboard_tagline }}</p>
36
+ {% elif page == "log" %}
37
+ <p class="eyebrow">{{ t.log_eyebrow }}</p>
38
+ <h1>{{ t.log_title }}</h1>
39
+ <p class="tagline">{{ t.log_lede }}</p>
40
+ {% elif page == "patterns" %}
41
+ <p class="eyebrow">{{ t.pattern_map }}</p>
42
+ <h1>{{ t.nav_patterns }}</h1>
43
+ <p class="tagline">{{ t.open_dream_day }}</p>
44
+ {% elif page == "gallery" %}
45
+ <p class="eyebrow">{{ t.gallery_eyebrow }}</p>
46
+ <h1>{{ t.gallery_title }}</h1>
47
+ <p class="tagline">{{ t.gallery_note }}</p>
48
+ {% else %}
49
+ <p class="eyebrow">{{ t.settings_eyebrow }}</p>
50
+ <h1>{{ t.settings_title }}</h1>
51
+ <p class="tagline">{{ t.settings_copy }}</p>
52
+ {% endif %}
53
+ </div>
54
+ <nav class="language-toggle" aria-label="Language">
55
+ <a href="?lang=zh" data-lang="zh" class="{{ 'active' if lang == 'zh' else '' }}">中文</a>
56
+ <a href="?lang=en" data-lang="en" class="{{ 'active' if lang == 'en' else '' }}">English</a>
57
+ </nav>
58
+ </header>
59
+
60
+ {% if page == "dashboard" %}
61
+ <section class="dashboard-hero constellation-sky">
62
+ <div class="star-field" aria-hidden="true"></div>
63
+ <article class="hero-copy">
64
+ <p class="eyebrow">{{ t.quick_loop }}</p>
65
+ <h2>{{ t.dashboard_tagline }}</h2>
66
+ <p class="lede">{{ t.dashboard_lede }}</p>
67
+ <div class="hero-actions">
68
+ <a class="primary-cta" href="/log?lang={{ lang }}">{{ t.dashboard_cta }}</a>
69
+ <span>{{ t.data_never_leaves }}</span>
70
+ </div>
71
+ </article>
72
+ <article class="insight-card">
73
+ <div class="section-title">
74
+ <span>{{ t.structured_output }}</span>
75
+ <h2>{{ t.ai_insight }}</h2>
76
+ </div>
77
+ <strong>{{ dashboard_insight.title }}</strong>
78
+ <p>{{ dashboard_insight.body }}</p>
79
+ </article>
80
+ </section>
81
+
82
+ <section class="status-strip dashboard-stats">
83
+ {% for stat in dashboard_stats[:4] %}
84
+ <div>
85
+ <span>{{ stat.label }}</span>
86
+ <strong>{{ stat.value }}</strong>
87
+ <small>{{ stat.detail }}</small>
88
+ </div>
89
+ {% endfor %}
90
+ </section>
91
+
92
+ <section class="dashboard-grid">
93
+ <article class="panel">
94
+ <div class="section-title">
95
+ <span>{{ t.pattern_map }}</span>
96
+ <h2>{{ t.dream_calendar }}</h2>
97
+ </div>
98
+ <div class="constellation-map mini-heatmap" aria-label="Dream heatmap">
99
+ {% for day in heatmap %}
100
+ <a class="heat-cell heat-level-{{ day.count if day.count < 4 else 4 }}" href="/log?lang={{ lang }}&date={{ day.date }}" title="{{ day.date }}: {{ day.count }} dream(s)">
101
+ <span>{{ day.date[5:] }}</span>
102
+ <strong>{{ day.count }}</strong>
103
+ </a>
104
+ {% else %}
105
+ <p class="empty-state">{{ t.no_heatmap }}</p>
106
+ {% endfor %}
107
+ </div>
108
+ </article>
109
+
110
+ <article class="panel">
111
+ <div class="section-title">
112
+ <span>{{ t.local_logbook }}</span>
113
+ <h2>{{ t.recent_dreams }}</h2>
114
+ </div>
115
+ <div class="recent-list">
116
+ {% for dream in recent_dreams %}
117
+ <a class="recent-row" href="/dreams/{{ dream.id }}?lang={{ lang }}">
118
+ <span>#{{ dream.id }} / {{ dream.dreamed_on }}</span>
119
+ <strong>{{ dream.content }}</strong>
120
+ </a>
121
+ {% else %}
122
+ <p class="empty-state">{{ t.first_dream_waiting }}</p>
123
+ {% endfor %}
124
+ </div>
125
+ </article>
126
+ </section>
127
+
128
+ {% elif page == "log" %}
129
+ <section class="primary-workbench">
130
+ <div class="star-field" aria-hidden="true"></div>
131
+
132
+ <article class="dream-compose">
133
+ <div class="section-title">
134
+ <span>{{ t.local_write }}</span>
135
+ <h2>{{ t.log_title }}</h2>
136
+ </div>
137
+ <p class="lede">{{ t.log_lede }}</p>
138
+ <form method="post" action="/drafts/analyze?lang={{ lang }}" class="quick-capture hero-quick-capture">
139
+ <textarea name="content" placeholder="{{ t.content_placeholder }}" required autofocus>{{ draft.content if draft else draft_content }}</textarea>
140
+ <div class="reflection-fields" aria-label="{{ t.reflection_prompt }}">
141
+ <p class="reflection-heading">{{ t.reflection_prompt }}</p>
142
+ {% for field in reflection_fields %}
143
+ <label>
144
+ <span>{{ field.label }}</span>
145
+ <input name="{{ field.name }}" value="{{ field.value }}" placeholder="{{ field.placeholder }}">
146
+ </label>
147
+ {% endfor %}
148
+ </div>
149
+ {% if analysis_error %}
150
+ <p class="status-message warning">{{ t.analysis_failed }}</p>
151
+ {% elif not ai.ready %}
152
+ <p class="status-message warning">{{ t.analysis_unavailable_before_save }}</p>
153
+ {% endif %}
154
+ <div class="form-actions">
155
+ <button type="submit">{{ t.analyze_dream }}</button>
156
+ {% if analysis_error or not ai.ready %}
157
+ <button type="submit" class="secondary-button" formaction="/dreams?lang={{ lang }}" formmethod="post">{{ t.save_without_ai }}</button>
158
+ {% endif %}
159
+ </div>
160
+ <p class="privacy-line">{{ t.data_never_leaves }}</p>
161
+ </form>
162
+ </article>
163
+
164
+ <article class="analysis-panel">
165
+ <div class="section-title">
166
+ <span>{{ t.structured_output }}</span>
167
+ <h2>{{ t.ai_analysis }}</h2>
168
+ </div>
169
+
170
+ {% if draft %}
171
+ <div class="latest-dream-preview">
172
+ <span>{{ t.draft_analysis }} / {{ draft.language }} / {{ t.draft_not_saved }}</span>
173
+ <p>{{ draft.content }}</p>
174
+ </div>
175
+ <div class="analysis-status ready">{{ t.analysis_ready }}</div>
176
+ <dl class="analysis-list">
177
+ <div><dt>{{ t.emotional_tone }}</dt><dd>{{ draft.analysis.emotional_tone }}</dd></div>
178
+ <div><dt>{{ t.symbols }}</dt><dd>{{ ", ".join(draft.analysis.symbols) or "-" }}</dd></div>
179
+ <div><dt>{{ t.themes }}</dt><dd>{{ ", ".join(draft.analysis.themes) or "-" }}</dd></div>
180
+ <div><dt>{{ t.confidence }}</dt><dd>{{ "%.0f"|format(draft.analysis.confidence * 100) }}%</dd></div>
181
+ </dl>
182
+ <p class="analysis-summary">{{ draft.analysis.summary }}</p>
183
+ {% set report = draft.analysis.report %}
184
+ {% if report.analysis_version %}
185
+ <div class="dream-report">
186
+ {% if report.dream_details %}
187
+ <section class="report-section">
188
+ <h3>{{ t.dream_details }}</h3>
189
+ <ul>
190
+ {% for item in report.dream_details %}
191
+ <li>{{ item }}</li>
192
+ {% endfor %}
193
+ </ul>
194
+ </section>
195
+ {% endif %}
196
+ {% if report.core_emotion %}
197
+ <section class="report-section">
198
+ <h3>{{ t.core_emotion }}</h3>
199
+ <p>{{ report.core_emotion }}</p>
200
+ </section>
201
+ {% endif %}
202
+ {% if report.real_life_links %}
203
+ <section class="report-section">
204
+ <h3>{{ t.real_life_links }}</h3>
205
+ <ul>
206
+ {% for item in report.real_life_links %}
207
+ <li>{{ item }}</li>
208
+ {% endfor %}
209
+ </ul>
210
+ </section>
211
+ {% endif %}
212
+ {% if report.possible_interpretations %}
213
+ <section class="report-section">
214
+ <h3>{{ t.possible_interpretations }}</h3>
215
+ <div class="interpretation-list">
216
+ {% for item in report.possible_interpretations %}
217
+ <article>
218
+ <strong>{{ item.title or loop.index }}</strong>
219
+ <p>{{ item.interpretation }}</p>
220
+ <small>{{ item.dream_evidence }}</small>
221
+ <small>{{ item.real_life_connection }}</small>
222
+ <em>{{ item.verification_question }}</em>
223
+ </article>
224
+ {% endfor %}
225
+ </div>
226
+ </section>
227
+ {% endif %}
228
+ {% if report.real_life_questions %}
229
+ <section class="report-section reality-focus">
230
+ <h3>{{ t.real_life_questions }}</h3>
231
+ <ul>
232
+ {% for item in report.real_life_questions %}
233
+ <li>{{ item }}</li>
234
+ {% endfor %}
235
+ </ul>
236
+ </section>
237
+ {% endif %}
238
+ {% if report.verification_prompts %}
239
+ <section class="report-section">
240
+ <h3>{{ t.verification_prompts }}</h3>
241
+ <ul>
242
+ {% for item in report.verification_prompts %}
243
+ <li>{{ item }}</li>
244
+ {% endfor %}
245
+ </ul>
246
+ </section>
247
+ {% endif %}
248
+ </div>
249
+ {% endif %}
250
+ <form method="post" action="/drafts/save?lang={{ lang }}" class="draft-actions">
251
+ <input type="hidden" name="content" value="{{ draft.content }}">
252
+ <input type="hidden" name="analysis_language" value="{{ draft.language }}">
253
+ <textarea name="reflections_json" hidden>{{ draft.reflections_json }}</textarea>
254
+ <textarea name="analysis_json" hidden>{{ draft.analysis_json }}</textarea>
255
+ <button type="submit">{{ t.save_analysis }}</button>
256
+ <a href="/log?lang={{ lang }}" class="secondary-link">{{ t.discard }}</a>
257
+ </form>
258
+ <details>
259
+ <summary>{{ t.raw_json }}</summary>
260
+ <pre><code>{{ draft.analysis.raw_json }}</code></pre>
261
+ </details>
262
+ {% elif not latest_dream %}
263
+ <p class="empty-state">{{ t.no_dream }}</p>
264
+ {% elif latest_dream.analysis %}
265
+ <div class="latest-dream-preview">
266
+ <span>{{ t.latest_dream }} / #{{ latest_dream.id }} / {{ latest_dream.dreamed_on }}</span>
267
+ <p>{{ latest_dream.content }}</p>
268
+ </div>
269
+ <div class="analysis-status ready">{{ t.analysis_ready }}</div>
270
+ <dl class="analysis-list">
271
+ <div><dt>{{ t.emotional_tone }}</dt><dd>{{ latest_dream.analysis.emotional_tone }}</dd></div>
272
+ <div><dt>{{ t.symbols }}</dt><dd>{{ ", ".join(latest_dream.analysis.symbols) or "-" }}</dd></div>
273
+ <div><dt>{{ t.themes }}</dt><dd>{{ ", ".join(latest_dream.analysis.themes) or "-" }}</dd></div>
274
+ <div><dt>{{ t.confidence }}</dt><dd>{{ "%.0f"|format(latest_dream.analysis.confidence * 100) }}%</dd></div>
275
+ </dl>
276
+ <p class="analysis-summary">{{ latest_dream.analysis.summary }}</p>
277
+ {% elif ai.ready %}
278
+ <div class="latest-dream-preview">
279
+ <span>{{ t.latest_dream }} / #{{ latest_dream.id }} / {{ latest_dream.dreamed_on }}</span>
280
+ <p>{{ latest_dream.content }}</p>
281
+ </div>
282
+ <div class="analysis-status pending">{{ t.missing_analysis }}</div>
283
+ <form method="post" action="/dreams/{{ latest_dream.id }}/analyze?lang={{ lang }}" class="analyze-form">
284
+ <button type="submit">{{ t.generate_chinese_analysis if lang == "zh" else t.generate_english_analysis }}</button>
285
+ </form>
286
+ {% else %}
287
+ <div class="analysis-status unavailable">{{ t.pending_analysis }}</div>
288
+ <p class="empty-state">{{ t.analysis_unavailable }}</p>
289
+ {% endif %}
290
+ </article>
291
+ </section>
292
+
293
+ {% if log_dreams %}
294
+ <section class="log-page-grid">
295
+ <article class="panel">
296
+ <div class="section-title">
297
+ <span>{{ t.local_logbook }}</span>
298
+ <h2>{{ t.dreamscape_log }}{% if date_filter or symbol_filter or theme_filter %} / {{ t.filter }}{% endif %}</h2>
299
+ </div>
300
+ {% if date_filter or symbol_filter or theme_filter %}
301
+ <p class="filter-badge">
302
+ {{ date_filter or symbol_filter or theme_filter }}
303
+ <a href="/log?lang={{ lang }}">{{ t.clear_filter }}</a>
304
+ </p>
305
+ {% endif %}
306
+ <div class="dream-grid">
307
+ {% for dream in log_dreams %}
308
+ <a class="dream-card" href="/dreams/{{ dream.id }}?lang={{ lang }}">
309
+ <span>#{{ dream.id }} / {{ dream.dreamed_on }}</span>
310
+ <strong>{{ dream.content }}</strong>
311
+ <small>{{ dream.manual_mood or (dream.analysis.emotional_tone if dream.analysis else t.no_mood) }} / {{ ", ".join(dream.tags) or t.no_tags }}</small>
312
+ </a>
313
+ {% endfor %}
314
+ </div>
315
+ </article>
316
+ </section>
317
+ {% endif %}
318
+
319
+ {% elif page == "patterns" %}
320
+ <section class="pattern-grid">
321
+ <article class="panel panel-wide">
322
+ <div class="section-title">
323
+ <span>{{ t.pattern_map }}</span>
324
+ <h2>{{ t.dream_calendar }}</h2>
325
+ </div>
326
+ <div class="constellation-map" aria-label="Dream heatmap">
327
+ {% for day in heatmap %}
328
+ <a class="heat-cell heat-level-{{ day.count if day.count < 4 else 4 }}" href="/log?lang={{ lang }}&date={{ day.date }}" title="{{ day.date }}: {{ day.count }} dream(s)">
329
+ <span>{{ day.date[5:] }}</span>
330
+ <strong>{{ day.count }}</strong>
331
+ </a>
332
+ {% else %}
333
+ <p class="empty-state">{{ t.no_heatmap }}</p>
334
+ {% endfor %}
335
+ </div>
336
+ <p class="runtime-note">{{ t.open_dream_day }}</p>
337
+ </article>
338
+
339
+ <article class="panel">
340
+ <div class="section-title">
341
+ <span>{{ t.signals }}</span>
342
+ <h2>{{ t.mood_spectrum }}</h2>
343
+ </div>
344
+ <div class="spectrum-list">
345
+ {% for mood in mood_spectrum %}
346
+ <div class="spectrum-row">
347
+ <span>{{ mood.name }}</span>
348
+ <div class="spectrum-track"><i style="width: {{ mood.percent }}%"></i></div>
349
+ <strong>{{ mood.count }}</strong>
350
+ </div>
351
+ {% else %}
352
+ <p class="empty-state">{{ t.no_moods }}</p>
353
+ {% endfor %}
354
+ </div>
355
+ </article>
356
+
357
+ <article class="panel">
358
+ <div class="section-title">
359
+ <span>{{ t.structured_output }}</span>
360
+ <h2>{{ t.symbols }}</h2>
361
+ </div>
362
+ <div class="symbol-stack">
363
+ {% for item in trends.symbols[:7] %}
364
+ <a class="symbol-row trend-link" href="/log?lang={{ lang }}&symbol={{ item.name }}">
365
+ <span>{{ item.name }}</span><i style="width: {{ item.percent or 36 }}%"></i><strong>{{ item.count }}</strong>
366
+ </a>
367
+ {% else %}
368
+ <div class="symbol-row muted-row"><span>{{ t.no_symbols }}</span><i></i><strong>0</strong></div>
369
+ {% endfor %}
370
+ </div>
371
+ </article>
372
+
373
+ <article class="panel">
374
+ <div class="section-title">
375
+ <span>{{ t.theme_trends }}</span>
376
+ <h2>{{ t.themes }}</h2>
377
+ </div>
378
+ <div class="symbol-stack">
379
+ {% for item in trends.themes[:7] %}
380
+ <a class="symbol-row trend-link" href="/log?lang={{ lang }}&theme={{ item.name }}">
381
+ <span>{{ item.name }}</span><i style="width: {{ item.percent or 36 }}%"></i><strong>{{ item.count }}</strong>
382
+ </a>
383
+ {% else %}
384
+ <div class="symbol-row muted-row"><span>{{ t.no_symbols }}</span><i></i><strong>0</strong></div>
385
+ {% endfor %}
386
+ </div>
387
+ </article>
388
+
389
+ <article class="panel">
390
+ <div class="section-title">
391
+ <span>{{ t.ai_insight }}</span>
392
+ <h2>{{ t.pattern_summary }}</h2>
393
+ </div>
394
+ <div class="insight-callout">
395
+ <strong>{{ dashboard_insight.title }}</strong>
396
+ <p>{{ dashboard_insight.body }}</p>
397
+ </div>
398
+ </article>
399
+ </section>
400
+
401
+ {% elif page == "gallery" %}
402
+ <section class="gallery-grid">
403
+ {% for dream in gallery_cards %}
404
+ <a class="gallery-card constellation-sky" href="/dreams/{{ dream.id }}?lang={{ lang }}">
405
+ <div class="star-field" aria-hidden="true"></div>
406
+ <span>#{{ dream.id }} / {{ dream.dreamed_on }}</span>
407
+ <strong>{{ dream.analysis.summary if dream.analysis else dream.content }}</strong>
408
+ <small>{{ ", ".join(dream.analysis.symbols[:3]) if dream.analysis else t.visual_memory_note }}</small>
409
+ </a>
410
+ {% else %}
411
+ <article class="panel empty-gallery">
412
+ <div class="section-title">
413
+ <span>{{ t.gallery_eyebrow }}</span>
414
+ <h2>{{ t.gallery_title }}</h2>
415
+ </div>
416
+ <p class="empty-state">{{ t.gallery_empty }}</p>
417
+ <a class="primary-cta" href="/log?lang={{ lang }}">{{ t.dashboard_cta }}</a>
418
+ </article>
419
+ {% endfor %}
420
+ </section>
421
+
422
+ {% elif page == "settings" %}
423
+ <section class="settings-grid">
424
+ <article class="panel settings-panel">
425
+ <div class="section-title">
426
+ <span>{{ t.settings_eyebrow }}</span>
427
+ <h2>{{ t.settings_title }}</h2>
428
+ </div>
429
+ <p class="lede">{{ t.settings_copy }}</p>
430
+ {% if settings_saved %}
431
+ <p class="status-message ready-message">{{ t.settings_saved }}</p>
432
+ {% endif %}
433
+ <form method="post" action="/settings/ai?lang={{ lang }}" class="settings-form">
434
+ <label>
435
+ <span>{{ t.provider }}</span>
436
+ <select name="provider">
437
+ <option value="ollama" {{ "selected" if ai_config.provider == "ollama" else "" }}>Ollama</option>
438
+ <option value="deepseek" {{ "selected" if ai_config.provider == "deepseek" else "" }}>DeepSeek</option>
439
+ <option value="openai" {{ "selected" if ai_config.provider == "openai" else "" }}>OpenAI</option>
440
+ <option value="custom" {{ "selected" if ai_config.provider == "custom" else "" }}>Custom OpenAI-compatible</option>
441
+ <option value="none" {{ "selected" if ai_config.provider == "none" else "" }}>None</option>
442
+ </select>
443
+ </label>
444
+ <label>
445
+ <span>{{ t.model }}</span>
446
+ <input name="model" value="{{ ai_config.model }}" placeholder="qwen3:8b / deepseek-v4-flash">
447
+ </label>
448
+ <label>
449
+ <span>{{ t.base_url }}</span>
450
+ <input name="base_url" value="{{ ai_config.base_url }}" placeholder="http://localhost:11434/v1">
451
+ </label>
452
+ <label>
453
+ <span>{{ t.api_key }}</span>
454
+ <input name="api_key" type="password" value="" placeholder="{{ t.api_key_placeholder }}">
455
+ </label>
456
+ <p class="runtime-note">{{ t.settings_secret_note }}</p>
457
+ <button type="submit">{{ t.save_settings }}</button>
458
+ </form>
459
+ </article>
460
+
461
+ <article class="panel">
462
+ <div class="section-title">
463
+ <span>{{ t.provider_status }}</span>
464
+ <h2>{{ t.local_runtime }}</h2>
465
+ </div>
466
+ <dl class="runtime-list">
467
+ <div><dt>{{ t.provider }}</dt><dd>{{ ai.provider }}</dd></div>
468
+ <div><dt>{{ t.model }}</dt><dd>{{ ai.model or "capture only" }}</dd></div>
469
+ <div><dt>{{ t.base_url }}</dt><dd>{{ ai.base_url or "-" }}</dd></div>
470
+ <div><dt>{{ t.data_dir }}</dt><dd>{{ data_dir }}</dd></div>
471
+ <div><dt>SQLite</dt><dd>local</dd></div>
472
+ </dl>
473
+ {% if ai.warning %}
474
+ <p class="runtime-note">{{ ai.warning }}</p>
475
+ {% else %}
476
+ <p class="runtime-note">{{ t.provider_configured }}</p>
477
+ {% endif %}
478
+ <div class="developer-note">
479
+ <strong>{{ t.developer_note }}</strong>
480
+ <p>{{ t.cli_note }}</p>
481
+ </div>
482
+ </article>
483
+ </section>
484
+ {% endif %}
485
+ </main>
486
+ </div>
487
+
488
+ <script>
489
+ (() => {
490
+ const params = new URLSearchParams(window.location.search);
491
+ const current = params.get("lang");
492
+ const saved = window.localStorage.getItem("dreamloop.lang");
493
+ if (!current && saved && ["zh", "en"].includes(saved)) {
494
+ params.set("lang", saved);
495
+ window.location.replace(`${window.location.pathname}?${params.toString()}${window.location.hash}`);
496
+ return;
497
+ }
498
+ if (current && ["zh", "en"].includes(current)) {
499
+ window.localStorage.setItem("dreamloop.lang", current);
500
+ }
501
+ document.querySelectorAll("[data-lang]").forEach((link) => {
502
+ link.addEventListener("click", () => {
503
+ window.localStorage.setItem("dreamloop.lang", link.dataset.lang);
504
+ });
505
+ });
506
+ })();
507
+ </script>
508
+ </body>
509
+ </html>