ltcai 4.4.0 → 4.5.1

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.
Files changed (61) hide show
  1. package/README.md +46 -18
  2. package/docs/CHANGELOG.md +85 -0
  3. package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +49 -0
  4. package/docs/V4_5_0_GRAPH_UX_REPORT.md +34 -0
  5. package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +40 -0
  6. package/docs/V4_5_0_ONBOARDING_REPORT.md +31 -0
  7. package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +49 -0
  8. package/docs/V4_5_0_VALIDATION_REPORT.md +60 -0
  9. package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +33 -0
  10. package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +37 -0
  11. package/docs/V4_5_1_NAVIGATION_REPORT.md +37 -0
  12. package/docs/V4_5_1_ONBOARDING_REPORT.md +29 -0
  13. package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +61 -0
  14. package/docs/V4_5_1_RC_ARTIFACTS.md +44 -0
  15. package/docs/V4_5_1_UX_REPORT.md +45 -0
  16. package/docs/V4_5_1_VALIDATION_REPORT.md +54 -0
  17. package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +30 -0
  18. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +16 -16
  19. package/docs/architecture.md +8 -4
  20. package/frontend/src/App.tsx +152 -91
  21. package/frontend/src/api/client.ts +83 -1
  22. package/frontend/src/components/FirstRunGuide.tsx +99 -0
  23. package/frontend/src/components/primitives.tsx +131 -25
  24. package/frontend/src/components/ui/badge.tsx +2 -2
  25. package/frontend/src/components/ui/button.tsx +7 -7
  26. package/frontend/src/components/ui/card.tsx +5 -5
  27. package/frontend/src/components/ui/input.tsx +1 -1
  28. package/frontend/src/components/ui/textarea.tsx +1 -1
  29. package/frontend/src/pages/Act.tsx +58 -28
  30. package/frontend/src/pages/Ask.tsx +51 -19
  31. package/frontend/src/pages/Brain.tsx +60 -42
  32. package/frontend/src/pages/Capture.tsx +24 -24
  33. package/frontend/src/pages/Library.tsx +222 -32
  34. package/frontend/src/pages/System.tsx +56 -34
  35. package/frontend/src/routes.ts +15 -13
  36. package/frontend/src/store/appStore.ts +8 -1
  37. package/frontend/src/styles.css +666 -36
  38. package/lattice_brain/__init__.py +1 -1
  39. package/lattice_brain/runtime/multi_agent.py +1 -1
  40. package/latticeai/__init__.py +1 -1
  41. package/latticeai/api/models.py +107 -18
  42. package/latticeai/core/marketplace.py +1 -1
  43. package/latticeai/core/model_compat.py +250 -0
  44. package/latticeai/core/workspace_os.py +1 -1
  45. package/latticeai/models/router.py +136 -32
  46. package/latticeai/services/model_catalog.py +2 -2
  47. package/latticeai/services/model_recommendation.py +8 -1
  48. package/latticeai/services/model_runtime.py +18 -3
  49. package/package.json +1 -1
  50. package/scripts/build_frontend_assets.mjs +12 -1
  51. package/src-tauri/Cargo.lock +1 -1
  52. package/src-tauri/Cargo.toml +1 -1
  53. package/src-tauri/tauri.conf.json +1 -1
  54. package/static/app/asset-manifest.json +5 -5
  55. package/static/app/assets/index-3G8qcrIS.js +336 -0
  56. package/static/app/assets/index-3G8qcrIS.js.map +1 -0
  57. package/static/app/assets/index-C0wYZp7k.css +2 -0
  58. package/static/app/index.html +2 -2
  59. package/static/app/assets/index-CHHal8Zl.css +0 -2
  60. package/static/app/assets/index-pdzil9ac.js +0 -333
  61. package/static/app/assets/index-pdzil9ac.js.map +0 -1
@@ -3,12 +3,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
3
  import cytoscape, { Core, ElementDefinition } from "cytoscape";
4
4
  import { BrainCircuit, DatabaseBackup, Filter, Focus, Layers3, LocateFixed, Search, Sparkles } from "lucide-react";
5
5
  import { latticeApi } from "@/api/client";
