ltcai 4.7.0 → 4.7.2

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.
@@ -9,6 +9,7 @@ import {
9
9
  Download,
10
10
  Eye,
11
11
  ImagePlus,
12
+ ListFilter,
12
13
  RotateCcw,
13
14
  Search,
14
15
  Send,
@@ -16,7 +17,7 @@ import {
16
17
  ShieldCheck,
17
18
  Users,
18
19
  } from "lucide-react";
19
- import { latticeApi, type ApiResult } from "@/api/client";
20
+ import { latticeApi, type AdminAuditFilters, type ApiResult } from "@/api/client";
20
21
  import { Button } from "@/components/ui/button";
21
22
  import { type BrainState, LivingBrain, triggerBrainRecall } from "@/components/LivingBrain";
22
23
  import { ProductFlow, readProductFlowComplete } from "@/components/ProductFlow";
@@ -25,6 +26,7 @@ import { asArray } from "@/lib/utils";
25
26
 
26
27
  type ApiRecord = Record<string, unknown>;
27
28
  type BrainDepth = 1 | 2 | 3 | 4 | 5;
29
+ type AdminFilterState = Required<Pick<AdminAuditFilters, "q" | "actor" | "action" | "severity">> & { limit: number };
28
30
 
29
31
  type Message = {
30
32
  role: "user" | "assistant";
@@ -159,6 +161,7 @@ function BrainHome({
159
161
  const [explorationDepth, setExplorationDepth] = React.useState<BrainDepth>(1);
160
162
  const [graphSearch, setGraphSearch] = React.useState("");
161
163
  const [selectedGraphId, setSelectedGraphId] = React.useState<string | null>(null);
164
+ const [memoryFeedback, setMemoryFeedback] = React.useState<string | null>(null);
162
165
  const streamRef = React.useRef<HTMLDivElement>(null);
163
166
  const recallTimerRef = React.useRef<number | null>(null);
164
167
 
@@ -210,6 +213,7 @@ function BrainHome({
210
213
  setDraft("");
211
214
  setImageData(null);
212
215
  setStreaming(true);
216
+ setMemoryFeedback(null);
213
217
  onBrainChange("thinking", 0.96);
214
218
 
215
219
  try {
@@ -238,6 +242,8 @@ function BrainHome({
238
242
  next[next.length - 1] = { role: "assistant", content: `Unavailable: ${result.error}` };
239
243
  return next;
240
244
  });
245
+ } else {
246
+ setMemoryFeedback(`기억에 저장됨 · 연결된 주제 ${knowledgeConcepts.length}개 · 관련 기억 ${memoryFragments.length}개`);
241
247
  }
242
248
  } finally {
243
249
  setStreaming(false);
@@ -257,6 +263,13 @@ function BrainHome({
257
263
  });
258
264
  }
259
265
 
266
+ function jumpToDepth(next: BrainDepth) {
267
+ setExplorationDepth(next);
268
+ const nextDepth = DEPTHS[next - 1];
269
+ onBrainChange(nextDepth.state, next === 1 ? 0.58 : 0.66 + next * 0.06);
270
+ if (next >= 2) triggerBrainRecall();
271
+ }
272
+
260
273
  function surface() {
261
274
  setExplorationDepth(1);
262
275
  setSelectedGraphId(null);
@@ -269,7 +282,7 @@ function BrainHome({
269
282
  setExplorationDepth((depth) => Math.max(depth, 2) as BrainDepth);
270
283
  setMessages((items) => [
271
284
  ...items,
272
- { role: "assistant", content: `I am recalling ${fragment.kind.toLowerCase()}: ${fragment.title}` },
285
+ { role: "assistant", content: `기억을 다시 꺼냈습니다: ${fragment.title}` },
273
286
  ]);
274
287
  }
275
288
 
@@ -291,6 +304,13 @@ function BrainHome({
291
304
  <strong>{currentDepth.label}</strong>
292
305
  </div>
293
306
 
307
+ <div className="brain-depth-actions" aria-label="Brain quick views">
308
+ <button type="button" className={explorationDepth === 2 ? "is-active" : ""} onClick={() => jumpToDepth(2)}>기억 보기</button>
309
+ <button type="button" className={explorationDepth === 3 ? "is-active" : ""} onClick={() => jumpToDepth(3)}>주제 보기</button>
310
+ <button type="button" className={explorationDepth === 4 ? "is-active" : ""} onClick={() => jumpToDepth(4)}>관계 보기</button>
311
+ <button type="button" className={explorationDepth === 5 ? "is-active" : ""} onClick={() => jumpToDepth(5)}>그래프로 보기</button>
312
+ </div>
313
+
294
314
  <div className="brain-field-layer" aria-hidden={explorationDepth < 2}>
295
315
  <DepthEmergence
296
316
  depth={explorationDepth}
@@ -333,12 +353,17 @@ function BrainHome({
333
353
  </div>
334
354
 
335
355
  <div ref={streamRef} className="brain-stream">
356
+ <BrainOverviewPanel
357
+ memories={memoryFragments}
358
+ concepts={knowledgeConcepts}
359
+ onOpenDepth={jumpToDepth}
360
+ />
336
361
  {messages.length === 0 ? (
337
362
  <div className="mind-empty">
338
- <div className="mind-empty-kicker">Your durable context</div>
339
- <div className="mind-empty-title">Start with what should not be forgotten.</div>
363
+ <div className="mind-empty-kicker">내 오래가는 기억</div>
364
+ <div className="mind-empty-title">잊으면 되는 일부터 말해 주세요.</div>
340
365
  <p>
341
- Lattice keeps your documents, conversations, projects, and decisions available while models can change around them.
366
+ 문서, 대화, 프로젝트, 결정이 Brain에 쌓이고 나중에 주제와 관계로 다시 보입니다.
342
367
  </p>
343
368
  <div className="mind-empty-prompts" aria-label="Starter prompts">
344
369
  {STARTER_PROMPTS.map((prompt) => (
@@ -357,6 +382,8 @@ function BrainHome({
357
382
  )}
358
383
  </div>
359
384
 
385
+ {memoryFeedback ? <div className="brain-save-feedback" role="status">{memoryFeedback}</div> : null}
386
+
360
387
  <BrainCarePanel />
361
388
 
362
389
  <div className="brain-composer">
@@ -398,14 +425,8 @@ function BrainHome({
398
425
 
399
426
  function AdminConsole({ onBack }: { onBack: () => void }) {
400
427
  const qc = useQueryClient();
401
- const summaryQ = useQuery({ queryKey: ["adminSummary"], queryFn: latticeApi.adminSummary });
402
- const statsQ = useQuery({ queryKey: ["adminStats"], queryFn: latticeApi.adminStats });
403
- const usersQ = useQuery({ queryKey: ["adminUsers"], queryFn: latticeApi.adminUsers });
404
- const auditQ = useQuery({ queryKey: ["adminAudit"], queryFn: latticeApi.adminAudit });
405
- const securityQ = useQuery({ queryKey: ["adminSecurity"], queryFn: latticeApi.adminSecurity });
406
- const securityEventsQ = useQuery({ queryKey: ["adminSecurityEvents"], queryFn: () => latticeApi.adminSecurityEvents(50) });
407
- const policiesQ = useQuery({ queryKey: ["adminPolicies"], queryFn: latticeApi.adminPolicies });
408
- const indexQ = useQuery({ queryKey: ["indexStatus"], queryFn: latticeApi.indexStatus });
428
+ const [filters, setFilters] = React.useState<AdminFilterState>({ q: "", actor: "", action: "", severity: "", limit: 50 });
429
+ const { summaryQ, statsQ, usersQ, auditQ, securityQ, securityEventsQ, policiesQ, rolesQ, retentionQ, indexQ } = useAdminConsoleData(filters);
409
430
  const rebuildIndex = useMutation({
410
431
  mutationFn: latticeApi.rebuildIndex,
411
432
  onSuccess: () => void qc.invalidateQueries({ queryKey: ["indexStatus"] }),
@@ -415,6 +436,8 @@ function AdminConsole({ onBack }: { onBack: () => void }) {
415
436
  const auditEvents = asArray((auditQ.data?.data as ApiRecord | undefined)?.recent_events);
416
437
  const securityEvents = asArray((securityEventsQ.data?.data as ApiRecord | undefined)?.events);
417
438
  const policies = asArray((policiesQ.data?.data as ApiRecord | undefined)?.policies);
439
+ const roles = asArray((rolesQ.data?.data as ApiRecord | undefined)?.roles);
440
+ const retention = (retentionQ.data?.data || {}) as ApiRecord;
418
441
 
419
442
  return (
420
443
  <main className="admin-console" aria-label="Lattice Admin">
@@ -469,7 +492,24 @@ function AdminConsole({ onBack }: { onBack: () => void }) {
469
492
  />
470
493
  </AdminPanel>
471
494
 
495
+ <AdminPanel title="Role Permissions" eyebrow="Access">
496
+ <AdminList
497
+ items={roles.slice(0, 6)}
498
+ empty="No role matrix reported."
499
+ render={(item) => {
500
+ const role = item as ApiRecord;
501
+ return (
502
+ <>
503
+ <strong>{stringValue(role.role, "role")} · {stringValue(role.members, "0")} users</strong>
504
+ <span>{asArray(role.caps).slice(0, 4).map((cap) => stringValue(cap, "")).filter(Boolean).join(", ") || "No caps"}</span>
505
+ </>
506
+ );
507
+ }}
508
+ />
509
+ </AdminPanel>
510
+
472
511
  <AdminPanel title="Activity Logs" eyebrow="Audit">
512
+ <AdminLogFilters filters={filters} onChange={setFilters} matched={(auditQ.data?.data as ApiRecord | undefined)?.filters as ApiRecord | undefined} />
473
513
  <AdminList
474
514
  items={auditEvents.slice(0, 8)}
475
515
  empty="No recent audit events."
@@ -499,16 +539,82 @@ function AdminConsole({ onBack }: { onBack: () => void }) {
499
539
  <div className="admin-policy-strip">
500
540
  {policies.slice(0, 5).map((item, index) => {
501
541
  const policy = item as ApiRecord;
502
- return <span key={`${stringValue(policy.id || policy.name, "policy")}-${index}`}>{stringValue(policy.name || policy.id, "Policy")}</span>;
542
+ return <span key={`${stringValue(policy.id || policy.name, "policy")}-${index}`}>{stringValue(policy.label || policy.name || policy.id, "Policy")}</span>;
503
543
  })}
504
544
  {!policies.length ? <span>Policy API quiet</span> : null}
505
545
  </div>
546
+ <div className="admin-retention">
547
+ <strong>{stringValue(retention.retention_days, "90")} day retention</strong>
548
+ <span>{stringValue(retention.retained_events, "0")} retained · {stringValue(retention.prune_candidates, "0")} ready for export/prune review</span>
549
+ </div>
506
550
  </AdminPanel>
507
551
  </section>
508
552
  </main>
509
553
  );
510
554
  }
511
555
 
556
+ function useAdminConsoleData(filters: AdminFilterState) {
557
+ const auditFilters = React.useMemo<AdminAuditFilters>(() => ({
558
+ q: filters.q || undefined,
559
+ actor: filters.actor || undefined,
560
+ action: filters.action || undefined,
561
+ severity: filters.severity || undefined,
562
+ limit: filters.limit,
563
+ }), [filters]);
564
+
565
+ return {
566
+ summaryQ: useQuery({ queryKey: ["adminSummary"], queryFn: latticeApi.adminSummary }),
567
+ statsQ: useQuery({ queryKey: ["adminStats"], queryFn: latticeApi.adminStats }),
568
+ usersQ: useQuery({ queryKey: ["adminUsers"], queryFn: latticeApi.adminUsers }),
569
+ auditQ: useQuery({ queryKey: ["adminAudit", auditFilters], queryFn: () => latticeApi.adminAudit(auditFilters) }),
570
+ securityQ: useQuery({ queryKey: ["adminSecurity"], queryFn: latticeApi.adminSecurity }),
571
+ securityEventsQ: useQuery({ queryKey: ["adminSecurityEvents"], queryFn: () => latticeApi.adminSecurityEvents(50) }),
572
+ policiesQ: useQuery({ queryKey: ["adminPolicies"], queryFn: latticeApi.adminPolicies }),
573
+ rolesQ: useQuery({ queryKey: ["adminRoles"], queryFn: latticeApi.adminRoles }),
574
+ retentionQ: useQuery({ queryKey: ["adminLogRetention"], queryFn: latticeApi.adminLogRetention }),
575
+ indexQ: useQuery({ queryKey: ["indexStatus"], queryFn: latticeApi.indexStatus }),
576
+ };
577
+ }
578
+
579
+ function AdminLogFilters({
580
+ filters,
581
+ matched,
582
+ onChange,
583
+ }: {
584
+ filters: AdminFilterState;
585
+ matched?: ApiRecord;
586
+ onChange: React.Dispatch<React.SetStateAction<AdminFilterState>>;
587
+ }) {
588
+ return (
589
+ <div className="admin-log-filters" aria-label="Audit log filters">
590
+ <label>
591
+ <Search className="h-3.5 w-3.5" />
592
+ <input
593
+ value={filters.q}
594
+ onChange={(event) => onChange((current) => ({ ...current, q: event.target.value }))}
595
+ placeholder="Search logs"
596
+ aria-label="Search audit logs"
597
+ />
598
+ </label>
599
+ <label>
600
+ <ListFilter className="h-3.5 w-3.5" />
601
+ <select
602
+ value={filters.severity}
603
+ onChange={(event) => onChange((current) => ({ ...current, severity: event.target.value }))}
604
+ aria-label="Filter by severity"
605
+ >
606
+ <option value="">All severities</option>
607
+ <option value="informational">Informational</option>
608
+ <option value="notice">Notice</option>
609
+ <option value="warning">Warning</option>
610
+ <option value="high">High</option>
611
+ </select>
612
+ </label>
613
+ <span>{stringValue(matched?.matched_events, "0")} matched</span>
614
+ </div>
615
+ );
616
+ }
617
+
512
618
  function AdminMetric({ icon, label, value, detail }: { icon: React.ReactNode; label: string; value: string; detail: string }) {
513
619
  return (
514
620
  <div className="admin-metric">
@@ -757,6 +863,75 @@ function DepthEmergence({
757
863
  );
758
864
  }
759
865
 
866
+ function BrainOverviewPanel({
867
+ memories,
868
+ concepts,
869
+ onOpenDepth,
870
+ }: {
871
+ memories: MemoryFragment[];
872
+ concepts: KnowledgeConcept[];
873
+ onOpenDepth: (depth: BrainDepth) => void;
874
+ }) {
875
+ const recent = memories.slice(0, 3);
876
+ const older = memories.slice(3, 6);
877
+ const topics = concepts.slice(0, 4);
878
+
879
+ return (
880
+ <section className="brain-overview-panel" aria-label="Brain overview">
881
+ <div className="brain-overview-head">
882
+ <div>
883
+ <span>Brain 한눈에 보기</span>
884
+ <strong>기억과 주제를 바로 확인하세요.</strong>
885
+ </div>
886
+ <button type="button" onClick={() => onOpenDepth(5)}>전체 그래프</button>
887
+ </div>
888
+ <div className="brain-overview-grid">
889
+ <BrainOverviewColumn
890
+ title="최근 기억"
891
+ empty="아직 최근 기억이 없습니다."
892
+ items={recent.map((memory) => memory.title)}
893
+ onOpen={() => onOpenDepth(2)}
894
+ />
895
+ <BrainOverviewColumn
896
+ title="이전 기억"
897
+ empty="대화가 쌓이면 과거 기억이 보입니다."
898
+ items={older.map((memory) => memory.title)}
899
+ onOpen={() => onOpenDepth(2)}
900
+ />
901
+ <BrainOverviewColumn
902
+ title="주요 주제"
903
+ empty="주제가 형성되는 중입니다."
904
+ items={topics.map((concept) => concept.label)}
905
+ onOpen={() => onOpenDepth(3)}
906
+ />
907
+ </div>
908
+ </section>
909
+ );
910
+ }
911
+
912
+ function BrainOverviewColumn({
913
+ title,
914
+ empty,
915
+ items,
916
+ onOpen,
917
+ }: {
918
+ title: string;
919
+ empty: string;
920
+ items: string[];
921
+ onOpen: () => void;
922
+ }) {
923
+ return (
924
+ <button type="button" className="brain-overview-column" onClick={onOpen}>
925
+ <span>{title}</span>
926
+ {items.length ? (
927
+ items.slice(0, 3).map((item) => <strong key={item}>{item}</strong>)
928
+ ) : (
929
+ <em>{empty}</em>
930
+ )}
931
+ </button>
932
+ );
933
+ }
934
+
760
935
  function MemoryLayer({
761
936
  memories,
762
937
  depth,
@@ -945,9 +1120,10 @@ function EmergentKnowledgeGraph({
945
1120
  <span>{selected.type}</span>
946
1121
  <strong>{selected.label}</strong>
947
1122
  <p>{selected.summary || "This concept is part of the deepest knowledge layer."}</p>
1123
+ <p>대화와 문서에서 함께 나온 내용이 선으로 이어집니다.</p>
948
1124
  </>
949
1125
  ) : (
950
- <p>Capture documents, conversations, or projects to grow the graph.</p>
1126
+ <p>대화, 문서, 프로젝트를 쌓으면 Brain 그래프가 자랍니다.</p>
951
1127
  )}
952
1128
  </div>
953
1129
  </section>
@@ -12,6 +12,13 @@ export type ApiResult<T = unknown> = {
12
12
 
13
13
  type HttpMethod = "GET" | "POST" | "PATCH" | "DELETE";
14
14
  type Query = Record<string, string | number | boolean | null | undefined>;
15
+ export type AdminAuditFilters = {
16
+ q?: string;
17
+ actor?: string;
18
+ action?: string;
19
+ severity?: string;
20
+ limit?: number;
21
+ };
15
22
 
16
23
  const TIMEOUT_MS = 10_000;
17
24
  const clients = new Map<string, ReturnType<typeof createClient<paths>>>();
@@ -419,9 +426,10 @@ export const latticeApi = {
419
426
  adminSummary: () => get("/admin/summary", {}),
420
427
  adminStats: () => get("/admin/stats", {}),
421
428
  adminUsers: () => get("/admin/users", []),
422
- adminAudit: () => get("/admin/audit", { recent_events: [] }),
429
+ adminAudit: (filters?: AdminAuditFilters) => get("/admin/audit", { recent_events: [], filters: {} }, filters),
423
430
  adminRoles: () => get("/admin/roles", { roles: [] }),
424
431
  adminPolicies: () => get("/admin/policies", { policies: [] }),
432
+ adminLogRetention: () => get("/admin/log-retention", {}),
425
433
  adminProductHardening: () => get("/admin/product-hardening", {}),
426
434
  adminSecurity: () => get("/admin/security/overview", {}),
427
435
  adminSecurityEvents: (limit = 50) => get("/admin/security/events", { events: [] }, { limit }),