@gmickel/gno 0.29.1 → 0.30.0
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 +17 -0
- package/package.json +1 -1
- package/src/cli/commands/ask.ts +1 -0
- package/src/cli/commands/completion/scripts.ts +2 -0
- package/src/cli/commands/daemon.ts +159 -0
- package/src/cli/commands/get.ts +4 -1
- package/src/cli/commands/graph.ts +4 -1
- package/src/cli/commands/links.ts +12 -3
- package/src/cli/commands/ls.ts +4 -1
- package/src/cli/commands/multi-get.ts +4 -1
- package/src/cli/commands/query.ts +1 -0
- package/src/cli/commands/search.ts +1 -0
- package/src/cli/commands/shared.ts +6 -0
- package/src/cli/commands/vsearch.ts +1 -0
- package/src/cli/program.ts +26 -0
- package/src/serve/background-runtime.ts +219 -0
- package/src/serve/context.ts +12 -4
- package/src/serve/index.ts +6 -0
- package/src/serve/public/app.tsx +14 -11
- package/src/serve/public/components/WorkspaceTabs.tsx +13 -7
- package/src/serve/public/globals.css +113 -10
- package/src/serve/public/pages/Ask.tsx +15 -6
- package/src/serve/public/pages/Collections.tsx +1 -1
- package/src/serve/public/pages/Dashboard.tsx +27 -19
- package/src/serve/public/pages/Search.tsx +1 -1
- package/src/serve/server.ts +20 -85
- package/src/serve/watch-service.ts +47 -5
- package/src/store/migrations/runner.ts +12 -2
- package/src/store/sqlite/adapter.ts +22 -1
package/src/serve/public/app.tsx
CHANGED
|
@@ -144,45 +144,48 @@ function AppContent({
|
|
|
144
144
|
<div className="flex-1">
|
|
145
145
|
<Page key={location} navigate={navigate} />
|
|
146
146
|
</div>
|
|
147
|
-
<footer className="border-t border-border/
|
|
148
|
-
<div className="
|
|
147
|
+
<footer className="border-t border-border/30 bg-background/60 py-6 text-center text-sm backdrop-blur-sm">
|
|
148
|
+
<div className="ornament mx-auto mb-4 max-w-[8rem] text-muted-foreground/20">
|
|
149
|
+
<span className="text-[10px]">◆</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="flex items-center justify-center gap-5 text-muted-foreground/60">
|
|
149
152
|
<button
|
|
150
|
-
className="transition-colors hover:text-
|
|
153
|
+
className="transition-colors duration-300 hover:text-primary"
|
|
151
154
|
onClick={() => navigate("/collections")}
|
|
152
155
|
type="button"
|
|
153
156
|
>
|
|
154
157
|
Collections
|
|
155
158
|
</button>
|
|
156
|
-
<span className="text-border"
|
|
159
|
+
<span className="text-border/30">—</span>
|
|
157
160
|
<a
|
|
158
|
-
className="transition-colors hover:text-
|
|
161
|
+
className="transition-colors duration-300 hover:text-primary"
|
|
159
162
|
href="https://github.com/gmickel/gno"
|
|
160
163
|
rel="noopener noreferrer"
|
|
161
164
|
target="_blank"
|
|
162
165
|
>
|
|
163
166
|
GitHub
|
|
164
167
|
</a>
|
|
165
|
-
<span className="text-border"
|
|
168
|
+
<span className="text-border/30">—</span>
|
|
166
169
|
<a
|
|
167
|
-
className="transition-colors hover:text-
|
|
170
|
+
className="transition-colors duration-300 hover:text-primary"
|
|
168
171
|
href="https://discord.gg/nHEmyJB5tg"
|
|
169
172
|
rel="noopener noreferrer"
|
|
170
173
|
target="_blank"
|
|
171
174
|
>
|
|
172
175
|
Discord
|
|
173
176
|
</a>
|
|
174
|
-
<span className="text-border"
|
|
177
|
+
<span className="text-border/30">—</span>
|
|
175
178
|
<a
|
|
176
|
-
className="transition-colors hover:text-
|
|
179
|
+
className="transition-colors duration-300 hover:text-primary"
|
|
177
180
|
href="https://gno.sh"
|
|
178
181
|
rel="noopener noreferrer"
|
|
179
182
|
target="_blank"
|
|
180
183
|
>
|
|
181
184
|
gno.sh
|
|
182
185
|
</a>
|
|
183
|
-
<span className="text-border"
|
|
186
|
+
<span className="text-border/30">—</span>
|
|
184
187
|
<a
|
|
185
|
-
className="transition-colors hover:text-
|
|
188
|
+
className="transition-colors duration-300 hover:text-primary"
|
|
186
189
|
href="https://twitter.com/gmickel"
|
|
187
190
|
rel="noopener noreferrer"
|
|
188
191
|
target="_blank"
|
|
@@ -20,16 +20,16 @@ export function WorkspaceTabs({
|
|
|
20
20
|
onNewTab,
|
|
21
21
|
}: WorkspaceTabsProps) {
|
|
22
22
|
return (
|
|
23
|
-
<div className="border-border/
|
|
24
|
-
<div className="mx-auto flex max-w-7xl items-center gap-
|
|
23
|
+
<div className="border-border/40 border-b bg-background/95 backdrop-blur-sm">
|
|
24
|
+
<div className="mx-auto flex max-w-7xl items-center gap-1.5 overflow-x-auto px-4 py-2">
|
|
25
25
|
{tabs.map((tab) => {
|
|
26
26
|
const active = tab.id === activeTabId;
|
|
27
27
|
return (
|
|
28
28
|
<div
|
|
29
|
-
className={`flex items-center rounded-lg border px-2 py-1 ${
|
|
29
|
+
className={`group flex items-center rounded-lg border px-2 py-1 transition-all duration-200 ${
|
|
30
30
|
active
|
|
31
|
-
? "border-primary/40 bg-primary/10 text-primary"
|
|
32
|
-
: "border-border/60 bg-card/70"
|
|
31
|
+
? "border-primary/40 bg-primary/10 text-primary shadow-[0_0_12px_-4px_hsl(var(--primary)/0.25)]"
|
|
32
|
+
: "border-border/30 bg-card/40 hover:border-border/60 hover:bg-card/70"
|
|
33
33
|
}`}
|
|
34
34
|
key={tab.id}
|
|
35
35
|
>
|
|
@@ -41,6 +41,7 @@ export function WorkspaceTabs({
|
|
|
41
41
|
{tab.label}
|
|
42
42
|
</button>
|
|
43
43
|
<Button
|
|
44
|
+
className="opacity-50 transition-opacity group-hover:opacity-100"
|
|
44
45
|
onClick={() => onClose(tab.id)}
|
|
45
46
|
size="icon-sm"
|
|
46
47
|
variant="ghost"
|
|
@@ -50,8 +51,13 @@ export function WorkspaceTabs({
|
|
|
50
51
|
</div>
|
|
51
52
|
);
|
|
52
53
|
})}
|
|
53
|
-
<Button
|
|
54
|
-
|
|
54
|
+
<Button
|
|
55
|
+
className="border-dashed"
|
|
56
|
+
onClick={onNewTab}
|
|
57
|
+
size="sm"
|
|
58
|
+
variant="outline"
|
|
59
|
+
>
|
|
60
|
+
<PlusIcon className="mr-1.5 size-3.5" />
|
|
55
61
|
New Tab
|
|
56
62
|
</Button>
|
|
57
63
|
</div>
|
|
@@ -98,16 +98,25 @@ body {
|
|
|
98
98
|
margin: 0;
|
|
99
99
|
min-height: 100vh;
|
|
100
100
|
|
|
101
|
-
/*
|
|
101
|
+
/* Layered background: teal radial glow + refined grid */
|
|
102
102
|
background-image:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
radial-gradient(
|
|
104
|
+
ellipse at 50% 0%,
|
|
105
|
+
hsl(var(--primary) / 0.05) 0%,
|
|
106
|
+
transparent 50%
|
|
107
|
+
),
|
|
108
|
+
linear-gradient(hsl(var(--foreground) / 0.018) 1px, transparent 1px),
|
|
109
|
+
linear-gradient(90deg, hsl(var(--foreground) / 0.018) 1px, transparent 1px);
|
|
110
|
+
background-size:
|
|
111
|
+
100% 100%,
|
|
112
|
+
32px 32px,
|
|
113
|
+
32px 32px;
|
|
114
|
+
background-attachment: fixed, scroll, scroll;
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
::selection {
|
|
109
|
-
background-color: hsl(var(--primary) / 0.
|
|
110
|
-
color: hsl(var(--
|
|
118
|
+
background-color: hsl(var(--primary) / 0.25);
|
|
119
|
+
color: hsl(var(--foreground));
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
/* Typography */
|
|
@@ -117,11 +126,14 @@ h3,
|
|
|
117
126
|
h4,
|
|
118
127
|
h5,
|
|
119
128
|
h6 {
|
|
120
|
-
/*
|
|
121
|
-
font-family:
|
|
122
|
-
|
|
129
|
+
/* Distinctive serif stack - offline-safe system fonts */
|
|
130
|
+
font-family:
|
|
131
|
+
"Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia,
|
|
132
|
+
serif;
|
|
133
|
+
line-height: 1.2;
|
|
123
134
|
font-weight: 600;
|
|
124
|
-
letter-spacing: -0.
|
|
135
|
+
letter-spacing: -0.02em;
|
|
136
|
+
text-wrap: balance;
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
code,
|
|
@@ -208,6 +220,44 @@ mark {
|
|
|
208
220
|
.stagger-4 {
|
|
209
221
|
animation-delay: 0.4s;
|
|
210
222
|
}
|
|
223
|
+
.stagger-5 {
|
|
224
|
+
animation-delay: 0.5s;
|
|
225
|
+
}
|
|
226
|
+
.stagger-6 {
|
|
227
|
+
animation-delay: 0.6s;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Slide-up reveal with spring-like ease */
|
|
231
|
+
@keyframes slide-up {
|
|
232
|
+
from {
|
|
233
|
+
opacity: 0;
|
|
234
|
+
transform: translateY(16px);
|
|
235
|
+
}
|
|
236
|
+
to {
|
|
237
|
+
opacity: 1;
|
|
238
|
+
transform: translateY(0);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.animate-slide-up {
|
|
243
|
+
animation: slide-up 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* Scale entrance */
|
|
247
|
+
@keyframes scale-in {
|
|
248
|
+
from {
|
|
249
|
+
opacity: 0;
|
|
250
|
+
transform: scale(0.96);
|
|
251
|
+
}
|
|
252
|
+
to {
|
|
253
|
+
opacity: 1;
|
|
254
|
+
transform: scale(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.animate-scale-in {
|
|
259
|
+
animation: scale-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
|
260
|
+
}
|
|
211
261
|
|
|
212
262
|
/* Collapsible animations */
|
|
213
263
|
@keyframes collapse-down {
|
|
@@ -283,6 +333,59 @@ mark {
|
|
|
283
333
|
box-shadow: 0 8px 30px -10px hsl(var(--primary) / 0.2);
|
|
284
334
|
}
|
|
285
335
|
|
|
336
|
+
/* Text glow for hero / display elements */
|
|
337
|
+
.glow-text {
|
|
338
|
+
text-shadow:
|
|
339
|
+
0 0 40px hsl(var(--primary) / 0.3),
|
|
340
|
+
0 0 80px hsl(var(--primary) / 0.15);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
[data-theme="light"] .glow-text {
|
|
344
|
+
text-shadow: none;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* Decorative ornamental divider */
|
|
348
|
+
.ornament {
|
|
349
|
+
display: flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
gap: 0.75rem;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.ornament::before,
|
|
355
|
+
.ornament::after {
|
|
356
|
+
content: "";
|
|
357
|
+
flex: 1;
|
|
358
|
+
height: 1px;
|
|
359
|
+
background: linear-gradient(
|
|
360
|
+
90deg,
|
|
361
|
+
transparent,
|
|
362
|
+
hsl(var(--border)),
|
|
363
|
+
transparent
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* Warm glass variant with secondary/gold accent. Reserved for future cards. */
|
|
368
|
+
.glass-warm {
|
|
369
|
+
background: hsl(var(--card) / 0.8);
|
|
370
|
+
backdrop-filter: blur(16px) saturate(150%);
|
|
371
|
+
-webkit-backdrop-filter: blur(16px) saturate(150%);
|
|
372
|
+
border: 1px solid hsl(var(--secondary) / 0.12);
|
|
373
|
+
box-shadow: 0 0 30px -10px hsl(var(--secondary) / 0.08);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* Vignette overlay for atmospheric depth. Requires position: relative on host. */
|
|
377
|
+
.vignette::after {
|
|
378
|
+
content: "";
|
|
379
|
+
position: absolute;
|
|
380
|
+
inset: 0;
|
|
381
|
+
pointer-events: none;
|
|
382
|
+
background: radial-gradient(
|
|
383
|
+
ellipse at center,
|
|
384
|
+
transparent 50%,
|
|
385
|
+
hsl(var(--background) / 0.4) 100%
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
286
389
|
/* Focus ring with glow */
|
|
287
390
|
.focus-glow:focus-visible {
|
|
288
391
|
outline: 2px solid hsl(var(--primary));
|
|
@@ -783,21 +783,30 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
783
783
|
)}
|
|
784
784
|
|
|
785
785
|
{conversation.length === 0 && (
|
|
786
|
-
<div className="py-
|
|
787
|
-
<
|
|
788
|
-
|
|
789
|
-
|
|
786
|
+
<div className="animate-scale-in py-14 text-center opacity-0 md:py-20">
|
|
787
|
+
<div className="relative mx-auto mb-6 size-16">
|
|
788
|
+
<div className="absolute inset-0 animate-pulse-glow rounded-full bg-primary/10" />
|
|
789
|
+
<Sparkles className="absolute inset-0 m-auto size-8 text-primary/70" />
|
|
790
|
+
</div>
|
|
791
|
+
<h2 className="mb-2 text-xl">Ask anything</h2>
|
|
792
|
+
<p className="mx-auto max-w-sm text-muted-foreground">
|
|
790
793
|
{answerAvailable
|
|
791
794
|
? "Get AI-powered answers with citations from your documents"
|
|
792
795
|
: "AI answers not available. Install a generation model to enable."}
|
|
793
796
|
</p>
|
|
797
|
+
<div
|
|
798
|
+
aria-hidden="true"
|
|
799
|
+
className="ornament mx-auto mt-6 max-w-[8rem] text-muted-foreground/20"
|
|
800
|
+
>
|
|
801
|
+
<span className="text-[10px]">◆</span>
|
|
802
|
+
</div>
|
|
794
803
|
</div>
|
|
795
804
|
)}
|
|
796
805
|
|
|
797
806
|
{conversation.map((entry) => (
|
|
798
807
|
<div className="space-y-4" key={entry.id}>
|
|
799
808
|
<div className="flex justify-end">
|
|
800
|
-
<div className="max-w-[80%] rounded-
|
|
809
|
+
<div className="max-w-[80%] rounded-xl bg-secondary px-5 py-3 shadow-[0_0_20px_-8px_hsl(var(--secondary)/0.3)]">
|
|
801
810
|
<p className="text-foreground">{entry.query}</p>
|
|
802
811
|
</div>
|
|
803
812
|
</div>
|
|
@@ -823,7 +832,7 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
823
832
|
{entry.response && (
|
|
824
833
|
<>
|
|
825
834
|
{entry.response.answer && (
|
|
826
|
-
<div className="prose prose-sm prose-invert max-w-none rounded-lg bg-card/
|
|
835
|
+
<div className="prose prose-sm prose-invert max-w-none rounded-lg border border-border/30 bg-card/60 p-5 shadow-[0_0_30px_-10px_hsl(var(--primary)/0.08)]">
|
|
827
836
|
<p className="whitespace-pre-wrap leading-relaxed">
|
|
828
837
|
{renderAnswer(
|
|
829
838
|
entry.response.answer,
|
|
@@ -129,7 +129,7 @@ function CollectionCard({
|
|
|
129
129
|
|
|
130
130
|
return (
|
|
131
131
|
<Card
|
|
132
|
-
className="group relative cursor-pointer overflow-hidden transition-all hover:border-primary/30 hover:bg-card/90"
|
|
132
|
+
className="group relative cursor-pointer overflow-hidden transition-all duration-300 hover:border-primary/30 hover:bg-card/90 hover:shadow-[0_0_30px_-12px_hsl(var(--primary)/0.15)]"
|
|
133
133
|
onClick={onBrowse}
|
|
134
134
|
onKeyDown={(event) => {
|
|
135
135
|
if (event.key === "Enter" || event.key === " ") {
|
|
@@ -373,20 +373,26 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
373
373
|
</div>
|
|
374
374
|
)}
|
|
375
375
|
|
|
376
|
-
<header className="relative border-border/50 border-b bg-card/50 backdrop-blur-sm">
|
|
377
|
-
<div className="aurora-glow absolute inset-0 opacity-
|
|
378
|
-
<div className="relative px-8 py-
|
|
379
|
-
<div className="grid gap-
|
|
376
|
+
<header className="relative overflow-hidden border-border/50 border-b bg-card/50 backdrop-blur-sm">
|
|
377
|
+
<div className="aurora-glow absolute inset-0 opacity-40" />
|
|
378
|
+
<div className="relative px-8 py-14 md:py-16">
|
|
379
|
+
<div className="grid gap-8 md:grid-cols-[minmax(0,1fr)_auto] md:items-center">
|
|
380
380
|
<div className="min-w-0">
|
|
381
|
-
<div className="flex items-center gap-
|
|
382
|
-
<GnoLogo className="size-
|
|
383
|
-
<h1 className="font-bold text-
|
|
381
|
+
<div className="flex items-center gap-4">
|
|
382
|
+
<GnoLogo className="size-10 shrink-0 text-primary" />
|
|
383
|
+
<h1 className="glow-text font-bold text-5xl text-primary tracking-tighter">
|
|
384
384
|
GNO
|
|
385
385
|
</h1>
|
|
386
386
|
</div>
|
|
387
|
-
<p className="mt-
|
|
387
|
+
<p className="mt-3 text-lg tracking-wide text-muted-foreground">
|
|
388
388
|
Your Local Knowledge Index
|
|
389
389
|
</p>
|
|
390
|
+
<div
|
|
391
|
+
aria-hidden="true"
|
|
392
|
+
className="ornament mt-5 max-w-[12rem] text-muted-foreground/30"
|
|
393
|
+
>
|
|
394
|
+
<span className="text-xs">✦</span>
|
|
395
|
+
</div>
|
|
390
396
|
</div>
|
|
391
397
|
|
|
392
398
|
<div className="flex flex-wrap items-center gap-3 md:justify-end">
|
|
@@ -441,7 +447,7 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
441
447
|
|
|
442
448
|
<nav className="mb-10 flex flex-wrap gap-4">
|
|
443
449
|
<Button
|
|
444
|
-
className="gap-2"
|
|
450
|
+
className="gap-2 shadow-[0_0_24px_-6px_hsl(var(--primary)/0.4)]"
|
|
445
451
|
onClick={() => navigate("/search")}
|
|
446
452
|
size="lg"
|
|
447
453
|
>
|
|
@@ -449,7 +455,7 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
449
455
|
Search
|
|
450
456
|
</Button>
|
|
451
457
|
<Button
|
|
452
|
-
className="gap-2"
|
|
458
|
+
className="gap-2 shadow-[0_0_24px_-6px_hsl(var(--secondary)/0.4)]"
|
|
453
459
|
onClick={() => navigate("/ask")}
|
|
454
460
|
size="lg"
|
|
455
461
|
variant="secondary"
|
|
@@ -606,9 +612,9 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
606
612
|
)}
|
|
607
613
|
|
|
608
614
|
{status && (
|
|
609
|
-
<div className="mb-10 grid animate-
|
|
610
|
-
<Card className="group relative overflow-hidden border-primary/30 bg-gradient-to-br from-primary/10 via-primary/5 to-transparent transition-all duration-300 hover:border-primary/50 hover:shadow-[
|
|
611
|
-
<div className="pointer-events-none absolute -top-12 -right-12 size-
|
|
615
|
+
<div className="mb-10 grid animate-slide-up gap-6 opacity-0 md:grid-cols-4">
|
|
616
|
+
<Card className="group relative overflow-hidden border-primary/30 bg-gradient-to-br from-primary/10 via-primary/5 to-transparent transition-all duration-300 hover:border-primary/50 hover:shadow-[0_0_40px_-10px_hsl(var(--primary)/0.35)]">
|
|
617
|
+
<div className="pointer-events-none absolute -top-12 -right-12 size-36 rounded-full bg-primary/10 blur-3xl transition-all duration-500 group-hover:bg-primary/15" />
|
|
612
618
|
<CardHeader className="relative pb-2">
|
|
613
619
|
<CardDescription className="flex items-center gap-2 text-primary/80">
|
|
614
620
|
<Database className="size-4" />
|
|
@@ -616,7 +622,7 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
616
622
|
</CardDescription>
|
|
617
623
|
</CardHeader>
|
|
618
624
|
<CardContent className="relative">
|
|
619
|
-
<div className="font-bold text-5xl tracking-tight text-primary">
|
|
625
|
+
<div className="glow-text font-bold text-5xl tracking-tight text-primary">
|
|
620
626
|
{status.totalDocuments.toLocaleString()}
|
|
621
627
|
</div>
|
|
622
628
|
<p className="mt-1 text-muted-foreground text-sm">
|
|
@@ -625,7 +631,7 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
625
631
|
</CardContent>
|
|
626
632
|
</Card>
|
|
627
633
|
|
|
628
|
-
<Card className="group stagger-1 animate-
|
|
634
|
+
<Card className="group stagger-1 animate-slide-up opacity-0 transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/50 hover:shadow-lg">
|
|
629
635
|
<CardHeader className="pb-2">
|
|
630
636
|
<CardDescription className="flex items-center gap-2">
|
|
631
637
|
<Layers className="size-4" />
|
|
@@ -640,7 +646,7 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
640
646
|
</Card>
|
|
641
647
|
|
|
642
648
|
<Card
|
|
643
|
-
className="group stagger-2 animate-
|
|
649
|
+
className="group stagger-2 animate-slide-up cursor-pointer opacity-0 transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/50 hover:shadow-lg"
|
|
644
650
|
onClick={openCollections}
|
|
645
651
|
onKeyDown={(event) => {
|
|
646
652
|
if (event.key === "Enter" || event.key === " ") {
|
|
@@ -673,7 +679,7 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
673
679
|
</Card>
|
|
674
680
|
|
|
675
681
|
<Card
|
|
676
|
-
className="group stagger-3 animate-
|
|
682
|
+
className="group stagger-3 animate-slide-up cursor-pointer opacity-0 transition-all duration-200 hover:-translate-y-0.5 hover:border-secondary/50 hover:bg-secondary/5 hover:shadow-[0_0_30px_-10px_hsl(var(--secondary)/0.25)]"
|
|
677
683
|
onClick={() => openCapture()}
|
|
678
684
|
>
|
|
679
685
|
<CardHeader className="pb-2">
|
|
@@ -697,9 +703,11 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
697
703
|
)}
|
|
698
704
|
|
|
699
705
|
{status && status.collections.length > 0 && (
|
|
700
|
-
<section className="stagger-
|
|
706
|
+
<section className="stagger-4 animate-slide-up opacity-0">
|
|
701
707
|
<div className="mb-6 flex items-center justify-between gap-4 border-border/50 border-b pb-3">
|
|
702
|
-
<h2 className="font-semibold text-2xl">
|
|
708
|
+
<h2 className="font-semibold text-2xl tracking-tight">
|
|
709
|
+
Collections
|
|
710
|
+
</h2>
|
|
703
711
|
<Button onClick={openCollections} size="sm" variant="outline">
|
|
704
712
|
Manage Collections
|
|
705
713
|
</Button>
|
|
@@ -1057,7 +1057,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
1057
1057
|
</div>
|
|
1058
1058
|
{results.map((result, i) => (
|
|
1059
1059
|
<Card
|
|
1060
|
-
className="group animate-fade-in cursor-pointer opacity-0 transition-all hover:border-primary/50 hover:bg-card/80"
|
|
1060
|
+
className="group animate-fade-in cursor-pointer opacity-0 transition-all duration-200 hover:border-primary/50 hover:bg-card/80 hover:shadow-[0_0_24px_-10px_hsl(var(--primary)/0.12)]"
|
|
1061
1061
|
key={`${result.docid}-${i}`}
|
|
1062
1062
|
onClick={() =>
|
|
1063
1063
|
navigate(
|
package/src/serve/server.ts
CHANGED
|
@@ -8,13 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import type { ContextHolder } from "./routes/api";
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import { getConfigPaths, isInitialized, loadConfig } from "../config";
|
|
13
|
-
import { getActivePreset } from "../llm/registry";
|
|
14
|
-
import { SqliteAdapter } from "../store/sqlite/adapter";
|
|
15
|
-
import { createServerContext, disposeServerContext } from "./context";
|
|
11
|
+
import { startBackgroundRuntime } from "./background-runtime";
|
|
16
12
|
import { DocumentEventBus } from "./doc-events";
|
|
17
|
-
import { createEmbedScheduler } from "./embed-scheduler";
|
|
18
13
|
// HTML import - Bun handles bundling TSX/CSS automatically via routes
|
|
19
14
|
import homepage from "./public/index.html";
|
|
20
15
|
import {
|
|
@@ -57,7 +52,6 @@ import {
|
|
|
57
52
|
handleDocSimilar,
|
|
58
53
|
} from "./routes/links";
|
|
59
54
|
import { forbiddenResponse, isRequestAllowed } from "./security";
|
|
60
|
-
import { CollectionWatchService } from "./watch-service";
|
|
61
55
|
|
|
62
56
|
export interface ServeOptions {
|
|
63
57
|
/** Port to listen on (default: 3000) */
|
|
@@ -131,78 +125,18 @@ export async function startServer(
|
|
|
131
125
|
): Promise<ServeResult> {
|
|
132
126
|
const port = options.port ?? 3000;
|
|
133
127
|
const isDev = process.env.NODE_ENV !== "production";
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Load config
|
|
142
|
-
const configResult = await loadConfig(options.configPath);
|
|
143
|
-
if (!configResult.ok) {
|
|
144
|
-
return { success: false, error: configResult.error.message };
|
|
145
|
-
}
|
|
146
|
-
const config = configResult.value;
|
|
147
|
-
|
|
148
|
-
// Open database once for server lifetime
|
|
149
|
-
const store = new SqliteAdapter();
|
|
150
|
-
const dbPath = getIndexDbPath(options.index);
|
|
151
|
-
// Use actual config path (from options or default) for consistency
|
|
152
|
-
const paths = getConfigPaths();
|
|
153
|
-
const actualConfigPath = options.configPath ?? paths.configFile;
|
|
154
|
-
store.setConfigPath(actualConfigPath);
|
|
155
|
-
|
|
156
|
-
const openResult = await store.open(dbPath, config.ftsTokenizer);
|
|
157
|
-
if (!openResult.ok) {
|
|
158
|
-
return { success: false, error: openResult.error.message };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Sync collections and contexts from config to DB (same as CLI initStore)
|
|
162
|
-
const syncCollResult = await store.syncCollections(config.collections);
|
|
163
|
-
if (!syncCollResult.ok) {
|
|
164
|
-
await store.close();
|
|
165
|
-
return { success: false, error: syncCollResult.error.message };
|
|
166
|
-
}
|
|
167
|
-
const syncCtxResult = await store.syncContexts(config.contexts ?? []);
|
|
168
|
-
if (!syncCtxResult.ok) {
|
|
169
|
-
await store.close();
|
|
170
|
-
return { success: false, error: syncCtxResult.error.message };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Create server context with LLM ports for hybrid search and AI answers
|
|
174
|
-
// Use holder pattern to allow hot-reloading presets
|
|
175
|
-
const ctx = await createServerContext(store, config);
|
|
176
|
-
|
|
177
|
-
const ctxHolder: ContextHolder = {
|
|
178
|
-
current: ctx,
|
|
179
|
-
config, // Keep original config for reloading
|
|
180
|
-
scheduler: null, // Will be set below
|
|
181
|
-
eventBus: null,
|
|
182
|
-
watchService: null,
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
// Create embed scheduler with getters (survives context/preset reloads)
|
|
186
|
-
const scheduler = createEmbedScheduler({
|
|
187
|
-
db: store.getRawDb(),
|
|
188
|
-
getEmbedPort: () => ctxHolder.current.embedPort,
|
|
189
|
-
getVectorIndex: () => ctxHolder.current.vectorIndex,
|
|
190
|
-
getModelUri: () => getActivePreset(ctxHolder.config).embed,
|
|
128
|
+
const runtimeResult = await startBackgroundRuntime({
|
|
129
|
+
configPath: options.configPath,
|
|
130
|
+
index: options.index,
|
|
131
|
+
requireCollections: false,
|
|
132
|
+
eventBus: new DocumentEventBus(),
|
|
191
133
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
collections: config.collections,
|
|
199
|
-
store,
|
|
200
|
-
scheduler,
|
|
201
|
-
eventBus,
|
|
202
|
-
});
|
|
203
|
-
watchService.start();
|
|
204
|
-
ctxHolder.watchService = watchService;
|
|
205
|
-
ctxHolder.current.watchService = watchService;
|
|
134
|
+
if (!runtimeResult.success) {
|
|
135
|
+
return { success: false, error: runtimeResult.error };
|
|
136
|
+
}
|
|
137
|
+
const runtime = runtimeResult.runtime;
|
|
138
|
+
const store = runtime.store;
|
|
139
|
+
const ctxHolder: ContextHolder = runtime.ctxHolder;
|
|
206
140
|
|
|
207
141
|
// Shutdown controller for clean lifecycle
|
|
208
142
|
const shutdownController = new AbortController();
|
|
@@ -210,11 +144,7 @@ export async function startServer(
|
|
|
210
144
|
// Graceful shutdown handler
|
|
211
145
|
const shutdown = async () => {
|
|
212
146
|
console.log("\nShutting down...");
|
|
213
|
-
|
|
214
|
-
eventBus.close();
|
|
215
|
-
scheduler.dispose();
|
|
216
|
-
await disposeServerContext(ctxHolder.current);
|
|
217
|
-
await store.close();
|
|
147
|
+
await runtime.dispose();
|
|
218
148
|
shutdownController.abort();
|
|
219
149
|
};
|
|
220
150
|
|
|
@@ -420,7 +350,12 @@ export async function startServer(
|
|
|
420
350
|
},
|
|
421
351
|
},
|
|
422
352
|
"/api/events": {
|
|
423
|
-
GET: () =>
|
|
353
|
+
GET: () =>
|
|
354
|
+
withSecurityHeaders(
|
|
355
|
+
runtime.eventBus?.createResponse() ??
|
|
356
|
+
new Response("event stream unavailable", { status: 503 }),
|
|
357
|
+
isDev
|
|
358
|
+
),
|
|
424
359
|
},
|
|
425
360
|
"/api/tags": {
|
|
426
361
|
GET: async (req: Request) => {
|
|
@@ -568,7 +503,7 @@ export async function startServer(
|
|
|
568
503
|
},
|
|
569
504
|
});
|
|
570
505
|
} catch (e) {
|
|
571
|
-
await
|
|
506
|
+
await runtime.dispose();
|
|
572
507
|
return {
|
|
573
508
|
success: false,
|
|
574
509
|
error: e instanceof Error ? e.message : String(e),
|