getfilepress 0.1.2 → 0.1.4
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.
- package/README.md +36 -10
- package/package.json +99 -99
- package/packages/app/README.md +19 -0
- package/packages/app/package.json +1 -1
- package/packages/app/src/lib/genie/GeniePanel.svelte +672 -144
- package/packages/app/src/lib/genie/config-patch.ts +72 -0
- package/packages/app/src/lib/genie/ops.ts +231 -10
- package/packages/app/src/lib/genie/store.ts +12 -3
- package/packages/app/src/lib/pages.server.ts +4 -2
- package/packages/app/src/routes/sitemap.xml/+server.ts +5 -2
- package/packages/app/vite-plugin-genie.ts +64 -1
- package/packages/app/vite-plugin-path-mounts.ts +107 -0
- package/packages/app/vite.config.ts +112 -81
- package/packages/core/README.md +29 -0
- package/packages/core/package.json +1 -1
- package/packages/core/src/lib/config.ts +12 -1
- package/packages/core/src/lib/content/feeds.ts +5 -1
- package/packages/core/src/lib/content/pages.ts +11 -1
- package/packages/core/src/lib/content/parse.ts +12 -3
- package/packages/core/src/lib/index.ts +2 -1
- package/packages/core/src/lib/paths-shared.ts +89 -0
- package/packages/core/src/lib/paths.ts +84 -0
- package/packages/core/src/lib/server.ts +8 -1
- package/packages/core/src/lib/styles/theme.css +3 -2
- package/packages/import/package.json +2 -1
- package/packages/import/src/cli.ts +77 -3
- package/packages/import/src/ollama.ts +43 -1
- package/packages/import/src/ollanet-scan.ts +132 -0
- package/packages/import/tsconfig.json +1 -1
- package/scripts/copy-path-mounts.mjs +40 -0
- package/scripts/create-site.mjs +4 -11
- package/scripts/filepress.mjs +31 -3
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
type Health = {
|
|
11
|
-
ollama: {
|
|
11
|
+
ollama: {
|
|
12
|
+
available: boolean;
|
|
13
|
+
hint: string;
|
|
14
|
+
model: string;
|
|
15
|
+
host: string;
|
|
16
|
+
models: string[];
|
|
17
|
+
};
|
|
12
18
|
active: { versionId: string; activatedAt: string } | null;
|
|
13
19
|
versions: VersionRow[];
|
|
14
20
|
brief: {
|
|
@@ -22,12 +28,44 @@
|
|
|
22
28
|
};
|
|
23
29
|
};
|
|
24
30
|
|
|
31
|
+
type DiscoveredServer = {
|
|
32
|
+
label: string;
|
|
33
|
+
endpoint: string;
|
|
34
|
+
source: string;
|
|
35
|
+
self: boolean;
|
|
36
|
+
models: string[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type TabId = 'refine' | 'look' | 'images' | 'inspire' | 'config' | 'history';
|
|
40
|
+
|
|
41
|
+
const TABS: Array<{ id: TabId; label: string; hint: string }> = [
|
|
42
|
+
{ id: 'refine', label: 'Refine', hint: 'Ask Ollama' },
|
|
43
|
+
{ id: 'look', label: 'Look', hint: 'Quick steers' },
|
|
44
|
+
{ id: 'images', label: 'Images', hint: 'Stock / upload' },
|
|
45
|
+
{ id: 'inspire', label: 'Inspire', hint: 'From URLs' },
|
|
46
|
+
{ id: 'config', label: 'Config', hint: 'Lede / logo' },
|
|
47
|
+
{ id: 'history', label: 'History', hint: 'Versions' }
|
|
48
|
+
];
|
|
49
|
+
|
|
25
50
|
let open = $state(false);
|
|
51
|
+
let tab = $state<TabId>('refine');
|
|
26
52
|
let loading = $state(false);
|
|
27
53
|
let error = $state('');
|
|
28
54
|
let health = $state<Health | null>(null);
|
|
29
55
|
let stockQuery = $state('abstract dark texture');
|
|
30
56
|
let accent = $state('#1e4d6b');
|
|
57
|
+
let inspireUrls = $state('https://www.catalystforge.com\n');
|
|
58
|
+
let useLlm = $state(true);
|
|
59
|
+
let selectedModel = $state('');
|
|
60
|
+
let selectedHost = $state('');
|
|
61
|
+
let discovered = $state<DiscoveredServer[]>([]);
|
|
62
|
+
let includeLan = $state(false);
|
|
63
|
+
let scanning = $state(false);
|
|
64
|
+
let scanNote = $state('');
|
|
65
|
+
let refinePrompt = $state('');
|
|
66
|
+
let cfgLede = $state('');
|
|
67
|
+
let cfgTagline = $state('');
|
|
68
|
+
let cfgLogo = $state('');
|
|
31
69
|
|
|
32
70
|
async function api(path: string, init?: RequestInit) {
|
|
33
71
|
const res = await fetch(`/__filepress/genie${path}`, {
|
|
@@ -48,6 +86,11 @@
|
|
|
48
86
|
try {
|
|
49
87
|
health = await api('/health');
|
|
50
88
|
if (health?.brief?.tokens?.accent) accent = health.brief.tokens.accent;
|
|
89
|
+
if (health?.ollama?.host && !selectedHost) selectedHost = health.ollama.host;
|
|
90
|
+
if (health?.ollama?.model && !selectedModel) selectedModel = health.ollama.model;
|
|
91
|
+
else if (health?.ollama?.models?.length && !selectedModel) {
|
|
92
|
+
selectedModel = health.ollama.models[0];
|
|
93
|
+
}
|
|
51
94
|
} catch (e) {
|
|
52
95
|
error = e instanceof Error ? e.message : String(e);
|
|
53
96
|
} finally {
|
|
@@ -57,6 +100,7 @@
|
|
|
57
100
|
|
|
58
101
|
async function openPanel() {
|
|
59
102
|
open = true;
|
|
103
|
+
tab = 'refine';
|
|
60
104
|
await refresh();
|
|
61
105
|
}
|
|
62
106
|
|
|
@@ -66,7 +110,6 @@
|
|
|
66
110
|
try {
|
|
67
111
|
await api('/steer', { method: 'POST', body: JSON.stringify({ brief, label }) });
|
|
68
112
|
await refresh();
|
|
69
|
-
// Theme alias is a real file — full reload picks up CSS reliably.
|
|
70
113
|
location.reload();
|
|
71
114
|
} catch (e) {
|
|
72
115
|
error = e instanceof Error ? e.message : String(e);
|
|
@@ -101,7 +144,7 @@
|
|
|
101
144
|
}
|
|
102
145
|
}
|
|
103
146
|
|
|
104
|
-
async function onUpload(ev: Event, role: 'hero' | 'background') {
|
|
147
|
+
async function onUpload(ev: Event, role: 'hero' | 'background' | 'logo') {
|
|
105
148
|
const input = ev.currentTarget as HTMLInputElement;
|
|
106
149
|
const file = input.files?.[0];
|
|
107
150
|
if (!file) return;
|
|
@@ -130,6 +173,144 @@
|
|
|
130
173
|
input.value = '';
|
|
131
174
|
}
|
|
132
175
|
}
|
|
176
|
+
|
|
177
|
+
function normalizeHost(host: string) {
|
|
178
|
+
return host.trim().replace(/\/+$/, '').toLowerCase();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function serverOptions(): DiscoveredServer[] {
|
|
182
|
+
const map = new Map<string, DiscoveredServer>();
|
|
183
|
+
if (health?.ollama?.host) {
|
|
184
|
+
map.set(normalizeHost(health.ollama.host), {
|
|
185
|
+
label: `OLLAMA_HOST (${health.ollama.host})`,
|
|
186
|
+
endpoint: health.ollama.host.replace(/\/+$/, ''),
|
|
187
|
+
source: 'env',
|
|
188
|
+
self: true,
|
|
189
|
+
models: health.ollama.models ?? []
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
for (const s of discovered) {
|
|
193
|
+
map.set(normalizeHost(s.endpoint), s);
|
|
194
|
+
}
|
|
195
|
+
return [...map.values()];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function modelsForHost(host: string): string[] {
|
|
199
|
+
const hit = serverOptions().find((s) => normalizeHost(s.endpoint) === normalizeHost(host));
|
|
200
|
+
return hit?.models ?? health?.ollama?.models ?? [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function pickModel(models: string[], preferred: string) {
|
|
204
|
+
if (preferred && models.includes(preferred)) return preferred;
|
|
205
|
+
return models[0] || preferred || '';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function onHostChange() {
|
|
209
|
+
selectedModel = pickModel(modelsForHost(selectedHost), selectedModel || health?.ollama?.model || '');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function hostIsReady() {
|
|
213
|
+
if (modelsForHost(selectedHost).length) return true;
|
|
214
|
+
if (!health) return false;
|
|
215
|
+
return (
|
|
216
|
+
health.ollama.available &&
|
|
217
|
+
normalizeHost(selectedHost || health.ollama.host) === normalizeHost(health.ollama.host)
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function runScan() {
|
|
222
|
+
scanning = true;
|
|
223
|
+
error = '';
|
|
224
|
+
scanNote = includeLan ? 'Scanning LAN + known hosts…' : 'Scanning known hosts / Tailscale…';
|
|
225
|
+
try {
|
|
226
|
+
const data = await api('/scan', {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
body: JSON.stringify({ lan: includeLan })
|
|
229
|
+
});
|
|
230
|
+
discovered = Array.isArray(data.servers) ? data.servers : [];
|
|
231
|
+
if (data.error) {
|
|
232
|
+
scanNote = data.error;
|
|
233
|
+
} else if (!discovered.length) {
|
|
234
|
+
scanNote =
|
|
235
|
+
'No Ollama servers found. Add hosts in ~/.ollanet/config.json, set OLLANET_HOSTS, or enable LAN.';
|
|
236
|
+
} else {
|
|
237
|
+
const sources = Array.isArray(data.sources) ? data.sources.join(', ') : 'scan';
|
|
238
|
+
scanNote = `Found ${discovered.length} server${discovered.length === 1 ? '' : 's'} (${sources}).`;
|
|
239
|
+
}
|
|
240
|
+
const options = serverOptions();
|
|
241
|
+
if (!options.some((s) => normalizeHost(s.endpoint) === normalizeHost(selectedHost))) {
|
|
242
|
+
selectedHost = options[0]?.endpoint || selectedHost;
|
|
243
|
+
}
|
|
244
|
+
onHostChange();
|
|
245
|
+
} catch (e) {
|
|
246
|
+
error = e instanceof Error ? e.message : String(e);
|
|
247
|
+
scanNote = '';
|
|
248
|
+
} finally {
|
|
249
|
+
scanning = false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function runInspire() {
|
|
254
|
+
loading = true;
|
|
255
|
+
error = '';
|
|
256
|
+
try {
|
|
257
|
+
await api('/inspire', {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
urls: inspireUrls
|
|
261
|
+
.split(/\n+/)
|
|
262
|
+
.map((s) => s.trim())
|
|
263
|
+
.filter(Boolean),
|
|
264
|
+
useLlm,
|
|
265
|
+
model: selectedModel || undefined,
|
|
266
|
+
host: selectedHost || undefined
|
|
267
|
+
})
|
|
268
|
+
});
|
|
269
|
+
location.reload();
|
|
270
|
+
} catch (e) {
|
|
271
|
+
error = e instanceof Error ? e.message : String(e);
|
|
272
|
+
loading = false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function runRefine() {
|
|
277
|
+
loading = true;
|
|
278
|
+
error = '';
|
|
279
|
+
try {
|
|
280
|
+
await api('/refine', {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
body: JSON.stringify({
|
|
283
|
+
prompt: refinePrompt,
|
|
284
|
+
model: selectedModel || undefined,
|
|
285
|
+
host: selectedHost || undefined
|
|
286
|
+
})
|
|
287
|
+
});
|
|
288
|
+
location.reload();
|
|
289
|
+
} catch (e) {
|
|
290
|
+
error = e instanceof Error ? e.message : String(e);
|
|
291
|
+
loading = false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function runConfig() {
|
|
296
|
+
loading = true;
|
|
297
|
+
error = '';
|
|
298
|
+
try {
|
|
299
|
+
const patch: Record<string, string | null> = {};
|
|
300
|
+
if (cfgLede.trim()) patch.lede = cfgLede.trim();
|
|
301
|
+
if (cfgTagline.trim()) patch.tagline = cfgTagline.trim();
|
|
302
|
+
if (cfgLogo.trim()) patch.logo = cfgLogo.trim();
|
|
303
|
+
if (!Object.keys(patch).length) throw new Error('Fill at least one config field');
|
|
304
|
+
await api('/config', {
|
|
305
|
+
method: 'POST',
|
|
306
|
+
body: JSON.stringify({ patch })
|
|
307
|
+
});
|
|
308
|
+
location.reload();
|
|
309
|
+
} catch (e) {
|
|
310
|
+
error = e instanceof Error ? e.message : String(e);
|
|
311
|
+
loading = false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
133
314
|
</script>
|
|
134
315
|
|
|
135
316
|
{#if !open}
|
|
@@ -139,7 +320,10 @@
|
|
|
139
320
|
{:else}
|
|
140
321
|
<aside class="genie-panel" aria-label="Genie Mode">
|
|
141
322
|
<header class="genie-head">
|
|
142
|
-
<
|
|
323
|
+
<div>
|
|
324
|
+
<strong>Genie</strong>
|
|
325
|
+
<p class="genie-tagline">Try a look → activate → commit baked files</p>
|
|
326
|
+
</div>
|
|
143
327
|
<button type="button" class="genie-x" onclick={() => (open = false)}>Close</button>
|
|
144
328
|
</header>
|
|
145
329
|
|
|
@@ -148,132 +332,308 @@
|
|
|
148
332
|
{/if}
|
|
149
333
|
|
|
150
334
|
{#if health}
|
|
151
|
-
<
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
{
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
<input
|
|
237
|
-
type="file"
|
|
238
|
-
accept="image/jpeg,image/png,image/webp,image/gif"
|
|
239
|
-
disabled={loading}
|
|
240
|
-
onchange={(e) => onUpload(e, 'hero')}
|
|
241
|
-
/>
|
|
242
|
-
</label>
|
|
243
|
-
<label class="genie-row">
|
|
244
|
-
Page background
|
|
245
|
-
<input
|
|
246
|
-
type="file"
|
|
247
|
-
accept="image/jpeg,image/png,image/webp,image/gif"
|
|
248
|
-
disabled={loading}
|
|
249
|
-
onchange={(e) => onUpload(e, 'background')}
|
|
250
|
-
/>
|
|
251
|
-
</label>
|
|
252
|
-
</section>
|
|
253
|
-
|
|
254
|
-
<section class="genie-sec">
|
|
255
|
-
<h3>Versions</h3>
|
|
256
|
-
<p class="genie-muted">
|
|
257
|
-
Active: {health.active?.versionId ?? '(none)'}
|
|
258
|
-
</p>
|
|
259
|
-
<ul class="genie-versions">
|
|
260
|
-
{#each health.versions as v (v.id)}
|
|
261
|
-
<li class:active={health.active?.versionId === v.id}>
|
|
335
|
+
<div class="genie-shell">
|
|
336
|
+
<nav class="genie-tabs" aria-label="Genie sections">
|
|
337
|
+
{#each TABS as t (t.id)}
|
|
338
|
+
<button
|
|
339
|
+
type="button"
|
|
340
|
+
class="genie-tab"
|
|
341
|
+
class:active={tab === t.id}
|
|
342
|
+
onclick={() => (tab = t.id)}
|
|
343
|
+
title={t.hint}
|
|
344
|
+
>
|
|
345
|
+
<span class="genie-tab-label">{t.label}</span>
|
|
346
|
+
<span class="genie-tab-hint">{t.hint}</span>
|
|
347
|
+
</button>
|
|
348
|
+
{/each}
|
|
349
|
+
</nav>
|
|
350
|
+
|
|
351
|
+
<div class="genie-body">
|
|
352
|
+
{#if tab === 'refine'}
|
|
353
|
+
<section class="genie-sec">
|
|
354
|
+
<h3>Ollama refine</h3>
|
|
355
|
+
<p class="genie-howto">
|
|
356
|
+
Describe the look you want in plain language. Genie asks Ollama — local or another
|
|
357
|
+
server you scan on the network — for a design brief, writes a new version, and
|
|
358
|
+
activates it (page reloads). Undo anytime from <strong>History</strong>.
|
|
359
|
+
</p>
|
|
360
|
+
|
|
361
|
+
<div class="genie-status-pill" class:up={hostIsReady()}>
|
|
362
|
+
{hostIsReady() ? 'Ollama up' : 'Ollama down'}
|
|
363
|
+
{#if selectedHost}
|
|
364
|
+
· {selectedHost.replace(/^https?:\/\//, '')}
|
|
365
|
+
{/if}
|
|
366
|
+
{#if hostIsReady() && selectedModel}
|
|
367
|
+
· {selectedModel}
|
|
368
|
+
{/if}
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
<label class="genie-row">
|
|
372
|
+
Server
|
|
373
|
+
<select bind:value={selectedHost} disabled={loading || scanning} onchange={onHostChange}>
|
|
374
|
+
{#each serverOptions() as s (s.endpoint)}
|
|
375
|
+
<option value={s.endpoint}>{s.label}</option>
|
|
376
|
+
{/each}
|
|
377
|
+
</select>
|
|
378
|
+
</label>
|
|
379
|
+
<div class="genie-scan-row">
|
|
380
|
+
<label class="genie-check genie-check-inline">
|
|
381
|
+
<input type="checkbox" bind:checked={includeLan} disabled={loading || scanning} />
|
|
382
|
+
Include LAN
|
|
383
|
+
</label>
|
|
384
|
+
<button type="button" disabled={loading || scanning} onclick={runScan}>
|
|
385
|
+
{scanning ? 'Scanning…' : 'Scan network'}
|
|
386
|
+
</button>
|
|
387
|
+
</div>
|
|
388
|
+
{#if scanNote}
|
|
389
|
+
<p class="genie-muted">{scanNote}</p>
|
|
390
|
+
{/if}
|
|
391
|
+
|
|
392
|
+
{#if hostIsReady() && modelsForHost(selectedHost).length}
|
|
393
|
+
<label class="genie-row">
|
|
394
|
+
Model
|
|
395
|
+
<select bind:value={selectedModel} disabled={loading || scanning}>
|
|
396
|
+
{#each modelsForHost(selectedHost) as m (m)}
|
|
397
|
+
<option value={m}>{m}</option>
|
|
398
|
+
{/each}
|
|
399
|
+
</select>
|
|
400
|
+
</label>
|
|
401
|
+
{:else if !hostIsReady()}
|
|
402
|
+
<p class="genie-hint">{health.ollama.hint}</p>
|
|
403
|
+
<p class="genie-muted">
|
|
404
|
+
Steers, images, and inspire still work without Ollama — use the other tabs. Or scan
|
|
405
|
+
for a server on Tailscale / LAN.
|
|
406
|
+
</p>
|
|
407
|
+
{:else}
|
|
408
|
+
<p class="genie-hint">{health.ollama.hint}</p>
|
|
409
|
+
{/if}
|
|
410
|
+
|
|
411
|
+
<label class="genie-row">
|
|
412
|
+
Direction
|
|
413
|
+
<textarea
|
|
414
|
+
rows="3"
|
|
415
|
+
bind:value={refinePrompt}
|
|
416
|
+
placeholder="warmer gold accents, denser nav, softer hero, less noise"
|
|
417
|
+
disabled={loading || !hostIsReady()}
|
|
418
|
+
></textarea>
|
|
419
|
+
</label>
|
|
262
420
|
<button
|
|
263
421
|
type="button"
|
|
264
|
-
|
|
265
|
-
|
|
422
|
+
class="genie-primary"
|
|
423
|
+
disabled={loading || scanning || !hostIsReady() || !refinePrompt.trim()}
|
|
424
|
+
onclick={runRefine}
|
|
266
425
|
>
|
|
267
|
-
{
|
|
426
|
+
{loading ? 'Working…' : 'Refine & activate'}
|
|
268
427
|
</button>
|
|
269
|
-
<
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
428
|
+
<p class="genie-muted">
|
|
429
|
+
Tip: set <code>FILEPRESS_OLLAMA_MODEL</code> / <code>OLLAMA_HOST</code> for defaults;
|
|
430
|
+
<a href="https://ollanet.dev" target="_blank" rel="noreferrer">ollanet</a> finds other
|
|
431
|
+
boxes. Finetuna can tune a named variant.
|
|
432
|
+
</p>
|
|
433
|
+
</section>
|
|
434
|
+
{:else if tab === 'look'}
|
|
435
|
+
<section class="genie-sec">
|
|
436
|
+
<h3>Quick steers</h3>
|
|
437
|
+
<p class="genie-howto">
|
|
438
|
+
Instant, no LLM. Each chip or accent change creates a version and reloads.
|
|
439
|
+
</p>
|
|
440
|
+
<label class="genie-row">
|
|
441
|
+
Accent
|
|
442
|
+
<input type="color" bind:value={accent} disabled={loading} />
|
|
443
|
+
<button
|
|
444
|
+
type="button"
|
|
445
|
+
disabled={loading}
|
|
446
|
+
onclick={() =>
|
|
447
|
+
steer(
|
|
448
|
+
{ tokens: { accent, accentStrong: accent } },
|
|
449
|
+
`Accent ${accent}`
|
|
450
|
+
)}
|
|
451
|
+
>
|
|
452
|
+
Apply accent
|
|
453
|
+
</button>
|
|
454
|
+
</label>
|
|
455
|
+
<div class="genie-chips">
|
|
456
|
+
<button
|
|
457
|
+
type="button"
|
|
458
|
+
disabled={loading}
|
|
459
|
+
onclick={() =>
|
|
460
|
+
steer(
|
|
461
|
+
{
|
|
462
|
+
paletteMode: 'dark',
|
|
463
|
+
hero: 'bold',
|
|
464
|
+
atmosphere: 'noise',
|
|
465
|
+
elevatedCards: true,
|
|
466
|
+
navStyle: 'uppercase-tracked',
|
|
467
|
+
density: 'balanced'
|
|
468
|
+
},
|
|
469
|
+
'Dark punchy'
|
|
470
|
+
)}
|
|
471
|
+
>
|
|
472
|
+
Dark punchy
|
|
473
|
+
</button>
|
|
474
|
+
<button
|
|
475
|
+
type="button"
|
|
476
|
+
disabled={loading}
|
|
477
|
+
onclick={() =>
|
|
478
|
+
steer(
|
|
479
|
+
{
|
|
480
|
+
paletteMode: 'light',
|
|
481
|
+
hero: 'editorial',
|
|
482
|
+
atmosphere: 'none',
|
|
483
|
+
elevatedCards: false,
|
|
484
|
+
navStyle: 'soft',
|
|
485
|
+
density: 'sparse'
|
|
486
|
+
},
|
|
487
|
+
'Light editorial'
|
|
488
|
+
)}
|
|
489
|
+
>
|
|
490
|
+
Light editorial
|
|
491
|
+
</button>
|
|
492
|
+
<button
|
|
493
|
+
type="button"
|
|
494
|
+
disabled={loading}
|
|
495
|
+
onclick={() => steer({ density: 'dense' }, 'Denser')}
|
|
496
|
+
>
|
|
497
|
+
Denser
|
|
498
|
+
</button>
|
|
499
|
+
<button
|
|
500
|
+
type="button"
|
|
501
|
+
disabled={loading}
|
|
502
|
+
onclick={() => steer({ hero: 'bold' }, 'Bold hero')}
|
|
503
|
+
>
|
|
504
|
+
Bold hero
|
|
505
|
+
</button>
|
|
506
|
+
</div>
|
|
507
|
+
</section>
|
|
508
|
+
{:else if tab === 'images'}
|
|
509
|
+
<section class="genie-sec">
|
|
510
|
+
<h3>Images</h3>
|
|
511
|
+
<p class="genie-howto">
|
|
512
|
+
Pull a CC stock background from Openverse, or upload a local file. Logo upload
|
|
513
|
+
also sets <code>logo</code> in config.
|
|
514
|
+
</p>
|
|
515
|
+
<label class="genie-row">
|
|
516
|
+
Openverse query
|
|
517
|
+
<input type="text" bind:value={stockQuery} disabled={loading} />
|
|
518
|
+
<button type="button" disabled={loading} onclick={applyStock}>
|
|
519
|
+
Fetch background
|
|
520
|
+
</button>
|
|
521
|
+
</label>
|
|
522
|
+
<label class="genie-row">
|
|
523
|
+
Hero background
|
|
524
|
+
<input
|
|
525
|
+
type="file"
|
|
526
|
+
accept="image/jpeg,image/png,image/webp,image/gif"
|
|
527
|
+
disabled={loading}
|
|
528
|
+
onchange={(e) => onUpload(e, 'hero')}
|
|
529
|
+
/>
|
|
530
|
+
</label>
|
|
531
|
+
<label class="genie-row">
|
|
532
|
+
Page background
|
|
533
|
+
<input
|
|
534
|
+
type="file"
|
|
535
|
+
accept="image/jpeg,image/png,image/webp,image/gif"
|
|
536
|
+
disabled={loading}
|
|
537
|
+
onchange={(e) => onUpload(e, 'background')}
|
|
538
|
+
/>
|
|
539
|
+
</label>
|
|
540
|
+
<label class="genie-row">
|
|
541
|
+
Logo
|
|
542
|
+
<input
|
|
543
|
+
type="file"
|
|
544
|
+
accept="image/jpeg,image/png,image/webp,image/gif,image/svg+xml"
|
|
545
|
+
disabled={loading}
|
|
546
|
+
onchange={(e) => onUpload(e, 'logo')}
|
|
547
|
+
/>
|
|
548
|
+
</label>
|
|
549
|
+
</section>
|
|
550
|
+
{:else if tab === 'inspire'}
|
|
551
|
+
<section class="genie-sec">
|
|
552
|
+
<h3>Inspire from URLs</h3>
|
|
553
|
+
<p class="genie-howto">
|
|
554
|
+
Paste 1–3 public site URLs. Genie crawls them and blends a look. Optionally refine
|
|
555
|
+
with the model selected under <strong>Refine</strong>.
|
|
556
|
+
</p>
|
|
557
|
+
<label class="genie-row">
|
|
558
|
+
URLs (one per line)
|
|
559
|
+
<textarea rows="4" bind:value={inspireUrls} disabled={loading}></textarea>
|
|
560
|
+
</label>
|
|
561
|
+
<label class="genie-check">
|
|
562
|
+
<input type="checkbox" bind:checked={useLlm} disabled={loading} />
|
|
563
|
+
Refine with Ollama when available
|
|
564
|
+
{#if selectedModel}
|
|
565
|
+
<span class="genie-muted">({selectedModel})</span>
|
|
566
|
+
{/if}
|
|
567
|
+
{#if selectedHost}
|
|
568
|
+
<span class="genie-muted">{selectedHost.replace(/^https?:\/\//, '')}</span>
|
|
569
|
+
{/if}
|
|
570
|
+
</label>
|
|
571
|
+
<button type="button" class="genie-primary" disabled={loading} onclick={runInspire}>
|
|
572
|
+
Crawl & apply
|
|
573
|
+
</button>
|
|
574
|
+
</section>
|
|
575
|
+
{:else if tab === 'config'}
|
|
576
|
+
<section class="genie-sec">
|
|
577
|
+
<h3>Site chrome</h3>
|
|
578
|
+
<p class="genie-howto">
|
|
579
|
+
Patches <code>filepress.config.ts</code> on activate (lede, tagline, logo path).
|
|
580
|
+
Leave a field blank to skip it.
|
|
581
|
+
</p>
|
|
582
|
+
<label class="genie-row">
|
|
583
|
+
Lede
|
|
584
|
+
<input type="text" bind:value={cfgLede} disabled={loading} />
|
|
585
|
+
</label>
|
|
586
|
+
<label class="genie-row">
|
|
587
|
+
Tagline
|
|
588
|
+
<input type="text" bind:value={cfgTagline} disabled={loading} />
|
|
589
|
+
</label>
|
|
590
|
+
<label class="genie-row">
|
|
591
|
+
Logo path
|
|
592
|
+
<input
|
|
593
|
+
type="text"
|
|
594
|
+
bind:value={cfgLogo}
|
|
595
|
+
placeholder="/images/logo.svg"
|
|
596
|
+
disabled={loading}
|
|
597
|
+
/>
|
|
598
|
+
</label>
|
|
599
|
+
<button type="button" class="genie-primary" disabled={loading} onclick={runConfig}>
|
|
600
|
+
Apply config
|
|
601
|
+
</button>
|
|
602
|
+
</section>
|
|
603
|
+
{:else}
|
|
604
|
+
<section class="genie-sec">
|
|
605
|
+
<h3>Versions</h3>
|
|
606
|
+
<p class="genie-howto">
|
|
607
|
+
Every Genie action saves a snapshot under <code>.filepress-genie/</code> (gitignored).
|
|
608
|
+
Activate to bake into the working tree; commit when you like it. <code>baseline</code> is
|
|
609
|
+
the pre-Genie look.
|
|
610
|
+
</p>
|
|
611
|
+
<p class="genie-muted">
|
|
612
|
+
Active: {health.active?.versionId ?? '(none)'}
|
|
613
|
+
</p>
|
|
614
|
+
<ul class="genie-versions">
|
|
615
|
+
{#each health.versions as v (v.id)}
|
|
616
|
+
<li class:active={health.active?.versionId === v.id}>
|
|
617
|
+
<button
|
|
618
|
+
type="button"
|
|
619
|
+
disabled={loading || health.active?.versionId === v.id}
|
|
620
|
+
onclick={() => activate(v.id)}
|
|
621
|
+
>
|
|
622
|
+
{v.label}
|
|
623
|
+
</button>
|
|
624
|
+
<span class="genie-muted"
|
|
625
|
+
>{v.id === 'baseline' ? 'baseline' : v.createdAt.slice(11, 19)}</span
|
|
626
|
+
>
|
|
627
|
+
</li>
|
|
628
|
+
{/each}
|
|
629
|
+
</ul>
|
|
630
|
+
<button type="button" class="genie-linkish" disabled={loading} onclick={refresh}>
|
|
631
|
+
Refresh list
|
|
632
|
+
</button>
|
|
633
|
+
</section>
|
|
634
|
+
{/if}
|
|
635
|
+
</div>
|
|
636
|
+
</div>
|
|
277
637
|
{:else if loading}
|
|
278
638
|
<p class="genie-muted">Loading…</p>
|
|
279
639
|
{/if}
|
|
@@ -304,21 +664,33 @@
|
|
|
304
664
|
right: 0;
|
|
305
665
|
bottom: 0;
|
|
306
666
|
z-index: 100000;
|
|
307
|
-
width: min(
|
|
308
|
-
|
|
667
|
+
width: min(28rem, 100vw);
|
|
668
|
+
display: flex;
|
|
669
|
+
flex-direction: column;
|
|
309
670
|
background: color-mix(in srgb, var(--bg, #12121a) 94%, #000);
|
|
310
671
|
color: var(--ink, #eee);
|
|
311
672
|
border-left: 1px solid var(--rule, #333);
|
|
312
|
-
padding:
|
|
673
|
+
padding: 0.85rem 0.75rem 1rem;
|
|
313
674
|
font: 0.9rem/1.45 var(--font-sans, system-ui, sans-serif);
|
|
314
675
|
box-shadow: -12px 0 40px color-mix(in srgb, #000 40%, transparent);
|
|
315
676
|
}
|
|
316
677
|
|
|
317
678
|
.genie-head {
|
|
318
679
|
display: flex;
|
|
319
|
-
align-items:
|
|
680
|
+
align-items: flex-start;
|
|
320
681
|
justify-content: space-between;
|
|
321
|
-
|
|
682
|
+
gap: 0.75rem;
|
|
683
|
+
padding: 0 0.25rem 0.75rem;
|
|
684
|
+
border-bottom: 1px solid var(--rule, #333);
|
|
685
|
+
margin-bottom: 0.75rem;
|
|
686
|
+
flex-shrink: 0;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.genie-tagline {
|
|
690
|
+
margin: 0.2rem 0 0;
|
|
691
|
+
font-size: 0.75rem;
|
|
692
|
+
color: var(--ink-soft, #999);
|
|
693
|
+
font-weight: 400;
|
|
322
694
|
}
|
|
323
695
|
|
|
324
696
|
.genie-x,
|
|
@@ -326,32 +698,111 @@
|
|
|
326
698
|
cursor: pointer;
|
|
327
699
|
}
|
|
328
700
|
|
|
329
|
-
.genie-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
701
|
+
.genie-shell {
|
|
702
|
+
display: grid;
|
|
703
|
+
grid-template-columns: 5.75rem 1fr;
|
|
704
|
+
gap: 0.65rem;
|
|
705
|
+
min-height: 0;
|
|
706
|
+
flex: 1;
|
|
707
|
+
overflow: hidden;
|
|
333
708
|
}
|
|
334
709
|
|
|
335
|
-
.genie-
|
|
336
|
-
|
|
710
|
+
.genie-tabs {
|
|
711
|
+
display: flex;
|
|
712
|
+
flex-direction: column;
|
|
713
|
+
gap: 0.25rem;
|
|
714
|
+
overflow: auto;
|
|
715
|
+
padding-right: 0.15rem;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.genie-tab {
|
|
719
|
+
display: grid;
|
|
720
|
+
gap: 0.1rem;
|
|
721
|
+
text-align: left;
|
|
722
|
+
border: 1px solid transparent;
|
|
723
|
+
background: transparent;
|
|
724
|
+
color: var(--ink-soft, #999);
|
|
725
|
+
border-radius: 8px;
|
|
726
|
+
padding: 0.45rem 0.4rem;
|
|
337
727
|
font-size: 0.72rem;
|
|
338
|
-
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.genie-tab-label {
|
|
731
|
+
font-weight: 700;
|
|
732
|
+
letter-spacing: 0.04em;
|
|
733
|
+
text-transform: uppercase;
|
|
734
|
+
color: inherit;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.genie-tab-hint {
|
|
738
|
+
font-size: 0.65rem;
|
|
739
|
+
opacity: 0.85;
|
|
740
|
+
line-height: 1.2;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.genie-tab:hover {
|
|
744
|
+
border-color: var(--rule, #444);
|
|
745
|
+
color: var(--ink, #eee);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.genie-tab.active {
|
|
749
|
+
border-color: color-mix(in srgb, var(--accent, #f0c040) 55%, var(--rule, #444));
|
|
750
|
+
background: color-mix(in srgb, var(--accent, #f0c040) 12%, transparent);
|
|
751
|
+
color: var(--accent, #f0c040);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.genie-body {
|
|
755
|
+
overflow: auto;
|
|
756
|
+
padding: 0 0.15rem 1rem 0.35rem;
|
|
757
|
+
border-left: 1px solid var(--rule, #333);
|
|
758
|
+
min-width: 0;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.genie-sec h3 {
|
|
762
|
+
margin: 0 0 0.45rem;
|
|
763
|
+
font-size: 0.78rem;
|
|
764
|
+
letter-spacing: 0.08em;
|
|
339
765
|
text-transform: uppercase;
|
|
340
766
|
color: var(--ink-soft, #999);
|
|
341
767
|
font-weight: 600;
|
|
342
768
|
}
|
|
343
769
|
|
|
770
|
+
.genie-howto {
|
|
771
|
+
font-size: 0.8rem;
|
|
772
|
+
color: var(--ink-soft, #bbb);
|
|
773
|
+
line-height: 1.45;
|
|
774
|
+
margin: 0 0 0.85rem;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.genie-status-pill {
|
|
778
|
+
display: inline-block;
|
|
779
|
+
font-size: 0.72rem;
|
|
780
|
+
font-weight: 600;
|
|
781
|
+
letter-spacing: 0.04em;
|
|
782
|
+
text-transform: uppercase;
|
|
783
|
+
padding: 0.25rem 0.55rem;
|
|
784
|
+
border-radius: 999px;
|
|
785
|
+
border: 1px solid var(--rule, #444);
|
|
786
|
+
color: var(--ink-soft, #999);
|
|
787
|
+
margin-bottom: 0.75rem;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.genie-status-pill.up {
|
|
791
|
+
border-color: color-mix(in srgb, #3a8 50%, var(--rule));
|
|
792
|
+
color: #7dca9a;
|
|
793
|
+
}
|
|
794
|
+
|
|
344
795
|
.genie-muted {
|
|
345
796
|
color: var(--ink-soft, #999);
|
|
346
797
|
font-size: 0.82rem;
|
|
347
|
-
margin: 0.
|
|
798
|
+
margin: 0.35rem 0;
|
|
348
799
|
}
|
|
349
800
|
|
|
350
801
|
.genie-hint {
|
|
351
802
|
font-size: 0.78rem;
|
|
352
803
|
color: var(--ink-soft, #aaa);
|
|
353
804
|
line-height: 1.4;
|
|
354
|
-
margin: 0.4rem 0 0;
|
|
805
|
+
margin: 0.4rem 0 0.75rem;
|
|
355
806
|
word-break: break-word;
|
|
356
807
|
}
|
|
357
808
|
|
|
@@ -361,6 +812,8 @@
|
|
|
361
812
|
padding: 0.5rem 0.65rem;
|
|
362
813
|
border-radius: 6px;
|
|
363
814
|
font-size: 0.82rem;
|
|
815
|
+
margin: 0 0.25rem 0.65rem;
|
|
816
|
+
flex-shrink: 0;
|
|
364
817
|
}
|
|
365
818
|
|
|
366
819
|
.genie-row {
|
|
@@ -371,8 +824,32 @@
|
|
|
371
824
|
}
|
|
372
825
|
|
|
373
826
|
.genie-row input[type='text'],
|
|
374
|
-
.genie-row input[type='file']
|
|
827
|
+
.genie-row input[type='file'],
|
|
828
|
+
.genie-row textarea,
|
|
829
|
+
.genie-row select {
|
|
375
830
|
width: 100%;
|
|
831
|
+
box-sizing: border-box;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.genie-check {
|
|
835
|
+
display: flex;
|
|
836
|
+
flex-wrap: wrap;
|
|
837
|
+
align-items: center;
|
|
838
|
+
gap: 0.45rem;
|
|
839
|
+
font-size: 0.8rem;
|
|
840
|
+
margin-bottom: 0.65rem;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.genie-scan-row {
|
|
844
|
+
display: flex;
|
|
845
|
+
flex-wrap: wrap;
|
|
846
|
+
align-items: center;
|
|
847
|
+
gap: 0.5rem;
|
|
848
|
+
margin-bottom: 0.65rem;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.genie-check-inline {
|
|
852
|
+
margin-bottom: 0;
|
|
376
853
|
}
|
|
377
854
|
|
|
378
855
|
.genie-chips {
|
|
@@ -383,21 +860,39 @@
|
|
|
383
860
|
|
|
384
861
|
.genie-chips button,
|
|
385
862
|
.genie-row button,
|
|
386
|
-
.genie-versions button
|
|
863
|
+
.genie-versions button,
|
|
864
|
+
.genie-primary {
|
|
387
865
|
border: 1px solid var(--rule, #444);
|
|
388
866
|
background: var(--surface, #1c1c22);
|
|
389
867
|
color: var(--ink, #eee);
|
|
390
868
|
border-radius: 6px;
|
|
391
|
-
padding: 0.
|
|
869
|
+
padding: 0.4rem 0.65rem;
|
|
392
870
|
font-size: 0.78rem;
|
|
393
871
|
}
|
|
394
872
|
|
|
873
|
+
.genie-primary {
|
|
874
|
+
width: 100%;
|
|
875
|
+
margin-top: 0.25rem;
|
|
876
|
+
border-color: color-mix(in srgb, var(--accent, #f0c040) 50%, var(--rule));
|
|
877
|
+
background: color-mix(in srgb, var(--accent, #f0c040) 16%, var(--surface, #1c1c22));
|
|
878
|
+
color: var(--accent, #f0c040);
|
|
879
|
+
font-weight: 700;
|
|
880
|
+
}
|
|
881
|
+
|
|
395
882
|
.genie-chips button:hover,
|
|
396
883
|
.genie-row button:hover,
|
|
397
|
-
.genie-versions button:hover
|
|
884
|
+
.genie-versions button:hover,
|
|
885
|
+
.genie-primary:hover:not(:disabled) {
|
|
398
886
|
border-color: var(--accent, #f0c040);
|
|
399
887
|
}
|
|
400
888
|
|
|
889
|
+
.genie-primary:disabled,
|
|
890
|
+
.genie-chips button:disabled,
|
|
891
|
+
.genie-row button:disabled {
|
|
892
|
+
opacity: 0.45;
|
|
893
|
+
cursor: not-allowed;
|
|
894
|
+
}
|
|
895
|
+
|
|
401
896
|
.genie-versions {
|
|
402
897
|
list-style: none;
|
|
403
898
|
margin: 0.5rem 0;
|
|
@@ -426,4 +921,37 @@
|
|
|
426
921
|
font-size: 0.8rem;
|
|
427
922
|
text-decoration: underline;
|
|
428
923
|
}
|
|
924
|
+
|
|
925
|
+
.genie-howto code,
|
|
926
|
+
.genie-muted code {
|
|
927
|
+
font-size: 0.85em;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
@media (max-width: 28rem) {
|
|
931
|
+
.genie-shell {
|
|
932
|
+
grid-template-columns: 1fr;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.genie-tabs {
|
|
936
|
+
flex-direction: row;
|
|
937
|
+
flex-wrap: wrap;
|
|
938
|
+
border-bottom: 1px solid var(--rule, #333);
|
|
939
|
+
padding-bottom: 0.5rem;
|
|
940
|
+
margin-bottom: 0.25rem;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
.genie-tab {
|
|
944
|
+
flex: 1 1 auto;
|
|
945
|
+
min-width: 4.5rem;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
.genie-tab-hint {
|
|
949
|
+
display: none;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
.genie-body {
|
|
953
|
+
border-left: none;
|
|
954
|
+
padding-left: 0;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
429
957
|
</style>
|