6
- import { ActionButton, DataPanel, EmptyState, EntityList, LoadingPanel, OperationResult, StatGrid, StructuredView, Tabs } from "@/components/primitives";
6
+ import { ActionButton, DataPanel, EmptyState, EntityList, KeyValueList, LoadingPanel, OperationResult, StatGrid, StructuredView, Tabs } from "@/components/primitives";
7
7
  import { Badge } from "@/components/ui/badge";
8
8
  import { Button } from "@/components/ui/button";
9
9
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
10
10
  import { Input } from "@/components/ui/input";
11
11
  import { Textarea } from "@/components/ui/textarea";
12
+ import { useAppStore } from "@/store/appStore";
12
13
  import { asArray, fmtNumber, pct, shortId, titleize } from "@/lib/utils";
13
14
 
14
15
  type BrainTab = "overview" | "graph" | "search" | "memory" | "provenance" | "portability";
@@ -60,11 +61,11 @@ type ExplorerModel = ParsedGraph & {
60
61
  };
61
62
 
62
63
  const tabs: Array<{ id: BrainTab; label: string }> = [
63
- { id: "overview", label: "Overview" },
64
- { id: "graph", label: "Graph" },
64
+ { id: "overview", label: "Today" },
65
+ { id: "graph", label: "Map" },
65
66
  { id: "search", label: "Search" },
66
67
  { id: "memory", label: "Memory" },
67
- { id: "provenance", label: "Provenance" },
68
+ { id: "provenance", label: "Sources" },
68
69
  { id: "portability", label: "Portability" },
69
70
  ];
70
71
 
@@ -405,13 +406,14 @@ function CytoscapeGraph({
405
406
  <div
406
407
  ref={hostRef}
407
408
  data-testid="brain-cytoscape"
408
- className="h-[620px] min-h-[32rem] w-full overflow-hidden rounded-md border border-border bg-background brain-grid"
409
+ className="brain-grid h-[620px] min-h-[32rem] w-full overflow-hidden rounded-lg border border-border bg-background/80"
409
410
  />
410
411
  );
411
412
  }
412
413
 
