ltcai 6.7.0 → 7.1.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.
Files changed (65) hide show
  1. package/README.md +29 -31
  2. package/docs/CHANGELOG.md +48 -0
  3. package/frontend/src/App.tsx +80 -0
  4. package/frontend/src/api/client.ts +11 -0
  5. package/frontend/src/components/AdminAccessGate.tsx +70 -0
  6. package/frontend/src/components/BrainConversation.tsx +232 -8
  7. package/frontend/src/components/FeedbackState.tsx +45 -0
  8. package/frontend/src/components/WorkspaceProfileSwitcher.tsx +176 -0
  9. package/frontend/src/components/onboarding/AnalysisScreen.tsx +20 -12
  10. package/frontend/src/components/onboarding/InstallScreen.tsx +55 -0
  11. package/frontend/src/components/onboarding/RecommendationScreen.tsx +55 -5
  12. package/frontend/src/components/onboarding/recommendationModel.ts +59 -2
  13. package/frontend/src/features/brain/BrainConversation.tsx +382 -7
  14. package/frontend/src/features/brain/BrainGraphLayer.tsx +258 -25
  15. package/frontend/src/features/brain/BrainHome.tsx +300 -1
  16. package/frontend/src/features/brain/brainData.ts +25 -1
  17. package/frontend/src/features/brain/graphLayout.ts +12 -1
  18. package/frontend/src/features/brain/types.ts +54 -0
  19. package/frontend/src/i18n.ts +374 -0
  20. package/frontend/src/store/appStore.ts +15 -0
  21. package/frontend/src/styles.css +1383 -41
  22. package/lattice_brain/__init__.py +1 -1
  23. package/lattice_brain/runtime/multi_agent.py +1 -1
  24. package/latticeai/__init__.py +1 -1
  25. package/latticeai/api/workspace.py +59 -0
  26. package/latticeai/core/marketplace.py +1 -1
  27. package/latticeai/core/workspace_os.py +1 -1
  28. package/package.json +1 -1
  29. package/scripts/brain_quality_eval.py +118 -0
  30. package/src-tauri/Cargo.lock +1 -1
  31. package/src-tauri/Cargo.toml +1 -1
  32. package/src-tauri/tauri.conf.json +1 -1
  33. package/static/app/asset-manifest.json +28 -28
  34. package/static/app/assets/Act-BHEb8bAM.js +2 -0
  35. package/static/app/assets/{Act-DGN37eR4.js.map → Act-BHEb8bAM.js.map} +1 -1
  36. package/static/app/assets/Brain-yvR7xkrV.js +322 -0
  37. package/static/app/assets/Brain-yvR7xkrV.js.map +1 -0
  38. package/static/app/assets/Capture-BrAmsSEH.js +2 -0
  39. package/static/app/assets/Capture-BrAmsSEH.js.map +1 -0
  40. package/static/app/assets/Library-BOVzYfxI.js +2 -0
  41. package/static/app/assets/{Library-BbagRFyd.js.map → Library-BOVzYfxI.js.map} +1 -1
  42. package/static/app/assets/System-D4WaN4kj.js +2 -0
  43. package/static/app/assets/System-D4WaN4kj.js.map +1 -0
  44. package/static/app/assets/index-BOB-W1FZ.js +17 -0
  45. package/static/app/assets/index-BOB-W1FZ.js.map +1 -0
  46. package/static/app/assets/index-DvLsGy-z.css +2 -0
  47. package/static/app/assets/primitives-C3_BfUb8.js +2 -0
  48. package/static/app/assets/primitives-C3_BfUb8.js.map +1 -0
  49. package/static/app/assets/textarea-CbvhHvzg.js +2 -0
  50. package/static/app/assets/{textarea-C2PLu6_O.js.map → textarea-CbvhHvzg.js.map} +1 -1
  51. package/static/app/index.html +2 -2
  52. package/static/app/assets/Act-DGN37eR4.js +0 -2
  53. package/static/app/assets/Brain-HDcIwPHW.js +0 -322
  54. package/static/app/assets/Brain-HDcIwPHW.js.map +0 -1
  55. package/static/app/assets/Capture--eZfb_Ex.js +0 -2
  56. package/static/app/assets/Capture--eZfb_Ex.js.map +0 -1
  57. package/static/app/assets/Library-BbagRFyd.js +0 -2
  58. package/static/app/assets/System-C6pd3Mp_.js +0 -2
  59. package/static/app/assets/System-C6pd3Mp_.js.map +0 -1
  60. package/static/app/assets/index-C950aES_.js +0 -17
  61. package/static/app/assets/index-C950aES_.js.map +0 -1
  62. package/static/app/assets/index-ztppMg0c.css +0 -2
  63. package/static/app/assets/primitives-Bcm5lZRu.js +0 -2
  64. package/static/app/assets/primitives-Bcm5lZRu.js.map +0 -1
  65. package/static/app/assets/textarea-C2PLu6_O.js +0 -2
