@gmickel/gno 0.22.6 → 0.24.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.
@@ -10,6 +10,7 @@ import {
10
10
  } from "lucide-react";
11
11
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
12
12
 
13
+ import { normalizeStructuredQueryInput } from "../../../core/structured-query";
13
14
  import { Loader } from "../components/ai-elements/loader";
14
15
  import {
15
16
  Source,
@@ -199,6 +200,14 @@ export default function Ask({ navigate }: PageProps) {
199
200
  const textareaRef = useRef<HTMLTextAreaElement>(null);
200
201
 
201
202
  const hybridAvailable = capabilities?.hybrid ?? false;
203
+ const structuredQueryState = useMemo(
204
+ () => normalizeStructuredQueryInput(query, queryModes),
205
+ [query, queryModes]
206
+ );
207
+ const structuredQueryError =
208
+ query.trim().length > 0 && !structuredQueryState.ok
209
+ ? structuredQueryState.error.message
210
+ : null;
202
211
 
203
212
  useEffect(() => {
204
213
  async function bootstrap(): Promise<void> {
@@ -270,6 +279,11 @@ export default function Ask({ navigate }: PageProps) {
270
279
  if (!query.trim()) {
271
280
  return;
272
281
  }
282
+ if (!structuredQueryState.ok) {
283
+ setQueryModeError(structuredQueryState.error.message);
284
+ setShowAdvanced(true);
285
+ return;
286
+ }
273
287
 
274
288
  const entryId = crypto.randomUUID();
275
289
  const currentQuery = query.trim();
@@ -361,6 +375,7 @@ export default function Ask({ navigate }: PageProps) {
361
375
  queryModes,
362
376
  selectedCollection,
363
377
  since,
378
+ structuredQueryState,
364
379
  tagMode,
365
380
  tagsInput,
366
381
  thoroughness,
@@ -906,7 +921,7 @@ export default function Ask({ navigate }: PageProps) {
906
921
  onKeyDown={handleKeyDown}
907
922
  placeholder={
908
923
  answerAvailable
909
- ? "Ask a question about your documents..."
924
+ ? "Ask a question about your documents... Use Shift+Enter for structured query documents"
910
925
  : "AI answers not available"
911
926
  }
912
927
  ref={textareaRef}
@@ -915,13 +930,26 @@ export default function Ask({ navigate }: PageProps) {
915
930
  />
916
931
  <Button
917
932
  className="absolute right-2 bottom-2"
918
- disabled={!(query.trim() && answerAvailable)}
933
+ disabled={
934
+ !(query.trim() && answerAvailable) ||
935
+ Boolean(structuredQueryError)
936
+ }
919
937
  size="icon-sm"
920
938
  type="submit"
921
939
  >
922
940
  <CornerDownLeft className="size-4" />
923
941
  </Button>
924
942
  </div>
943
+ {structuredQueryError && (
944
+ <p className="text-destructive text-xs">{structuredQueryError}</p>
945
+ )}
946
+ {!structuredQueryError && (
947
+ <p className="text-muted-foreground/70 text-xs">
948
+ Press Enter to submit. Use Shift+Enter for multi-line structured
949
+ query documents with <code>term:</code>, <code>intent:</code>, and{" "}
950
+ <code>hyde:</code>.
951
+ </p>
952
+ )}
925
953
  </form>
926
954
  </footer>
927
955
  </div>
@@ -8,6 +8,7 @@ import {
8
8
  } from "lucide-react";
9
9
  import { useCallback, useEffect, useMemo, useState } from "react";
10
10
 
11
+ import { normalizeStructuredQueryInput } from "../../../core/structured-query";
11
12
  import { Loader } from "../components/ai-elements/loader";
12
13
  import { TagFacets } from "../components/TagFacets";
13
14
  import {
@@ -30,6 +31,7 @@ import {
30
31
  SelectTrigger,
31
32
  SelectValue,
32
33
  } from "../components/ui/select";
34
+ import { Textarea } from "../components/ui/textarea";
33
35
  import { apiFetch } from "../hooks/use-api";
34
36
  import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
35
37
  import {
@@ -187,9 +189,20 @@ export default function Search({ navigate }: PageProps) {
187
189
  const [showMobileTags, setShowMobileTags] = useState(false);
188
190
 
189
191
  const hybridAvailable = capabilities?.hybrid ?? false;
192
+ const structuredQueryState = useMemo(
193
+ () => normalizeStructuredQueryInput(query, queryModes),
194
+ [query, queryModes]
195
+ );
196
+ const structuredQueryError =
197
+ query.trim().length > 0 && !structuredQueryState.ok
198
+ ? structuredQueryState.error.message
199
+ : null;
190
200
  const forceHybridForModes =
191
201
  thoroughness === "fast" &&
192
- (queryModes.length > 0 || intent.trim().length > 0);
202
+ (queryModes.length > 0 ||
203
+ intent.trim().length > 0 ||
204
+ (structuredQueryState.ok &&
205
+ structuredQueryState.value.usedStructuredQuerySyntax));
193
206
 
194
207
  // Sync URL as filter state changes.
195
208
  useEffect(() => {
@@ -294,6 +307,10 @@ export default function Search({ navigate }: PageProps) {
294
307
  if (!query.trim()) {
295
308
  return;
296
309
  }
310
+ if (!structuredQueryState.ok) {
311
+ setError(structuredQueryState.error.message);
312
+ return;
313
+ }
297
314
 
298
315
  setLoading(true);
299
316
  setError(null);
@@ -302,7 +319,8 @@ export default function Search({ navigate }: PageProps) {
302
319
  const useBm25 =
303
320
  thoroughness === "fast" &&
304
321
  queryModes.length === 0 &&
305
- intent.trim().length === 0;
322
+ intent.trim().length === 0 &&
323
+ !structuredQueryState.value.usedStructuredQuerySyntax;
306
324
  const endpoint = useBm25 ? "/api/search" : "/api/query";
307
325
  const body: Record<string, unknown> = {
308
326
  query,
@@ -386,6 +404,7 @@ export default function Search({ navigate }: PageProps) {
386
404
  queryModes,
387
405
  selectedCollection,
388
406
  since,
407
+ structuredQueryState,
389
408
  tagMode,
390
409
  thoroughness,
391
410
  until,
@@ -448,6 +467,13 @@ export default function Search({ navigate }: PageProps) {
448
467
  setQueryModeError(null);
449
468
  };
450
469
 
470
+ const handleQueryKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
471
+ if (e.key === "Enter" && !e.shiftKey) {
472
+ e.preventDefault();
473
+ void handleSearch();
474
+ }
475
+ };
476
+
451
477
  return (
452
478
  <div className="min-h-screen">
453
479
  <header className="glass sticky top-0 z-10 border-border/50 border-b">
@@ -482,17 +508,19 @@ export default function Search({ navigate }: PageProps) {
482
508
  <div className="pointer-events-none absolute -inset-[1px] rounded-lg bg-gradient-to-r from-primary/50 via-primary to-primary/50 opacity-0 blur-sm transition-opacity duration-300 group-focus-within:opacity-100" />
483
509
  <div className="relative">
484
510
  <SearchIcon className="absolute top-1/2 left-4 size-5 -translate-y-1/2 text-muted-foreground transition-colors duration-200 group-focus-within:text-primary" />
485
- <Input
511
+ <Textarea
486
512
  autoFocus
487
- className="border-border/50 bg-card py-6 pr-4 pl-12 text-lg transition-all duration-200 focus:border-primary focus:bg-card/80 focus:shadow-[0_0_20px_-5px_hsl(var(--primary)/0.3)]"
513
+ className="min-h-[72px] resize-y border-border/50 bg-card pt-5 pr-20 pb-4 pl-12 text-base transition-all duration-200 focus:border-primary focus:bg-card/80 focus:shadow-[0_0_20px_-5px_hsl(var(--primary)/0.3)]"
488
514
  onChange={(e) => setQuery(e.target.value)}
489
- placeholder="Search your documents..."
490
- type="text"
515
+ onKeyDown={handleQueryKeyDown}
516
+ placeholder="Search your documents... Use Shift+Enter for structured query documents"
491
517
  value={query}
492
518
  />
493
519
  <Button
494
520
  className="absolute top-1/2 right-2 -translate-y-1/2"
495
- disabled={loading || !query.trim()}
521
+ disabled={
522
+ loading || !query.trim() || Boolean(structuredQueryError)
523
+ }
496
524
  size="sm"
497
525
  type="submit"
498
526
  >
@@ -501,6 +529,18 @@ export default function Search({ navigate }: PageProps) {
501
529
  </div>
502
530
  </div>
503
531
 
532
+ {structuredQueryError && (
533
+ <p className="text-destructive text-xs">
534
+ {structuredQueryError}
535
+ </p>
536
+ )}
537
+
538
+ <p className="text-muted-foreground/70 text-xs">
539
+ Press Enter to search. Use Shift+Enter for multi-line structured
540
+ query documents with <code>term:</code>, <code>intent:</code>,
541
+ and <code>hyde:</code>.
542
+ </p>
543
+
504
544
  <div className="flex flex-wrap items-center gap-4">
505
545
  <ThoroughnessSelector
506
546
  disabled={
@@ -18,6 +18,7 @@ import type { EmbedScheduler } from "../embed-scheduler";
18
18
  import { modelsPull } from "../../cli/commands/models/pull";
19
19
  import { addCollection, removeCollection } from "../../collection";
20
20
  import { atomicWrite } from "../../core/file-ops";
21
+ import { normalizeStructuredQueryInput } from "../../core/structured-query";
21
22
  import {
22
23
  normalizeTag,
23
24
  parseAndValidateTagFilter,
@@ -32,6 +33,7 @@ import {
32
33
  processAnswerResult,
33
34
  } from "../../pipeline/answer";
34
35
  import { searchHybrid } from "../../pipeline/hybrid";
36
+ import { validateQueryModes } from "../../pipeline/query-modes";
35
37
  import { searchBm25 } from "../../pipeline/search";
36
38
  import { applyConfigChange } from "../config-sync";
37
39
  import {
@@ -242,7 +244,38 @@ function parseQueryModesInput(value: unknown): {
242
244
  queryModes.push({ mode, text: text.trim() });
243
245
  }
244
246
 
245
- return { queryModes };
247
+ const validated = validateQueryModes(queryModes);
248
+ if (!validated.ok) {
249
+ return {
250
+ error: errorResponse("VALIDATION", validated.error.message),
251
+ };
252
+ }
253
+
254
+ return { queryModes: validated.value };
255
+ }
256
+
257
+ function normalizeStructuredQueryBody(
258
+ query: string,
259
+ queryModes: QueryModeInput[] | undefined
260
+ ): {
261
+ query?: string;
262
+ queryModes?: QueryModeInput[];
263
+ error?: Response;
264
+ } {
265
+ const normalized = normalizeStructuredQueryInput(query, queryModes ?? []);
266
+ if (!normalized.ok) {
267
+ return {
268
+ error: errorResponse("VALIDATION", normalized.error.message),
269
+ };
270
+ }
271
+
272
+ return {
273
+ query: normalized.value.query,
274
+ queryModes:
275
+ normalized.value.queryModes.length > 0
276
+ ? normalized.value.queryModes
277
+ : undefined,
278
+ };
246
279
  }
247
280
 
248
281
  // ─────────────────────────────────────────────────────────────────────────────
@@ -1137,8 +1170,8 @@ export async function handleSearch(
1137
1170
  return errorResponse("VALIDATION", "Missing or invalid query");
1138
1171
  }
1139
1172
 
1140
- const query = body.query.trim();
1141
- if (!query) {
1173
+ const rawQuery = body.query.trim();
1174
+ if (!rawQuery) {
1142
1175
  return errorResponse("VALIDATION", "Query cannot be empty");
1143
1176
  }
1144
1177
 
@@ -1237,7 +1270,7 @@ export async function handleSearch(
1237
1270
  author,
1238
1271
  };
1239
1272
 
1240
- const result = await searchBm25(store, query, options);
1273
+ const result = await searchBm25(store, rawQuery, options);
1241
1274
 
1242
1275
  if (!result.ok) {
1243
1276
  return errorResponse("RUNTIME", result.error.message, 500);
@@ -1266,8 +1299,8 @@ export async function handleQuery(
1266
1299
  return errorResponse("VALIDATION", "Missing or invalid query");
1267
1300
  }
1268
1301
 
1269
- const query = body.query.trim();
1270
- if (!query) {
1302
+ const rawQuery = body.query.trim();
1303
+ if (!rawQuery) {
1271
1304
  return errorResponse("VALIDATION", "Query cannot be empty");
1272
1305
  }
1273
1306
 
@@ -1333,6 +1366,16 @@ export async function handleQuery(
1333
1366
  return queryModesError;
1334
1367
  }
1335
1368
 
1369
+ const {
1370
+ query,
1371
+ queryModes: normalizedQueryModes,
1372
+ error: structuredQueryError,
1373
+ } = normalizeStructuredQueryBody(rawQuery, queryModes);
1374
+ if (structuredQueryError) {
1375
+ return structuredQueryError;
1376
+ }
1377
+ const normalizedQuery = query ?? rawQuery;
1378
+
1336
1379
  // Parse tag filters
1337
1380
  let tagsAll: string[] | undefined;
1338
1381
  let tagsAny: string[] | undefined;
@@ -1376,7 +1419,7 @@ export async function handleQuery(
1376
1419
  genPort: ctx.genPort,
1377
1420
  rerankPort: ctx.rerankPort,
1378
1421
  },
1379
- query,
1422
+ normalizedQuery,
1380
1423
  {
1381
1424
  limit: Math.min(body.limit ?? 20, 50),
1382
1425
  minScore: body.minScore,
@@ -1388,7 +1431,7 @@ export async function handleQuery(
1388
1431
  ? Math.min(body.candidateLimit, 100)
1389
1432
  : undefined,
1390
1433
  exclude,
1391
- queryModes,
1434
+ queryModes: normalizedQueryModes,
1392
1435
  noExpand: body.noExpand,
1393
1436
  noRerank: body.noRerank,
1394
1437
  tagsAll,
@@ -1427,8 +1470,8 @@ export async function handleAsk(
1427
1470
  return errorResponse("VALIDATION", "Missing or invalid query");
1428
1471
  }
1429
1472
 
1430
- const query = body.query.trim();
1431
- if (!query) {
1473
+ const rawQuery = body.query.trim();
1474
+ if (!rawQuery) {
1432
1475
  return errorResponse("VALIDATION", "Query cannot be empty");
1433
1476
  }
1434
1477
 
@@ -1486,6 +1529,16 @@ export async function handleAsk(
1486
1529
  return queryModesError;
1487
1530
  }
1488
1531
 
1532
+ const {
1533
+ query,
1534
+ queryModes: normalizedQueryModes,
1535
+ error: structuredQueryError,
1536
+ } = normalizeStructuredQueryBody(rawQuery, queryModes);
1537
+ if (structuredQueryError) {
1538
+ return structuredQueryError;
1539
+ }
1540
+ const normalizedQuery = query ?? rawQuery;
1541
+
1489
1542
  if (body.tagsAll) {
1490
1543
  try {
1491
1544
  tagsAll = parseAndValidateTagFilter(body.tagsAll);
@@ -1528,7 +1581,7 @@ export async function handleAsk(
1528
1581
  genPort: ctx.genPort,
1529
1582
  rerankPort: ctx.rerankPort,
1530
1583
  },
1531
- query,
1584
+ normalizedQuery,
1532
1585
  {
1533
1586
  limit,
1534
1587
  collection: body.collection,
@@ -1541,7 +1594,7 @@ export async function handleAsk(
1541
1594
  ? Math.min(body.candidateLimit, 100)
1542
1595
  : undefined,
1543
1596
  exclude,
1544
- queryModes,
1597
+ queryModes: normalizedQueryModes,
1545
1598
  tagsAll,
1546
1599
  tagsAny,
1547
1600
  since: body.since,
@@ -1567,7 +1620,7 @@ export async function handleAsk(
1567
1620
  const maxTokens = body.maxAnswerTokens ?? 512;
1568
1621
  const rawResult = await generateGroundedAnswer(
1569
1622
  { genPort: ctx.genPort, store: ctx.store },
1570
- query,
1623
+ normalizedQuery,
1571
1624
  results,
1572
1625
  maxTokens
1573
1626
  );
@@ -1582,7 +1635,7 @@ export async function handleAsk(
1582
1635
  }
1583
1636
 
1584
1637
  const askResult: AskResult = {
1585
- query,
1638
+ query: normalizedQuery,
1586
1639
  mode: searchResult.value.meta.vectorsUsed ? "hybrid" : "bm25_only",
1587
1640
  queryLanguage: searchResult.value.meta.queryLanguage ?? "und",
1588
1641
  answer,