413
414
  export function BrainPage({ initialTab }: { initialTab?: string }) {
414
- const [tab, setTab] = React.useState<BrainTab>((initialTab as BrainTab) || "graph");
415
+ const mode = useAppStore((state) => state.mode);
416
+ const [tab, setTab] = React.useState<BrainTab>((initialTab as BrainTab) || "overview");
415
417
  React.useEffect(() => {
416
418
  if (initialTab && tabs.some((item) => item.id === initialTab)) setTab(initialTab as BrainTab);
417
419
  }, [initialTab]);
@@ -423,19 +425,19 @@ export function BrainPage({ initialTab }: { initialTab?: string }) {
423
425
  const memory = useQuery({ queryKey: ["memoryManager"], queryFn: latticeApi.memoryManager });
424
426
 
425
427
  return (
426
- <div className="space-y-4">
427
- <header className="grid gap-4 xl:grid-cols-[1.4fr_0.6fr]">
428
+ <div className="space-y-5">
429
+ <header className="page-hero grid gap-4 xl:grid-cols-[1.4fr_0.6fr]">
428
430
  <div>
429
- <div className="flex items-center gap-2 text-sm text-primary"><BrainCircuit className="h-4 w-4" /> Graph-first Digital Brain</div>
430
- <h1 className="mt-2 text-3xl font-semibold tracking-normal">Brain</h1>
431
- <p className="mt-2 max-w-3xl text-sm text-muted-foreground">
432
- Explore the knowledge graph, memory, provenance, retrieval, and portable brain state. Empty states reflect live API availability.
431
+ <div className="page-kicker"><BrainCircuit className="h-4 w-4" /> Home</div>
432
+ <h1 className="page-title">Start from the shape of your work.</h1>
433
+ <p className="page-copy">
434
+ Lattice turns what you add into a living memory map, then keeps every answer tied back to its source.
433
435
  </p>
434
436
  </div>
435
- <div className="rounded-md border border-border bg-card p-4">
436
- <div className="text-xs uppercase text-muted-foreground">Provenance coverage</div>
437
+ <div className="rounded-lg border border-border bg-background/58 p-4">
438
+ <div className="text-xs uppercase text-muted-foreground">Source coverage</div>
437
439
  <div className="mt-2 text-3xl font-semibold">{pct((coverage.data?.data as Record<string, unknown>)?.coverage_ratio)}</div>
438
- <div className="mt-2 text-sm text-muted-foreground">Source: {coverage.data?.source || "loading"}</div>
440
+ <div className="mt-2 text-sm text-muted-foreground">{coverage.data?.ok ? "Sources are linked to graph records." : "Checking source coverage."}</div>
439
441
  </div>
440
442
  </header>
441
443
  <Tabs tabs={tabs} value={tab} onChange={(id) => setTab(id as BrainTab)} />
@@ -451,7 +453,7 @@ export function BrainPage({ initialTab }: { initialTab?: string }) {
451
453
  <DataPanel title="Memory tiers" result={memory.data}>
452
454
  {(data) => <MemoryStatus data={data as Record<string, unknown>} />}
453
455
  </DataPanel>
454
- <DataPanel title="Recent provenance" result={provenance.data}>
456
+ <DataPanel title="Recent sources" result={provenance.data}>
455
457
  {(data) => <EntityList items={(data as Record<string, unknown>).items || data} titleKey="source" metaKey="source_type" />}
456
458
  </DataPanel>
457
459
  </div>
@@ -459,7 +461,7 @@ export function BrainPage({ initialTab }: { initialTab?: string }) {
459
461
 
460
462
  {tab === "graph" ? (
461
463
  graph.isLoading ? <LoadingPanel title="Knowledge graph" /> : (
462
- <DataPanel title="Digital Brain explorer" description="Interactive Cytoscape.js explorer backed by /knowledge-graph/graph." result={graph.data}>
464
+ <DataPanel title="Brain map" description={mode === "basic" ? "Search, focus, and filter the ideas Lattice has learned from your workspace." : "Explore relationships, sources, and graph structure."} result={graph.data}>
463
465
  {(data) => <DigitalBrainExplorer data={data} />}
464
466
  </DataPanel>
465
467
  )
@@ -474,6 +476,7 @@ export function BrainPage({ initialTab }: { initialTab?: string }) {
474
476
  }
475
477
 
476
478
  function GraphStatus({ data }: { data: Record<string, unknown> }) {
479
+ const mode = useAppStore((state) => state.mode);
477
480
  const nodeTypes = Object.keys((data.nodes as Record<string, unknown>) || {});
478
481
  const edgeTypes = Object.keys((data.edges as Record<string, unknown>) || {});
479
482
  return (
@@ -484,7 +487,11 @@ function GraphStatus({ data }: { data: Record<string, unknown> }) {
484
487
  { label: "Node types", value: nodeTypes.length },
485
488
  { label: "Edge types", value: edgeTypes.length },
486
489
  ]} />
487
- <StructuredView value={{ node_types: nodeTypes, edge_types: edgeTypes }} />
490
+ {mode === "basic" ? (
491
+ <div className="flex flex-wrap gap-1">
492
+ {[...nodeTypes, ...edgeTypes].slice(0, 10).map((item) => <Badge key={item} variant="muted">{titleize(item)}</Badge>)}
493
+ </div>
494
+ ) : <StructuredView value={{ node_types: nodeTypes, edge_types: edgeTypes }} />}
488
495
  </div>
489
496
  );
490
497
  }
@@ -515,10 +522,11 @@ function MemoryStatus({ data }: { data: Record<string, unknown> }) {
515
522
  }
516
523
 
517
524
  function DigitalBrainExplorer({ data }: { data: unknown }) {
525
+ const mode = useAppStore((state) => state.mode);
518
526
  const parsed = React.useMemo(() => parseGraph(data), [data]);
519
527
  const [search, setSearch] = React.useState("");
520
528
  const [groupFilter, setGroupFilter] = React.useState("all");
521
- const [minImportance, setMinImportance] = React.useState(0);
529
+ const [minImportance, setMinImportance] = React.useState(mode === "basic" ? 0.1 : 0);
522
530
  const [labelMode, setLabelMode] = React.useState<LabelMode>("important");
523
531
  const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(new Set());
524
532
  const [selectedId, setSelectedId] = React.useState<string | null>(null);
@@ -544,11 +552,14 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
544
552
  return next;
545
553
  });
546
554
  };
555
+ React.useEffect(() => {
556
+ if (mode === "basic" && minImportance < 0.1) setMinImportance(0.1);
557
+ }, [mode, minImportance]);
547
558
  if (!parsed.nodes.length) {
548
559
  return (
549
560
  <EmptyState
550
561
  title="No graph records yet"
551
- detail="Capture a document, note, or local folder to create graph nodes with provenance."
562
+ detail="Capture a document, note, or local folder to create connected ideas with sources."
552
563
  />
553
564
  );
554
565
  }
@@ -557,7 +568,7 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
557
568
  <div className="grid gap-3 xl:grid-cols-[1fr_220px_180px_170px]">
558
569
  <div className="relative">
559
570
  <Search className="pointer-events-none absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
560
- <Input className="pl-9" value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search graph labels, types, provenance..." />
571
+ <Input className="pl-9" value={search} onChange={(event) => setSearch(event.target.value)} placeholder={mode === "basic" ? "Search ideas, files, people, and notes..." : "Search graph labels, types, provenance..."} />
561
572
  </div>
562
573
  <select className="h-9 rounded-md border border-border bg-background px-3 text-sm" value={groupFilter} onChange={(event) => setGroupFilter(event.target.value)}>
563
574
  <option value="all">All semantic groups</option>
@@ -574,9 +585,9 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
574
585
  <Card>
575
586
  <CardHeader className="flex-row items-start justify-between gap-3">
576
587
  <div>
577
- <CardTitle className="flex items-center gap-2"><Layers3 className="h-4 w-4" /> Semantic map</CardTitle>
588
+ <CardTitle className="flex items-center gap-2"><Layers3 className="h-4 w-4" /> Knowledge map</CardTitle>
578
589
  <CardDescription>
579
- Showing {fmtNumber(model.visibleNodes.length)} nodes and {fmtNumber(model.visibleEdges.length)} relationships from {fmtNumber(model.totalNodes)} graph nodes.
590
+ Showing {fmtNumber(model.visibleNodes.length)} ideas and {fmtNumber(model.visibleEdges.length)} connections from {fmtNumber(model.totalNodes)} saved items.
580
591
  </CardDescription>
581
592
  </div>
582
593
  <Badge variant={model.hiddenByFilters ? "warning" : "success"}>{model.hiddenByFilters ? `${fmtNumber(model.hiddenByFilters)} filtered` : "all in view"}</Badge>
@@ -598,7 +609,7 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
598
609
  />
599
610
  <Badge variant="muted">{Math.round(minImportance * 100)}%+</Badge>
600
611
  {selectedId ? <Button variant="outline" size="sm" onClick={() => setSelectedId(null)}>Clear focus</Button> : null}
601
- {search.trim() ? <Button variant="outline" size="sm" onClick={() => backendSearch.mutate()} disabled={backendSearch.isPending}>Search brain</Button> : null}
612
+ {search.trim() ? <Button variant="outline" size="sm" onClick={() => backendSearch.mutate()} disabled={backendSearch.isPending}>Search all memories</Button> : null}
602
613
  </div>
603
614
  <div className="flex flex-wrap gap-2">
604
615
  {model.groups.map((group) => (
@@ -620,7 +631,7 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
620
631
  <Card>
621
632
  <CardHeader>
622
633
  <CardTitle className="flex items-center gap-2"><Focus className="h-4 w-4" /> Focus</CardTitle>
623
- <CardDescription>Click a node to inspect its neighborhood.</CardDescription>
634
+ <CardDescription>Click any idea to see why it matters.</CardDescription>
624
635
  </CardHeader>
625
636
  <CardContent className="space-y-3">
626
637
  {selected ? (
@@ -634,11 +645,18 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
634
645
  </div>
635
646
  </div>
636
647
  {selected.summary ? <p className="text-sm text-muted-foreground">{selected.summary}</p> : null}
637
- <StructuredView value={{
638
- id: selected.id,
639
- degree: selected.degree,
640
- source: selected.source || "not reported",
641
- }} />
648
+ {mode === "basic" ? (
649
+ <KeyValueList data={{
650
+ connections: selected.degree,
651
+ source: selected.source || "not reported",
652
+ }} />
653
+ ) : (
654
+ <StructuredView value={{
655
+ id: selected.id,
656
+ degree: selected.degree,
657
+ source: selected.source || "not reported",
658
+ }} />
659
+ )}
642
660
  </>
643
661
  ) : selectedGroup ? (
644
662
  <div className="space-y-2">
@@ -651,8 +669,8 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
651
669
  </Card>
652
670
  <Card>
653
671
  <CardHeader>
654
- <CardTitle>Important nodes</CardTitle>
655
- <CardDescription>Highest-ranked visible graph records.</CardDescription>
672
+ <CardTitle>Important ideas</CardTitle>
673
+ <CardDescription>What Lattice thinks is most connected right now.</CardDescription>
656
674
  </CardHeader>
657
675
  <CardContent className="space-y-2">
658
676
  {model.visibleNodes.slice(0, 8).map((node) => (
@@ -687,8 +705,8 @@ function HybridSearch() {
687
705
  return (
688
706
  <Card>
689
707
  <CardHeader>
690
- <CardTitle className="flex items-center gap-2"><Search className="h-4 w-4" /> Hybrid search</CardTitle>
691
- <CardDescription>Calls the backend fused search endpoint and renders records with returned source scores.</CardDescription>
708
+ <CardTitle className="flex items-center gap-2"><Search className="h-4 w-4" /> Brain search</CardTitle>
709
+ <CardDescription>Find ideas across memories, documents, and connections.</CardDescription>
692
710
  </CardHeader>
693
711
  <CardContent className="space-y-3">
694
712
  <div className="flex flex-col gap-2 sm:flex-row">
@@ -717,7 +735,7 @@ function MemoryPanel() {
717
735
  <Card>
718
736
  <CardHeader>
719
737
  <CardTitle className="flex items-center gap-2"><Sparkles className="h-4 w-4" /> Recall</CardTitle>
720
- <CardDescription>Searches the real memory recall endpoint.</CardDescription>
738
+ <CardDescription>Bring back related memories from your workspace.</CardDescription>
721
739
  </CardHeader>
722
740
  <CardContent className="space-y-3">
723
741
  <Input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Recall memories about..." />
@@ -741,7 +759,7 @@ function ProvenancePanel() {
741
759
  <DataPanel title="Coverage" result={coverage.data}>
742
760
  {(data) => <StructuredView value={data} />}
743
761
  </DataPanel>
744
- <DataPanel title="Recent ingestion provenance" result={provenance.data}>
762
+ <DataPanel title="Recent sources" result={provenance.data}>
745
763
  {(data) => <EntityList items={(data as Record<string, unknown>).items || data} titleKey="source" metaKey="source_type" limit={14} />}
746
764
  </DataPanel>
747
765
  </div>
@@ -770,22 +788,22 @@ function PortabilityPanel() {
770
788
  <Card>
771
789
  <CardHeader>
772
790
  <CardTitle className="flex items-center gap-2"><DatabaseBackup className="h-4 w-4" /> Export, backup, import</CardTitle>
773
- <CardDescription>Every control calls a real portability endpoint. Import is dry-run by default from a pasted export artifact.</CardDescription>
791
+ <CardDescription>Export, back up, or preview an import before changing your brain.</CardDescription>
774
792
  </CardHeader>
775
793
  <CardContent className="space-y-3">
776
794
  <div className="flex flex-wrap gap-2">
777
- <ActionButton label="Export graph artifact" action={() => latticeApi.graphExport()} />
795
+ <ActionButton label="Export brain map" action={() => latticeApi.graphExport()} />
778
796
  <ActionButton label="Create backup" action={() => latticeApi.graphBackup()} />
779
797
  </div>
780
- <Textarea value={artifact} onChange={(e) => setArtifact(e.target.value)} placeholder="Paste an exported graph artifact for dry-run import" />
798
+ <Textarea value={artifact} onChange={(e) => setArtifact(e.target.value)} placeholder="Paste an exported brain map to preview import" />
781
799
  <Button
782
800
  variant="outline"
783
801
  disabled={!artifact.trim() || importMutation.isPending}
784
802
  onClick={() => importMutation.mutate()}
785
803
  >
786
- Dry-run import
804
+ Preview import
787
805
  </Button>
788
- {importMutation.data ? <OperationResult result={importMutation.data} successLabel="Dry run completed" /> : null}
806
+ {importMutation.data ? <OperationResult result={importMutation.data} successLabel="Import preview completed" /> : null}
789
807
  </CardContent>
790
808
  </Card>
791
809
  </div>
@@ -798,7 +816,7 @@ function PortabilityStatus({ data }: { data: Record<string, unknown> }) {
798
816
  return (
799
817
  <div className="space-y-3">
800
818
  <StatGrid stats={[
801
- { label: "Schema", value: data.graph_schema_version || data.schema_version || "reported" },
819
+ { label: "Graph version", value: data.graph_schema_version || data.schema_version || "reported" },
802
820
  { label: "Nodes", value: (stats.total_nodes as number) || Object.values((stats.nodes as Record<string, unknown>) || {}).reduce((sum: number, value) => sum + Number(value || 0), 0) },
803
821
  { label: "Edges", value: (stats.total_edges as number) || Object.values((stats.edges as Record<string, unknown>) || {}).reduce((sum: number, value) => sum + Number(value || 0), 0) },
804
822
  { label: "Storage", value: storage.engine || "reported" },
@@ -12,9 +12,9 @@ type CaptureTab = "files" | "local" | "browser" | "pipeline";
12
12
 
13
13
  const tabs: Array<{ id: CaptureTab; label: string }> = [
14
14
  { id: "files", label: "Files" },
15
- { id: "local", label: "Local folders" },
16
- { id: "browser", label: "Web capture" },
17
- { id: "pipeline", label: "Pipeline" },
15
+ { id: "local", label: "Folders" },
16
+ { id: "browser", label: "Web" },
17
+ { id: "pipeline", label: "Flow" },
18
18
  ];
19
19
 
20
20
  export function CapturePage({ initialTab }: { initialTab?: string }) {
@@ -23,11 +23,11 @@ export function CapturePage({ initialTab }: { initialTab?: string }) {
23
23
  if (initialTab === "pipeline" || initialTab === "local" || initialTab === "files") setTab(initialTab);
24
24
  }, [initialTab]);
25
25
  return (
26
- <div className="space-y-4">
27
- <header>
28
- <div className="flex items-center gap-2 text-sm text-primary"><Upload className="h-4 w-4" /> One ingestion door</div>
29
- <h1 className="mt-2 text-3xl font-semibold">Capture</h1>
30
- <p className="mt-2 max-w-3xl text-sm text-muted-foreground">Documents, folders, and URLs enter the brain through existing ingestion endpoints with provenance.</p>
26
+ <div className="space-y-5">
27
+ <header className="page-hero">
28
+ <div className="page-kicker"><Upload className="h-4 w-4" /> Add</div>
29
+ <h1 className="page-title">Feed the brain what matters.</h1>
30
+ <p className="page-copy">Drop in files, connect folders, or save a page. Lattice remembers the origin of every idea it learns.</p>
31
31
  </header>
32
32
  <Tabs tabs={tabs} value={tab} onChange={(id) => setTab(id as CaptureTab)} />
33
33
  {tab === "files" ? <FilesPanel /> : null}
@@ -49,14 +49,14 @@ function FilesPanel() {
49
49
  <div className="grid gap-4 xl:grid-cols-[0.75fr_1.25fr]">
50
50
  <Card>
51
51
  <CardHeader>
52
- <CardTitle className="flex items-center gap-2"><Upload className="h-4 w-4" /> Upload documents</CardTitle>
53
- <CardDescription>Multipart upload to `/upload/document`; accepted files are parsed and indexed by the backend.</CardDescription>
52
+ <CardTitle className="flex items-center gap-2"><Upload className="h-4 w-4" /> Add documents</CardTitle>
53
+ <CardDescription>Choose files and Lattice will prepare them for search and memory.</CardDescription>
54
54
  </CardHeader>
55
55
  <CardContent>
56
- <label className="flex min-h-44 cursor-pointer flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-muted/30 p-5 text-center">
56
+ <label className="flex min-h-56 cursor-pointer flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-muted/30 p-6 text-center transition hover:bg-muted/50">
57
57
  <Upload className="h-7 w-7 text-primary" />
58
- <span className="font-medium">Choose files</span>
59
- <span className="text-sm text-muted-foreground">PDF, DOCX, XLSX, PPTX, TXT, MD, CSV according to backend policy.</span>
58
+ <span className="text-lg font-semibold">Choose files</span>
59
+ <span className="max-w-sm text-sm leading-6 text-muted-foreground">PDF, Office files, notes, markdown, text, and spreadsheets are all welcome.</span>
60
60
  <input type="file" multiple className="sr-only" onChange={(e) => e.target.files && upload.mutate(e.target.files)} />
61
61
  </label>
62
62
  {upload.data ? (
@@ -86,12 +86,12 @@ function LocalPanel() {
86
86
  <div className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
87
87
  <Card>
88
88
  <CardHeader>
89
- <CardTitle className="flex items-center gap-2"><FolderPlus className="h-4 w-4" /> Connect folder</CardTitle>
90
- <CardDescription>The click is explicit consent; the backend still enforces its permission workflow.</CardDescription>
89
+ <CardTitle className="flex items-center gap-2"><FolderPlus className="h-4 w-4" /> Connect a folder</CardTitle>
90
+ <CardDescription>Point Lattice at a folder you want it to remember.</CardDescription>
91
91
  </CardHeader>
92
92
  <CardContent className="space-y-3">
93
- <Input value={path} onChange={(e) => setPath(e.target.value)} placeholder="/Users/me/Documents/project" />
94
- <Button disabled={!path.trim() || connect.isPending} onClick={() => connect.mutate()}>Connect and watch</Button>
93
+ <Input value={path} onChange={(e) => setPath(e.target.value)} placeholder="Folder path on this Mac" />
94
+ <Button disabled={!path.trim() || connect.isPending} onClick={() => connect.mutate()}>Connect Folder</Button>
95
95
  {connect.data ? <OperationResult result={connect.data} successLabel="Folder connection requested" /> : null}
96
96
  </CardContent>
97
97
  </Card>
@@ -110,7 +110,7 @@ function LocalPanel() {
110
110
  </div>
111
111
  )}
112
112
  </DataPanel>
113
- <DataPanel title="Local runtime probe" result={agent.data} className="xl:col-span-2">
113
+ <DataPanel title="Folder access" result={agent.data} className="xl:col-span-2">
114
114
  {(data) => <StructuredView value={data} />}
115
115
  </DataPanel>
116
116
  </div>
@@ -123,8 +123,8 @@ function BrowserPanel() {
123
123
  return (
124
124
  <Card>
125
125
  <CardHeader>
126
- <CardTitle className="flex items-center gap-2"><Globe2 className="h-4 w-4" /> URL capture</CardTitle>
127
- <CardDescription>Fetches a URL locally through `/api/browser/read-url` and ingests the content with provenance.</CardDescription>
126
+ <CardTitle className="flex items-center gap-2"><Globe2 className="h-4 w-4" /> Save a web page</CardTitle>
127
+ <CardDescription>Capture a page so Lattice can remember the useful parts.</CardDescription>
128
128
  </CardHeader>
129
129
  <CardContent className="space-y-3">
130
130
  <div className="flex flex-col gap-2 sm:flex-row">
@@ -142,16 +142,16 @@ function PipelinePanel() {
142
142
  const stats = useQuery({ queryKey: ["graphStats"], queryFn: latticeApi.graphStats });
143
143
  return (
144
144
  <div className="grid gap-4 xl:grid-cols-2">
145
- <DataPanel title="Index pipeline" result={index.data}>
145
+ <DataPanel title="Processing status" result={index.data}>
146
146
  {(data) => <StructuredView value={data} />}
147
147
  </DataPanel>
148
- <DataPanel title="Graph totals" result={stats.data}>
148
+ <DataPanel title="Brain growth" result={stats.data}>
149
149
  {(data) => <StructuredView value={data} />}
150
150
  </DataPanel>
151
151
  <Card className="xl:col-span-2">
152
152
  <CardHeader>
153
- <CardTitle className="flex items-center gap-2"><HardDrive className="h-4 w-4" /> Rebuild controls</CardTitle>
154
- <CardDescription>Rebuild calls the existing index endpoint. No background work is implied unless the API accepts it.</CardDescription>
153
+ <CardTitle className="flex items-center gap-2"><HardDrive className="h-4 w-4" /> Refresh memory</CardTitle>
154
+ <CardDescription>Refresh search when you want Lattice to re-check captured material.</CardDescription>
155
155
  </CardHeader>
156
156
  <CardContent>
157
157
  <ActionButton label="Rebuild retrieval index" action={() => latticeApi.rebuildIndex()} invalidate={["index"]} />