@@ -1,9 +1,9 @@
1
1
  import * as React from "react";
2
- import { Search } from "lucide-react";
2
+ import { Filter, RotateCcw, Search, X } from "lucide-react";
3
3
  import { t } from "@/i18n";
4
4
  import { useAppStore } from "@/store/appStore";
5
5
  import type { KnowledgeConcept, KnowledgeGraphModel } from "./types";
6
- import { clamp, layerStyle, layoutGraphNodes, polarPoint } from "./graphLayout";
6
+ import { clamp, computeGraphNeighbors, layerStyle, layoutGraphNodes, polarPoint } from "./graphLayout";
7
7
 
8
8
  export function BrainKnowledgeLayer({ concepts, depth }: { concepts: KnowledgeConcept[]; depth: number }) {
9
9
  const language = useAppStore((state) => state.language);
@@ -31,6 +31,31 @@ export function BrainKnowledgeLayer({ concepts, depth }: { concepts: KnowledgeCo
31
31
  );
32
32
  }
33
33
 
34
+ const DAY_MS = 24 * 60 * 60 * 1000;
35
+ // Time-exploration presets (days back). null == all time.
36
+ const TIME_WINDOWS: Array<{ days: number | null }> = [{ days: 7 }, { days: 30 }, { days: 90 }, { days: null }];
37
+ // A node added within this window of "now" is treated as recent for highlighting.
38
+ const RECENT_WINDOW_MS = 7 * DAY_MS;
39
+
40
+ // Highlight occurrences of `query` inside `label` by wrapping matches in <mark>.
41
+ function highlightMatch(label: string, query: string): React.ReactNode {
42
+ if (!query) return label;
43
+ const lower = label.toLowerCase();
44
+ const target = query.toLowerCase();
45
+ const parts: React.ReactNode[] = [];
46
+ let cursor = 0;
47
+ let matchIndex = lower.indexOf(target, cursor);
48
+ let key = 0;
49
+ while (matchIndex !== -1 && target.length > 0) {
50
+ if (matchIndex > cursor) parts.push(label.slice(cursor, matchIndex));
51
+ parts.push(<mark key={`m-${key++}`}>{label.slice(matchIndex, matchIndex + target.length)}</mark>);
52
+ cursor = matchIndex + target.length;
53
+ matchIndex = lower.indexOf(target, cursor);
54
+ }
55
+ if (cursor < label.length) parts.push(label.slice(cursor));
56
+ return parts.length ? parts : label;
57
+ }
58
+
34
59
  export function BrainGraphLayer({
35
60
  model,
36
61
  search,
@@ -46,13 +71,53 @@ export function BrainGraphLayer({
46
71
  }) {
47
72
  const language = useAppStore((state) => state.language);
48
73
  const query = search.trim().toLowerCase();
74
+
75
+ // Control state local to the graph view so existing lifted search/selection
76
+ // behavior (and its reset in surface()) is preserved untouched.
77
+ const [activeTypes, setActiveTypes] = React.useState<Set<string>>(() => new Set());
78
+ const [timeDays, setTimeDays] = React.useState<number | null>(null);
79
+ const [typeaheadOpen, setTypeaheadOpen] = React.useState(false);
80
+
81
+ // Distinct entity types available in the graph (for the type filter chips).
82
+ const availableTypes = React.useMemo(() => {
83
+ const seen = new Set<string>();
84
+ for (const node of model.nodes) seen.add(node.type);
85
+ return Array.from(seen).sort((a, b) => a.localeCompare(b));
86
+ }, [model.nodes]);
87
+
88
+ // Drop type selections that no longer exist after a graph refresh.
89
+ React.useEffect(() => {
90
+ setActiveTypes((prev) => {
91
+ if (prev.size === 0) return prev;
92
+ const next = new Set<string>();
93
+ for (const type of prev) if (availableTypes.includes(type)) next.add(type);
94
+ return next.size === prev.size ? prev : next;
95
+ });
96
+ }, [availableTypes]);
97
+
98
+ const hasTimestamps = React.useMemo(() => model.nodes.some((node) => typeof node.createdAt === "number"), [model.nodes]);
99
+ const timeCutoff = timeDays === null ? null : Date.now() - timeDays * DAY_MS;
100
+
101
+ const filtersActive = activeTypes.size > 0 || timeDays !== null;
102
+
49
103
  const visibleNodes = React.useMemo(() => {
50
104
  const filtered = model.nodes.filter((node) => {
105
+ if (activeTypes.size > 0 && !activeTypes.has(node.type)) return false;
106
+ if (timeCutoff !== null && typeof node.createdAt === "number" && node.createdAt < timeCutoff) return false;
51
107
  if (!query) return true;
52
108
  return `${node.label} ${node.type} ${node.summary}`.toLowerCase().includes(query);
53
109
  });
54
110
  return filtered.slice(0, 18);
111
+ }, [model.nodes, query, activeTypes, timeCutoff]);
112
+
113
+ // Typeahead suggestions: label-prefix/substring matches ranked by importance.
114
+ const suggestions = React.useMemo(() => {
115
+ if (!query) return [];
116
+ return model.nodes
117
+ .filter((node) => node.label.toLowerCase().includes(query) || node.type.toLowerCase().includes(query))
118
+ .slice(0, 6);
55
119
  }, [model.nodes, query]);
120
+
56
121
  const layout = React.useMemo(() => layoutGraphNodes(visibleNodes, 38, 24), [visibleNodes]);
57
122
  const positionById = React.useMemo(() => new Map(layout.map((item) => [item.node.id, item])), [layout]);
58
123
  const visibleEdges = React.useMemo(
@@ -61,6 +126,24 @@ export function BrainGraphLayer({
61
126
  );
62
127
  const selected = visibleNodes.find((node) => node.id === selectedId) || visibleNodes[0] || null;
63
128
 
129
+ // 1-hop neighbor set for the focused node, drives focus/dim + edge highlight.
130
+ const neighborIds = React.useMemo(
131
+ () => (selectedId ? computeGraphNeighbors(selectedId, model.edges) : new Set<string>()),
132
+ [selectedId, model.edges],
133
+ );
134
+ const focusActive = Boolean(selectedId) && neighborIds.size > 0;
135
+
136
+ const matchedIds = React.useMemo(() => {
137
+ if (!query) return new Set<string>();
138
+ return new Set(visibleNodes.filter((node) => node.label.toLowerCase().includes(query)).map((node) => node.id));
139
+ }, [visibleNodes, query]);
140
+
141
+ function focusState(nodeId: string): "true" | "false" | undefined {
142
+ if (!focusActive) return undefined;
143
+ if (nodeId === selectedId || neighborIds.has(nodeId)) return "true";
144
+ return "false";
145
+ }
146
+
64
147
  return (
65
148
  <section className="mind-core-graph" data-testid="emergent-knowledge-graph" aria-label={t(language, "brain.aria.graph")}>
66
149
  <div className="brain-graph-head">
@@ -68,24 +151,159 @@ export function BrainGraphLayer({
68
151
  <span>{t(language, "brain.level")} 5</span>
69
152
  <strong>{t(language, "brain.depth.5")}</strong>
70
153
  </div>
71
- <label className="brain-graph-search">
72
- <Search className="h-3.5 w-3.5" />
73
- <input
74
- value={search}
75
- onChange={(event) => onSearch(event.target.value)}
76
- placeholder={t(language, "brain.graph.search")}
77
- aria-label={t(language, "brain.graph.searchAria")}
78
- />
79
- </label>
154
+ <div className="brain-graph-search-wrap">
155
+ <label className="brain-graph-search">
156
+ <Search className="h-3.5 w-3.5" />
157
+ <input
158
+ value={search}
159
+ onChange={(event) => {
160
+ onSearch(event.target.value);
161
+ setTypeaheadOpen(true);
162
+ }}
163
+ onFocus={() => setTypeaheadOpen(true)}
164
+ onBlur={() => window.setTimeout(() => setTypeaheadOpen(false), 120)}
165
+ placeholder={t(language, "brain.graph.search")}
166
+ aria-label={t(language, "brain.graph.searchAria")}
167
+ role="combobox"
168
+ aria-expanded={typeaheadOpen && Boolean(query)}
169
+ aria-autocomplete="list"
170
+ aria-controls="brain-graph-typeahead"
171
+ />
172
+ {search ? (
173
+ <button
174
+ type="button"
175
+ className="brain-graph-search-clear"
176
+ onClick={() => onSearch("")}
177
+ aria-label={t(language, "brain.graph.focus.clear")}
178
+ >
179
+ <X className="h-3 w-3" />
180
+ </button>
181
+ ) : null}
182
+ </label>
183
+ {typeaheadOpen && query ? (
184
+ <ul
185
+ id="brain-graph-typeahead"
186
+ className="brain-graph-typeahead"
187
+ role="listbox"
188
+ aria-label={t(language, "brain.graph.search.typeaheadAria")}
189
+ >
190
+ {suggestions.length ? (
191
+ suggestions.map((node) => (
192
+ <li key={node.id} role="option" aria-selected={node.id === selectedId}>
193
+ <button
194
+ type="button"
195
+ onMouseDown={(event) => {
196
+ event.preventDefault();
197
+ onSearch(node.label);
198
+ onSelect(node.id);
199
+ setTypeaheadOpen(false);
200
+ }}
201
+ >
202
+ <span>{node.type}</span>
203
+ <strong>{highlightMatch(node.label, query)}</strong>
204
+ </button>
205
+ </li>
206
+ ))
207
+ ) : (
208
+ <li className="is-empty" aria-disabled>
209
+ {t(language, "brain.graph.search.noMatches")}
210
+ </li>
211
+ )}
212
+ </ul>
213
+ ) : null}
214
+ </div>
215
+ </div>
216
+
217
+ <div className="brain-graph-control-panel" role="group" aria-label={t(language, "brain.graph.control.panel")}>
218
+ <div className="brain-graph-filter-row">
219
+ <span className="brain-graph-control-label">
220
+ <Filter className="h-3 w-3" aria-hidden />
221
+ {t(language, "brain.graph.control.types")}
222
+ </span>
223
+ <div className="brain-graph-type-chips" role="group" aria-label={t(language, "brain.graph.control.types")}>
224
+ <button
225
+ type="button"
226
+ className={`brain-graph-chip ${activeTypes.size === 0 ? "is-active" : ""}`}
227
+ aria-pressed={activeTypes.size === 0}
228
+ onClick={() => setActiveTypes(new Set())}
229
+ >
230
+ {t(language, "brain.graph.control.allTypes")}
231
+ </button>
232
+ {availableTypes.map((type) => {
233
+ const active = activeTypes.has(type);
234
+ return (
235
+ <button
236
+ key={type}
237
+ type="button"
238
+ className={`brain-graph-chip ${active ? "is-active" : ""}`}
239
+ aria-pressed={active}
240
+ aria-label={t(language, "brain.graph.control.toggleType", { type })}
241
+ onClick={() =>
242
+ setActiveTypes((prev) => {
243
+ const next = new Set(prev);
244
+ if (next.has(type)) next.delete(type);
245
+ else next.add(type);
246
+ return next;
247
+ })
248
+ }
249
+ >
250
+ {type}
251
+ </button>
252
+ );
253
+ })}
254
+ </div>
255
+ </div>
256
+
257
+ <div className="brain-graph-filter-row">
258
+ <span className="brain-graph-control-label">{t(language, "brain.graph.control.time")}</span>
259
+ <div className="brain-graph-time-chips" role="group" aria-label={t(language, "brain.graph.control.dateRange")}>
260
+ {TIME_WINDOWS.map((window) => {
261
+ const active = timeDays === window.days;
262
+ const label =
263
+ window.days === null
264
+ ? t(language, "brain.graph.control.allTime")
265
+ : t(language, "brain.graph.control.recentWindow", { days: window.days });
266
+ return (
267
+ <button
268
+ key={window.days ?? "all"}
269
+ type="button"
270
+ className={`brain-graph-chip ${active ? "is-active" : ""}`}
271
+ aria-pressed={active}
272
+ disabled={window.days !== null && !hasTimestamps}
273
+ onClick={() => setTimeDays(window.days)}
274
+ >
275
+ {label}
276
+ </button>
277
+ );
278
+ })}
279
+ </div>
280
+ {filtersActive ? (
281
+ <button
282
+ type="button"
283
+ className="brain-graph-reset"
284
+ onClick={() => {
285
+ setActiveTypes(new Set());
286
+ setTimeDays(null);
287
+ }}
288
+ >
289
+ <RotateCcw className="h-3 w-3" aria-hidden />
290
+ {t(language, "brain.graph.control.resetFilters")}
291
+ </button>
292
+ ) : null}
293
+ </div>
80
294
  </div>
81
295
 
82
296
  {visibleNodes.length ? (
83
- <div className="brain-graph-canvas">
297
+ <div className="brain-graph-canvas" data-focus-active={focusActive ? "true" : "false"}>
84
298
  <svg className="brain-graph-edges" viewBox="0 0 100 100" aria-hidden>
85
299
  {visibleEdges.map((edge, index) => {
86
300
  const source = positionById.get(edge.source);
87
301
  const target = positionById.get(edge.target);
88
302
  if (!source || !target) return null;
303
+ const touchesFocus =
304
+ focusActive && (edge.source === selectedId || edge.target === selectedId);
305
+ const touchesMatch = matchedIds.has(edge.source) || matchedIds.has(edge.target);
306
+ const highlight = touchesFocus || (query ? touchesMatch : false);
89
307
  return (
90
308
  <line
91
309
  key={`${edge.id}-${index}`}
@@ -93,23 +311,32 @@ export function BrainGraphLayer({
93
311
  y1={source.y}
94
312
  x2={target.x}
95
313
  y2={target.y}
314
+ data-highlight={
315
+ focusActive || query ? (highlight ? "true" : "false") : undefined
316
+ }
96
317
  style={{ "--weight": String(clamp(edge.weight, 0.4, 2.8)) } as React.CSSProperties}
97
318
  />
98
319
  );
99
320
  })}
100
321
  </svg>
101
- {layout.map(({ node, x, y }, index) => (
102
- <button
103
- key={node.id}
104
- type="button"
105
- className={`graph-node ${selected?.id === node.id ? "is-selected" : ""}`}
106
- style={layerStyle({ "--x": `${x}%`, "--y": `${y}%`, "--delay": `${index * 35}ms` })}
107
- onClick={() => onSelect(node.id)}
108
- >
109
- <span>{node.type}</span>
110
- {node.label}
111
- </button>
112
- ))}
322
+ {layout.map(({ node, x, y }, index) => {
323
+ const isRecent =
324
+ typeof node.createdAt === "number" && Date.now() - node.createdAt <= RECENT_WINDOW_MS;
325
+ return (
326
+ <button
327
+ key={node.id}
328
+ type="button"
329
+ className={`graph-node ${selected?.id === node.id ? "is-selected" : ""} ${matchedIds.has(node.id) ? "is-match" : ""}`}
330
+ data-focus={focusState(node.id)}
331
+ data-recent={isRecent ? "true" : undefined}
332
+ style={layerStyle({ "--x": `${x}%`, "--y": `${y}%`, "--delay": `${index * 35}ms` })}
333
+ onClick={() => onSelect(selectedId === node.id ? null : node.id)}
334
+ >
335
+ <span>{node.type}</span>
336
+ {highlightMatch(node.label, query)}
337
+ </button>
338
+ );
339
+ })}
113
340
  </div>
114
341
  ) : (
115
342
  <div className="brain-graph-empty">{t(language, "brain.graph.empty")}</div>
@@ -121,7 +348,13 @@ export function BrainGraphLayer({
121
348
  <span>{selected.type}</span>
122
349
  <strong>{selected.label}</strong>
123
350
  <p>{selected.summary || t(language, "brain.graph.summaryFallback")}</p>
124
- <p>{t(language, "brain.graph.focused")}</p>
351
+ {focusActive ? (
352
+ <p className="brain-graph-focus-meta" role="status">
353
+ {t(language, "brain.graph.focus.neighbors", { count: neighborIds.size })}
354
+ </p>
355
+ ) : (
356
+ <p>{t(language, "brain.graph.focused")}</p>
357
+ )}
125
358
  </>
126
359
  ) : (
127
360
  <p>{t(language, "brain.graph.emptyFocus")}</p>