ltcai 8.0.0 → 8.2.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 (183) hide show
  1. package/README.md +33 -72
  2. package/docs/CHANGELOG.md +52 -874
  3. package/docs/DEVELOPMENT.md +26 -3
  4. package/docs/LEGACY_COMPATIBILITY.md +3 -1
  5. package/docs/TRUST_MODEL.md +26 -26
  6. package/docs/WHY_LATTICE.md +28 -25
  7. package/docs/kg-schema.md +6 -4
  8. package/lattice_brain/__init__.py +1 -1
  9. package/lattice_brain/embeddings.py +9 -1
  10. package/lattice_brain/graph/schema.py +6 -0
  11. package/lattice_brain/runtime/multi_agent.py +1 -1
  12. package/latticeai/__init__.py +1 -1
  13. package/latticeai/api/computer_use.py +2 -0
  14. package/latticeai/api/memory.py +13 -0
  15. package/latticeai/app_factory.py +43 -162
  16. package/latticeai/brain/__init__.py +1 -1
  17. package/latticeai/core/config.py +10 -0
  18. package/latticeai/core/local_embeddings.py +8 -0
  19. package/latticeai/core/marketplace.py +1 -1
  20. package/latticeai/core/mcp_registry.py +140 -1
  21. package/latticeai/core/tool_registry.py +10 -0
  22. package/latticeai/core/workspace_graph_trace.py +132 -0
  23. package/latticeai/core/workspace_memory.py +75 -0
  24. package/latticeai/core/workspace_os.py +135 -1283
  25. package/latticeai/core/workspace_os_utils.py +132 -0
  26. package/latticeai/core/workspace_permissions.py +99 -0
  27. package/latticeai/core/workspace_plugins.py +97 -0
  28. package/latticeai/core/workspace_runs.py +612 -0
  29. package/latticeai/core/workspace_skills.py +114 -0
  30. package/latticeai/core/workspace_snapshots.py +195 -0
  31. package/latticeai/core/workspace_timeline.py +107 -0
  32. package/latticeai/runtime/audit_runtime.py +64 -0
  33. package/latticeai/runtime/config_runtime.py +4 -0
  34. package/latticeai/runtime/sso_runtime.py +52 -0
  35. package/latticeai/services/architecture_readiness.py +6 -5
  36. package/latticeai/services/memory_service.py +215 -7
  37. package/latticeai/services/model_capability_registry.py +68 -0
  38. package/latticeai/services/model_engines.py +603 -0
  39. package/latticeai/services/model_loading.py +482 -0
  40. package/latticeai/services/model_recommendation.py +5 -0
  41. package/latticeai/services/model_runtime.py +146 -837
  42. package/latticeai/services/product_readiness.py +19 -18
  43. package/package.json +2 -2
  44. package/scripts/build_frontend_assets.mjs +11 -2
  45. package/src-tauri/Cargo.lock +1 -1
  46. package/src-tauri/Cargo.toml +1 -1
  47. package/src-tauri/tauri.conf.json +1 -1
  48. package/static/app/asset-manifest.json +11 -102
  49. package/static/app/assets/Act-D9jIknFd.js +1 -0
  50. package/static/app/assets/Brain-CFOtWbPN.js +321 -0
  51. package/static/app/assets/{Capture-B9Tlhzqr.js → Capture-Q4WYzwr5.js} +1 -2
  52. package/static/app/assets/Library-C5Q2yWee.js +1 -0
  53. package/static/app/assets/System-BLbjdr1_.js +1 -0
  54. package/static/app/assets/core-CwxXejkd.js +1 -2
  55. package/static/app/assets/index-BqammyNu.js +16 -0
  56. package/static/app/assets/index-ty1iGgZu.css +2 -0
  57. package/static/app/assets/primitives-Br8uSfZ4.js +1 -0
  58. package/static/app/assets/{textarea-BZk6ybp5.js → textarea-BnhNs1_X.js} +1 -2
  59. package/static/app/index.html +3 -10
  60. package/static/app/theme-boot.js +8 -0
  61. package/static/sw.js +1 -1
  62. package/tools/__init__.py +2 -1
  63. package/tools/computer.py +21 -0
  64. package/docs/CARRYOVER_AUDIT_v3.6.0.md +0 -61
  65. package/docs/HANDOVER_v3.6.0.md +0 -46
  66. package/docs/RUNTIME_HOOK_COVERAGE_v3.5.0.md +0 -56
  67. package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +0 -49
  68. package/docs/V2_ARCHITECTURE.md +0 -561
  69. package/docs/V3_2_AUDIT.md +0 -82
  70. package/docs/V3_BACKEND_ARCHITECTURE.md +0 -138
  71. package/docs/V3_FRONTEND.md +0 -140
  72. package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +0 -65
  73. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +0 -70
  74. package/docs/V4_1_VALIDATION_REPORT.md +0 -47
  75. package/docs/V4_2_BRAIN_CORE_ARCHITECTURE.md +0 -97
  76. package/docs/V4_2_STORAGE_MIGRATION_REPORT.md +0 -91
  77. package/docs/V4_2_VALIDATION_REPORT.md +0 -89
  78. package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +0 -174
  79. package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +0 -81
  80. package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +0 -75
  81. package/docs/V4_3_2_GRAPH_UX_REPORT.md +0 -48
  82. package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +0 -209
  83. package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +0 -57
  84. package/docs/V4_3_2_SELF_AUDIT_REPORT.md +0 -63
  85. package/docs/V4_3_2_VALIDATION_REPORT.md +0 -97
  86. package/docs/V4_3_3_VALIDATION_REPORT.md +0 -46
  87. package/docs/V4_3_PORTABILITY_ARCHITECTURE.md +0 -69
  88. package/docs/V4_3_PRIVACY_AUDIT.md +0 -60
  89. package/docs/V4_3_PRODUCT_HARDENING_REPORT.md +0 -53
  90. package/docs/V4_3_VALIDATION_REPORT.md +0 -58
  91. package/docs/V4_4_0_EXTRACTION_REPORT.md +0 -239
  92. package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +0 -49
  93. package/docs/V4_5_0_GRAPH_UX_REPORT.md +0 -34
  94. package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +0 -40
  95. package/docs/V4_5_0_ONBOARDING_REPORT.md +0 -31
  96. package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +0 -49
  97. package/docs/V4_5_0_VALIDATION_REPORT.md +0 -60
  98. package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +0 -33
  99. package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +0 -37
  100. package/docs/V4_5_1_NAVIGATION_REPORT.md +0 -37
  101. package/docs/V4_5_1_ONBOARDING_REPORT.md +0 -29
  102. package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +0 -61
  103. package/docs/V4_5_1_RC_ARTIFACTS.md +0 -44
  104. package/docs/V4_5_1_UX_REPORT.md +0 -45
  105. package/docs/V4_5_1_VALIDATION_REPORT.md +0 -55
  106. package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +0 -30
  107. package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +0 -72
  108. package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +0 -42
  109. package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +0 -42
  110. package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +0 -49
  111. package/docs/V4_7_2_INTUITIVE_BRAIN_UX_REPORT.md +0 -62
  112. package/docs/V4_BRAIN_ARCHITECTURE.md +0 -322
  113. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +0 -555
  114. package/docs/V4_IMPLEMENTATION_PLAN.md +0 -470
  115. package/frontend/index.html +0 -24
  116. package/frontend/openapi.json +0 -15425
  117. package/frontend/src/App.tsx +0 -243
  118. package/frontend/src/api/client.ts +0 -580
  119. package/frontend/src/api/openapi.ts +0 -17892
  120. package/frontend/src/components/AdminAccessGate.tsx +0 -70
  121. package/frontend/src/components/FeedbackState.tsx +0 -45
  122. package/frontend/src/components/LanguageSwitcher.tsx +0 -23
  123. package/frontend/src/components/LivingBrain.tsx +0 -220
  124. package/frontend/src/components/ProductFlow.tsx +0 -171
  125. package/frontend/src/components/WorkspaceProfileSwitcher.tsx +0 -176
  126. package/frontend/src/components/onboarding/AnalysisScreen.tsx +0 -135
  127. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +0 -29
  128. package/frontend/src/components/onboarding/InstallScreen.tsx +0 -263
  129. package/frontend/src/components/onboarding/LanguageChooser.tsx +0 -23
  130. package/frontend/src/components/onboarding/LoginScreen.tsx +0 -131
  131. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +0 -13
  132. package/frontend/src/components/onboarding/RecommendationScreen.tsx +0 -116
  133. package/frontend/src/components/onboarding/recommendationModel.ts +0 -189
  134. package/frontend/src/components/primitives.tsx +0 -392
  135. package/frontend/src/components/ui/badge.tsx +0 -27
  136. package/frontend/src/components/ui/button.tsx +0 -37
  137. package/frontend/src/components/ui/card.tsx +0 -22
  138. package/frontend/src/components/ui/input.tsx +0 -16
  139. package/frontend/src/components/ui/textarea.tsx +0 -16
  140. package/frontend/src/features/admin/AdminConsole.tsx +0 -334
  141. package/frontend/src/features/brain/BrainCarePanel.tsx +0 -254
  142. package/frontend/src/features/brain/BrainComposer.tsx +0 -85
  143. package/frontend/src/features/brain/BrainConversation.tsx +0 -688
  144. package/frontend/src/features/brain/BrainGraphLayer.tsx +0 -365
  145. package/frontend/src/features/brain/BrainHome.tsx +0 -624
  146. package/frontend/src/features/brain/BrainMemoryLayer.tsx +0 -45
  147. package/frontend/src/features/brain/BrainOverviewPanel.tsx +0 -149
  148. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +0 -43
  149. package/frontend/src/features/brain/DepthEmergence.tsx +0 -53
  150. package/frontend/src/features/brain/brainData.ts +0 -246
  151. package/frontend/src/features/brain/graphLayout.ts +0 -37
  152. package/frontend/src/features/brain/types.ts +0 -150
  153. package/frontend/src/features/review/ReviewCard.tsx +0 -100
  154. package/frontend/src/features/review/ReviewInbox.tsx +0 -127
  155. package/frontend/src/features/review/reviewHelpers.ts +0 -69
  156. package/frontend/src/i18n.ts +0 -1303
  157. package/frontend/src/lib/utils.ts +0 -33
  158. package/frontend/src/main.tsx +0 -23
  159. package/frontend/src/pages/Act.tsx +0 -458
  160. package/frontend/src/pages/Ask.tsx +0 -14
  161. package/frontend/src/pages/Brain.tsx +0 -914
  162. package/frontend/src/pages/Capture.tsx +0 -258
  163. package/frontend/src/pages/Library.tsx +0 -486
  164. package/frontend/src/pages/System.tsx +0 -621
  165. package/frontend/src/routes.ts +0 -92
  166. package/frontend/src/store/appStore.ts +0 -94
  167. package/frontend/src/styles.css +0 -6579
  168. package/static/app/assets/Act-DOvf59ru.js +0 -2
  169. package/static/app/assets/Act-DOvf59ru.js.map +0 -1
  170. package/static/app/assets/Brain-C7_0mEiI.js +0 -322
  171. package/static/app/assets/Brain-C7_0mEiI.js.map +0 -1
  172. package/static/app/assets/Capture-B9Tlhzqr.js.map +0 -1
  173. package/static/app/assets/Library-BJPEEm5O.js +0 -2
  174. package/static/app/assets/Library-BJPEEm5O.js.map +0 -1
  175. package/static/app/assets/System-D6t9jo9V.js +0 -2
  176. package/static/app/assets/System-D6t9jo9V.js.map +0 -1
  177. package/static/app/assets/core-CwxXejkd.js.map +0 -1
  178. package/static/app/assets/index-C7g26IF6.css +0 -2
  179. package/static/app/assets/index-DbcEYJQ2.js +0 -17
  180. package/static/app/assets/index-DbcEYJQ2.js.map +0 -1
  181. package/static/app/assets/primitives-CD38lt4n.js +0 -2
  182. package/static/app/assets/primitives-CD38lt4n.js.map +0 -1
  183. package/static/app/assets/textarea-BZk6ybp5.js.map +0 -1
@@ -1,334 +0,0 @@
1
- import * as React from "react";
2
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { Activity, ArrowLeft, ListFilter, RotateCcw, Search, ServerCog, ShieldCheck, Users } from "lucide-react";
4
- import { latticeApi, type AdminAuditFilters, type ApiResult } from "@/api/client";
5
- import { Button } from "@/components/ui/button";
6
- import { LanguageSwitcher } from "@/components/LanguageSwitcher";
7
- import { useAppStore } from "@/store/appStore";
8
- import { asArray } from "@/lib/utils";
9
- import { t } from "@/i18n";
10
-
11
- type ApiRecord = Record<string, unknown>;
12
- type AdminFilterState = Required<Pick<AdminAuditFilters, "q" | "actor" | "action" | "severity">> & { limit: number };
13
-
14
- export function AdminConsole({ onBack }: { onBack: () => void }) {
15
- const qc = useQueryClient();
16
- const language = useAppStore((state) => state.language);
17
- const [filters, setFilters] = React.useState<AdminFilterState>({ q: "", actor: "", action: "", severity: "", limit: 50 });
18
- const { summaryQ, statsQ, usersQ, auditQ, securityQ, securityEventsQ, policiesQ, rolesQ, retentionQ, indexQ, agentRuntimeQ, toolRegistryQ } = useAdminConsoleData(filters);
19
- const rebuildIndex = useMutation({
20
- mutationFn: latticeApi.rebuildIndex,
21
- onSuccess: () => void qc.invalidateQueries({ queryKey: ["indexStatus"] }),
22
- });
23
-
24
- const users = asArray(usersQ.data?.data);
25
- const auditEvents = asArray((auditQ.data?.data as ApiRecord | undefined)?.recent_events);
26
- const securityEvents = asArray((securityEventsQ.data?.data as ApiRecord | undefined)?.events);
27
- const policies = asArray((policiesQ.data?.data as ApiRecord | undefined)?.policies);
28
- const roles = asArray((rolesQ.data?.data as ApiRecord | undefined)?.roles);
29
- const retention = (retentionQ.data?.data || {}) as ApiRecord;
30
-
31
- return (
32
- <main className="admin-console" aria-label={t(language, "admin.aria.console")}>
33
- <header className="admin-console-header">
34
- <button className="admin-back-button" type="button" onClick={onBack}>
35
- <ArrowLeft className="h-4 w-4" />
36
- {t(language, "admin.back")}
37
- </button>
38
- <div>
39
- <span>{t(language, "admin.kicker")}</span>
40
- <h1>{t(language, "admin.title")}</h1>
41
- <p>{t(language, "admin.body")}</p>
42
- </div>
43
- <LanguageSwitcher compact />
44
- </header>
45
-
46
- <section className="admin-metrics" aria-label={t(language, "admin.overview")}>
47
- <AdminMetric icon={<Users className="h-4 w-4" />} label={t(language, "admin.metric.users")} value={String(users.length)} detail={sourceLabel(usersQ.data, language)} />
48
- <AdminMetric
49
- icon={<Activity className="h-4 w-4" />}
50
- label={t(language, "admin.metric.logs")}
51
- value={String(auditEvents.length + securityEvents.length)}
52
- detail={sourceLabel(auditQ.data, language)}
53
- />
54
- <AdminMetric
55
- icon={<ShieldCheck className="h-4 w-4" />}
56
- label={t(language, "admin.metric.security")}
57
- value={adminStatusLabel(securityQ.data?.data, "status") || (securityQ.data?.ok ? t(language, "admin.status.ready") : t(language, "admin.status.unavailable"))}
58
- detail={sourceLabel(securityQ.data, language)}
59
- />
60
- <AdminMetric
61
- icon={<ServerCog className="h-4 w-4" />}
62
- label={t(language, "admin.metric.index")}
63
- value={adminStatusLabel(indexQ.data?.data, "status") || (indexQ.data?.ok ? t(language, "admin.status.indexed") : t(language, "admin.status.unknown"))}
64
- detail={indexDetail(indexQ.data?.data, language)}
65
- />
66
- </section>
67
-
68
- <section className="admin-grid">
69
- <AdminPanel title={t(language, "admin.panel.users")} eyebrow={t(language, "admin.panel.people")}>
70
- <AdminList
71
- items={users.slice(0, 8)}
72
- empty={t(language, "admin.empty.users")}
73
- render={(item) => {
74
- const user = item as ApiRecord;
75
- return (
76
- <>
77
- <strong>{stringValue(user.name || user.email || user.id, t(language, "admin.fallback.localUser"))}</strong>
78
- <span>{stringValue(user.role || user.status || user.workspace_id, t(language, "admin.fallback.member"))}</span>
79
- </>
80
- );
81
- }}
82
- />
83
- </AdminPanel>
84
-
85
- <AdminPanel title={t(language, "admin.panel.roles")} eyebrow={t(language, "admin.panel.access")}>
86
- <AdminList
87
- items={roles.slice(0, 6)}
88
- empty={t(language, "admin.empty.roles")}
89
- render={(item) => {
90
- const role = item as ApiRecord;
91
- return (
92
- <>
93
- <strong>{stringValue(role.role, t(language, "admin.fallback.role"))} · {stringValue(role.members, "0")} {t(language, "admin.metric.users")}</strong>
94
- <span>{asArray(role.caps).slice(0, 4).map((cap) => stringValue(cap, "")).filter(Boolean).join(", ") || t(language, "admin.fallback.noCaps")}</span>
95
- </>
96
- );
97
- }}
98
- />
99
- </AdminPanel>
100
-
101
- <AdminPanel title={t(language, "admin.panel.logs")} eyebrow={t(language, "admin.panel.audit")}>
102
- <AdminLogFilters language={language} filters={filters} onChange={setFilters} matched={(auditQ.data?.data as ApiRecord | undefined)?.filters as ApiRecord | undefined} />
103
- <AdminList
104
- items={auditEvents.slice(0, 8)}
105
- empty={t(language, "admin.empty.audit")}
106
- render={(item) => renderLogRow(item as ApiRecord, language)}
107
- />
108
- </AdminPanel>
109
-
110
- <AdminPanel title={t(language, "admin.panel.securityEvents")} eyebrow={t(language, "admin.panel.protection")}>
111
- <AdminList
112
- items={securityEvents.slice(0, 8)}
113
- empty={t(language, "admin.empty.security")}
114
- render={(item) => renderLogRow(item as ApiRecord, language)}
115
- />
116
- </AdminPanel>
117
-
118
- <AdminPanel title={t(language, "admin.panel.brainOps")} eyebrow={t(language, "admin.panel.maintenance")}>
119
- <div className="admin-operation">
120
- <div>
121
- <strong>{indexDetail(indexQ.data?.data, language)}</strong>
122
- <span>{summaryText(summaryQ.data?.data) || summaryText(statsQ.data?.data) || t(language, "admin.brain.summaryFallback")}</span>
123
- </div>
124
- <Button variant="outline" size="sm" disabled={rebuildIndex.isPending} onClick={() => rebuildIndex.mutate()}>
125
- <RotateCcw className="h-3.5 w-3.5" />
126
- {rebuildIndex.isPending ? t(language, "admin.brain.rebuilding") : t(language, "admin.brain.rebuild")}
127
- </Button>
128
- </div>
129
- <div className="admin-policy-strip">
130
- {policies.slice(0, 5).map((item, index) => {
131
- const policy = item as ApiRecord;
132
- return <span key={`${stringValue(policy.id || policy.name, "policy")}-${index}`}>{stringValue(policy.label || policy.name || policy.id, t(language, "admin.policy.fallback"))}</span>;
133
- })}
134
- {!policies.length ? <span>{t(language, "admin.policy.quiet")}</span> : null}
135
- </div>
136
- <div className="admin-retention">
137
- <strong>{t(language, "admin.retention.days", { days: stringValue(retention.retention_days, "90") })}</strong>
138
- <span>{t(language, "admin.retention.detail", { events: stringValue(retention.retained_events, "0"), candidates: stringValue(retention.prune_candidates, "0") })}</span>
139
- </div>
140
- </AdminPanel>
141
-
142
- <AdminPanel title={t(language, "admin.panel.runtimeTrust")} eyebrow={t(language, "admin.panel.contracts")}>
143
- <RuntimeTrustPanel runtime={agentRuntimeQ.data?.data as ApiRecord | undefined} registry={toolRegistryQ.data?.data as ApiRecord | undefined} language={language} />
144
- </AdminPanel>
145
-
146
- </section>
147
- </main>
148
- );
149
- }
150
-
151
- function useAdminConsoleData(filters: AdminFilterState) {
152
- const auditFilters = React.useMemo<AdminAuditFilters>(() => ({
153
- q: filters.q || undefined,
154
- actor: filters.actor || undefined,
155
- action: filters.action || undefined,
156
- severity: filters.severity || undefined,
157
- limit: filters.limit,
158
- }), [filters]);
159
-
160
- return {
161
- summaryQ: useQuery({ queryKey: ["adminSummary"], queryFn: latticeApi.adminSummary }),
162
- statsQ: useQuery({ queryKey: ["adminStats"], queryFn: latticeApi.adminStats }),
163
- usersQ: useQuery({ queryKey: ["adminUsers"], queryFn: latticeApi.adminUsers }),
164
- auditQ: useQuery({ queryKey: ["adminAudit", auditFilters], queryFn: () => latticeApi.adminAudit(auditFilters) }),
165
- securityQ: useQuery({ queryKey: ["adminSecurity"], queryFn: latticeApi.adminSecurity }),
166
- securityEventsQ: useQuery({ queryKey: ["adminSecurityEvents"], queryFn: () => latticeApi.adminSecurityEvents(50) }),
167
- policiesQ: useQuery({ queryKey: ["adminPolicies"], queryFn: latticeApi.adminPolicies }),
168
- rolesQ: useQuery({ queryKey: ["adminRoles"], queryFn: latticeApi.adminRoles }),
169
- retentionQ: useQuery({ queryKey: ["adminLogRetention"], queryFn: latticeApi.adminLogRetention }),
170
- indexQ: useQuery({ queryKey: ["indexStatus"], queryFn: latticeApi.indexStatus }),
171
- agentRuntimeQ: useQuery({ queryKey: ["agentRuntime"], queryFn: latticeApi.agentRuntime }),
172
- toolRegistryQ: useQuery({ queryKey: ["toolRegistryDiagnostics"], queryFn: latticeApi.toolRegistryDiagnostics }),
173
- };
174
- }
175
-
176
- function RuntimeTrustPanel({ runtime, registry, language }: { runtime?: ApiRecord; registry?: ApiRecord; language: "ko" | "en" }) {
177
- const runtimeInfo = (runtime?.runtime || {}) as ApiRecord;
178
- const health = (runtime?.health || {}) as ApiRecord;
179
- const diagnostics = (registry?.diagnostics || {}) as ApiRecord;
180
- const ready = Boolean(runtimeInfo.ready);
181
- const registryReady = Boolean(diagnostics.ready);
182
- const blocking = stringValue(runtimeInfo.unavailable_reason, ready ? t(language, "admin.runtime.readyDetail") : t(language, "admin.runtime.blockedFallback"));
183
- return (
184
- <div className="admin-runtime-trust">
185
- <div className="admin-runtime-row">
186
- <strong>{t(language, "admin.runtime.agent")}</strong>
187
- <span className={ready ? "is-ok" : "is-warn"}>{ready ? t(language, "admin.status.ready") : t(language, "admin.status.unavailable")}</span>
188
- <small>{blocking}</small>
189
- </div>
190
- <div className="admin-runtime-row">
191
- <strong>{t(language, "admin.runtime.tools")}</strong>
192
- <span className={registryReady ? "is-ok" : "is-warn"}>{registryReady ? t(language, "admin.runtime.aligned") : t(language, "admin.runtime.drift")}</span>
193
- <small>
194
- {t(language, "admin.runtime.toolCounts", {
195
- registered: stringValue(diagnostics.registered_tools, "0"),
196
- governed: stringValue(diagnostics.governed_tools, "0"),
197
- described: stringValue(diagnostics.described_tools, "0"),
198
- })}
199
- </small>
200
- </div>
201
- <div className="admin-policy-strip">
202
- <span>{t(language, "admin.runtime.mode", { mode: stringValue(runtimeInfo.mode, "unknown") })}</span>
203
- <span>{t(language, "admin.runtime.execution", { mode: stringValue(runtimeInfo.execution_mode, "unknown") })}</span>
204
- <span>{t(language, "admin.runtime.health", { status: stringValue(health.status, "unknown") })}</span>
205
- </div>
206
- </div>
207
- );
208
- }
209
-
210
- function AdminLogFilters({
211
- filters,
212
- matched,
213
- onChange,
214
- language,
215
- }: {
216
- filters: AdminFilterState;
217
- matched?: ApiRecord;
218
- onChange: React.Dispatch<React.SetStateAction<AdminFilterState>>;
219
- language: "ko" | "en";
220
- }) {
221
- return (
222
- <div className="admin-log-filters" aria-label={t(language, "admin.filters.label")}>
223
- <label>
224
- <Search className="h-3.5 w-3.5" />
225
- <input
226
- value={filters.q}
227
- onChange={(event) => onChange((current) => ({ ...current, q: event.target.value }))}
228
- placeholder={t(language, "admin.filters.search")}
229
- aria-label={t(language, "admin.filters.searchAria")}
230
- />
231
- </label>
232
- <label>
233
- <ListFilter className="h-3.5 w-3.5" />
234
- <select
235
- value={filters.severity}
236
- onChange={(event) => onChange((current) => ({ ...current, severity: event.target.value }))}
237
- aria-label={t(language, "admin.filters.severityAria")}
238
- >
239
- <option value="">{t(language, "admin.filters.all")}</option>
240
- <option value="informational">{t(language, "admin.filters.informational")}</option>
241
- <option value="notice">{t(language, "admin.filters.notice")}</option>
242
- <option value="warning">{t(language, "admin.filters.warning")}</option>
243
- <option value="high">{t(language, "admin.filters.high")}</option>
244
- </select>
245
- </label>
246
- <span>{t(language, "admin.filters.matched", { count: stringValue(matched?.matched_events, "0") })}</span>
247
- </div>
248
- );
249
- }
250
-
251
- function AdminMetric({ icon, label, value, detail }: { icon: React.ReactNode; label: string; value: string; detail: string }) {
252
- return (
253
- <div className="admin-metric">
254
- <div>{icon}</div>
255
- <span>{label}</span>
256
- <strong>{value}</strong>
257
- <small>{detail}</small>
258
- </div>
259
- );
260
- }
261
-
262
- function AdminPanel({ eyebrow, title, children }: { eyebrow: string; title: string; children: React.ReactNode }) {
263
- return (
264
- <section className="admin-panel">
265
- <div className="admin-panel-head">
266
- <span>{eyebrow}</span>
267
- <h2>{title}</h2>
268
- </div>
269
- {children}
270
- </section>
271
- );
272
- }
273
-
274
- function AdminList({ items, empty, render }: { items: unknown[]; empty: string; render: (item: unknown) => React.ReactNode }) {
275
- if (!items.length) return <div className="admin-empty">{empty}</div>;
276
- return <div className="admin-list">{items.map((item, index) => <div key={index} className="admin-list-row">{render(item)}</div>)}</div>;
277
- }
278
-
279
- function renderLogRow(event: ApiRecord, language: "ko" | "en") {
280
- const action = stringValue(event.action || event.event || event.type || event.name, t(language, "admin.log.event"));
281
- const actor = stringValue(event.actor || event.user || event.user_id || event.workspace_id, t(language, "admin.log.system"));
282
- const when = stringValue(event.timestamp || event.time || event.created_at || event.ts, t(language, "admin.log.recently"));
283
- return (
284
- <>
285
- <strong>{action}</strong>
286
- <span>{actor} · {when}</span>
287
- </>
288
- );
289
- }
290
-
291
- function sourceLabel(result: ApiResult<unknown> | undefined, language: "ko" | "en") {
292
- if (!result) return t(language, "admin.source.loading");
293
- return result.ok ? t(language, "admin.source.live") : result.error || t(language, "admin.status.unavailable");
294
- }
295
-
296
- function adminStatusLabel(data: unknown, key: string) {
297
- const record = isRecord(data) ? data : {};
298
- return textValue(record, [key, "health", "state", "overall_status"]);
299
- }
300
-
301
- function indexDetail(data: unknown, language: "ko" | "en") {
302
- const record = isRecord(data) ? data : {};
303
- const docs = record.documents ?? record.document_count ?? record.docs;
304
- const chunks = record.chunks ?? record.chunk_count ?? record.vectors;
305
- if (docs !== undefined || chunks !== undefined) {
306
- return `${stringValue(docs, "0")} docs · ${stringValue(chunks, "0")} chunks`;
307
- }
308
- return textValue(record, ["message", "detail", "status"], t(language, "admin.index.ready"));
309
- }
310
-
311
- function summaryText(data: unknown) {
312
- const record = isRecord(data) ? data : {};
313
- return textValue(record, ["summary", "message", "status", "detail"]);
314
- }
315
-
316
- function stringValue(value: unknown, fallback = "") {
317
- if (typeof value === "string" && value.trim()) return value;
318
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
319
- if (typeof value === "boolean") return value ? "true" : "false";
320
- return fallback;
321
- }
322
-
323
- function isRecord(value: unknown): value is ApiRecord {
324
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
325
- }
326
-
327
- function textValue(record: ApiRecord, keys: string[], fallback = "") {
328
- for (const key of keys) {
329
- const value = record[key];
330
- if (typeof value === "string" && value.trim()) return value;
331
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
332
- }
333
- return fallback;
334
- }
@@ -1,254 +0,0 @@
1
- import * as React from "react";
2
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { Archive, ChevronDown, DatabaseBackup, Download, Eye, RotateCcw, ShieldCheck } from "lucide-react";
4
- import { latticeApi, type ApiResult } from "@/api/client";
5
- import { Button } from "@/components/ui/button";
6
- import { t, type Language } from "@/i18n";
7
- import { isRecord, textValue } from "./brainData";
8
-
9
- export function BrainCarePanel({ language }: { language: Language }) {
10
- const qc = useQueryClient();
11
- const [expanded, setExpanded] = React.useState(false);
12
- const [archivePath, setArchivePath] = React.useState("");
13
- const [passphrase, setPassphrase] = React.useState("");
14
- const [passphraseConfirm, setPassphraseConfirm] = React.useState("");
15
- const [latestResult, setLatestResult] = React.useState<ApiResult | null>(null);
16
- const portabilityQ = useQuery({ queryKey: ["portability"], queryFn: latticeApi.graphPortability });
17
- const backupHealthQ = useQuery({ queryKey: ["backupHealth"], queryFn: latticeApi.backupHealth });
18
- const rememberResult = React.useCallback((result: ApiResult) => setLatestResult(result), []);
19
- const pathProblem = archivePath.trim() ? archivePathProblem(archivePath.trim(), language) : "";
20
- const passphraseProblem = passphrase.trim() ? passphraseStrengthProblem(passphrase, language) : "";
21
- const passphrasesMatch = !passphrase || passphrase === passphraseConfirm;
22
- const canArchive = Boolean(passphrase.trim()) && passphrasesMatch && !passphraseProblem;
23
- const canUseExistingArchive = Boolean(archivePath.trim()) && !pathProblem;
24
-
25
- const exportGraph = useCareMutation(() => latticeApi.graphExport(), undefined, rememberResult);
26
- const backupGraph = useCareMutation(() => latticeApi.graphBackup(), () => {
27
- void qc.invalidateQueries({ queryKey: ["backupHealth"] });
28
- void qc.invalidateQueries({ queryKey: ["portability"] });
29
- }, rememberResult);
30
- const archiveBrain = useCareMutation(
31
- () => latticeApi.brainArchive({ path: archivePath.trim() || null, passphrase }),
32
- () => void qc.invalidateQueries({ queryKey: ["backupHealth"] }),
33
- rememberResult,
34
- );
35
- const inspectArchive = useCareMutation(() => latticeApi.brainArchiveInspect({
36
- path: archivePath.trim(),
37
- passphrase: passphrase || null,
38
- }), undefined, rememberResult);
39
- const restorePreview = useCareMutation(() => latticeApi.brainArchiveRestore({
40
- path: archivePath.trim(),
41
- passphrase,
42
- dry_run: true,
43
- confirm: false,
44
- }), undefined, rememberResult);
45
-
46
- const portableFormat = portabilityLabel(portabilityQ.data?.data);
47
- const backupStatus = backupHealthLabel(backupHealthQ.data?.data, language);
48
-
49
- return (
50
- <section className={`brain-care-panel ${expanded ? "is-expanded" : "is-collapsed"}`} aria-label={t(language, "care.title")}>
51
- <button
52
- className="brain-care-summary"
53
- type="button"
54
- aria-expanded={expanded}
55
- aria-controls="brain-care-details"
56
- onClick={() => setExpanded((value) => !value)}
57
- >
58
- <span className="brain-care-summary-main">
59
- <span><ShieldCheck className="h-3.5 w-3.5" /> {t(language, "care.title")}</span>
60
- <strong>{t(language, "care.subtitle")}</strong>
61
- </span>
62
- <div className="brain-care-proof" aria-label={t(language, "care.ownershipModel")}>
63
- <span>{t(language, "care.private")}</span>
64
- <span>{portableFormat}</span>
65
- <span>{backupStatus}</span>
66
- </div>
67
- <ChevronDown className="brain-care-toggle h-4 w-4" aria-hidden="true" />
68
- </button>
69
-
70
- {expanded ? (
71
- <div id="brain-care-details" className="brain-care-details">
72
- <div className="brain-care-actions">
73
- <CareButton
74
- icon={<Download className="h-3.5 w-3.5" />}
75
- label={t(language, "care.export")}
76
- detail={t(language, "care.export.detail")}
77
- pendingLabel={t(language, "care.working")}
78
- pending={exportGraph.isPending}
79
- onClick={() => exportGraph.mutate()}
80
- />
81
- <CareButton
82
- icon={<DatabaseBackup className="h-3.5 w-3.5" />}
83
- label={t(language, "care.backup")}
84
- detail={t(language, "care.backup.detail")}
85
- pendingLabel={t(language, "care.working")}
86
- pending={backupGraph.isPending}
87
- onClick={() => backupGraph.mutate()}
88
- />
89
- <CareButton
90
- icon={<Archive className="h-3.5 w-3.5" />}
91
- label={t(language, "care.archive")}
92
- detail={t(language, "care.archive.detail")}
93
- pendingLabel={t(language, "care.working")}
94
- pending={archiveBrain.isPending}
95
- disabled={!canArchive}
96
- onClick={() => archiveBrain.mutate()}
97
- />
98
- </div>
99
-
100
- <div className="brain-care-archive">
101
- <input
102
- value={archivePath}
103
- onChange={(event) => setArchivePath(event.target.value)}
104
- placeholder={t(language, "care.path.placeholder")}
105
- aria-label={t(language, "care.path.label")}
106
- />
107
- <input
108
- type="password"
109
- value={passphrase}
110
- onChange={(event) => setPassphrase(event.target.value)}
111
- placeholder={t(language, "care.passphrase.placeholder")}
112
- aria-label={t(language, "care.passphrase.label")}
113
- />
114
- <input
115
- type="password"
116
- value={passphraseConfirm}
117
- onChange={(event) => setPassphraseConfirm(event.target.value)}
118
- placeholder={t(language, "care.passphrase.confirmPlaceholder")}
119
- aria-label={t(language, "care.passphrase.confirmLabel")}
120
- />
121
- <div className="brain-care-guidance" role="status">
122
- {pathProblem ? <span className="is-error">{pathProblem}</span> : archivePath.trim() ? <span>{t(language, "care.path.ready")}</span> : null}
123
- {passphraseProblem ? <span className="is-error">{passphraseProblem}</span> : passphrase ? <span>{t(language, "care.passphrase.strong")}</span> : null}
124
- {!passphrasesMatch ? <span className="is-error">{t(language, "care.passphrase.mismatch")}</span> : null}
125
- </div>
126
- <div className="brain-care-archive-actions">
127
- <Button
128
- variant="outline"
129
- size="sm"
130
- disabled={!canUseExistingArchive || inspectArchive.isPending}
131
- onClick={() => inspectArchive.mutate()}
132
- >
133
- <Eye className="h-3.5 w-3.5" /> {t(language, "care.inspect")}
134
- </Button>
135
- <Button
136
- variant="outline"
137
- size="sm"
138
- disabled={!canUseExistingArchive || !passphrase.trim() || !passphrasesMatch || restorePreview.isPending}
139
- onClick={() => restorePreview.mutate()}
140
- >
141
- <RotateCcw className="h-3.5 w-3.5" /> {t(language, "care.restorePreview")}
142
- </Button>
143
- </div>
144
- </div>
145
-
146
- {latestResult ? (
147
- <div className={`brain-care-result ${latestResult.ok ? "is-ok" : "is-error"}`} role="status">
148
- <CareResultSummary result={latestResult} language={language} />
149
- </div>
150
- ) : (
151
- <p className="brain-care-note">
152
- {t(language, "care.note")}
153
- </p>
154
- )}
155
- </div>
156
- ) : null}
157
- </section>
158
- );
159
- }
160
-
161
- function useCareMutation<T extends ApiResult>(
162
- mutationFn: () => Promise<T>,
163
- onSuccess?: () => void,
164
- onResult?: (result: T) => void,
165
- ) {
166
- return useMutation({
167
- mutationFn,
168
- onSuccess: (result) => {
169
- onResult?.(result);
170
- onSuccess?.();
171
- },
172
- });
173
- }
174
-
175
- function CareButton({
176
- icon,
177
- label,
178
- detail,
179
- pendingLabel,
180
- pending,
181
- disabled,
182
- onClick,
183
- }: {
184
- icon: React.ReactNode;
185
- label: string;
186
- detail: string;
187
- pendingLabel: string;
188
- pending?: boolean;
189
- disabled?: boolean;
190
- onClick: () => void;
191
- }) {
192
- return (
193
- <button className="brain-care-button" type="button" disabled={disabled || pending} onClick={onClick}>
194
- {icon}
195
- <span>
196
- <strong>{pending ? pendingLabel : label}</strong>
197
- <small>{detail}</small>
198
- </span>
199
- </button>
200
- );
201
- }
202
-
203
- function portabilityLabel(data: unknown) {
204
- const record = isRecord(data) ? data : {};
205
- return textValue(record, ["archive_format", "format", "graph_schema_version", "schema_version"], ".latticebrain");
206
- }
207
-
208
- function backupHealthLabel(data: unknown, language: Language) {
209
- const record = isRecord(data) ? data : {};
210
- const count = record.count || record.backups || record.available;
211
- if (count !== undefined && count !== null && count !== "") return t(language, "care.backup.count", { count: String(count) });
212
- return t(language, "care.backup.ready");
213
- }
214
-
215
- function CareResultSummary({ result, language }: { result: ApiResult; language: Language }) {
216
- if (!result.ok) return <>{result.error || t(language, "care.result.error")}</>;
217
- const data = isRecord(result.data) ? result.data : {};
218
- const path = textValue(data, ["path", "archive_path", "backup_path", "export_path", "latest_backup"]);
219
- const status = textValue(data, ["message", "status", "result"], t(language, "care.result.completed"));
220
- const counts = {
221
- memories: textValue(data, ["nodes", "total_nodes", "memories", "memory_count"]),
222
- links: textValue(data, ["edges", "total_edges", "relationships", "relationship_count"]),
223
- conversations: textValue(data, ["conversations", "conversation_count"]),
224
- };
225
- return (
226
- <div className="brain-care-result-summary">
227
- <strong>{status}</strong>
228
- {path ? <span>{t(language, "care.result.path", { path })}</span> : null}
229
- {counts.memories || counts.links || counts.conversations ? (
230
- <span>
231
- {t(language, "care.result.contents", {
232
- memories: counts.memories || "0",
233
- links: counts.links || "0",
234
- conversations: counts.conversations || "0",
235
- })}
236
- </span>
237
- ) : null}
238
- {data.dry_run === true ? <span>{t(language, "care.result.dryRun")}</span> : null}
239
- </div>
240
- );
241
- }
242
-
243
- function archivePathProblem(path: string, language: Language) {
244
- if (/[<>:"|?*]/.test(path)) return t(language, "care.path.invalidChars");
245
- if (!/\.(latticebrain|zip|json)$/i.test(path)) return t(language, "care.path.extension");
246
- return "";
247
- }
248
-
249
- function passphraseStrengthProblem(value: string, language: Language) {
250
- if (value.length < 12) return t(language, "care.passphrase.tooShort");
251
- const variety = [/[a-z]/, /[A-Z]/, /\d/, /[^A-Za-z0-9]/].filter((pattern) => pattern.test(value)).length;
252
- if (variety < 3) return t(language, "care.passphrase.tooSimple");
253
- return "";
254
- }
@@ -1,85 +0,0 @@
1
- import * as React from "react";
2
- import { FileUp, ImagePlus, Send } from "lucide-react";
3
- import { Button } from "@/components/ui/button";
4
- import { t, type Language } from "@/i18n";
5
-
6
- export function BrainComposer({
7
- language,
8
- draft,
9
- streaming,
10
- imageData,
11
- uploadingDocument,
12
- onDraftChange,
13
- onImageDataChange,
14
- onUploadDocument,
15
- onSend,
16
- }: {
17
- language: Language;
18
- draft: string;
19
- streaming: boolean;
20
- imageData: string | null;
21
- uploadingDocument: boolean;
22
- onDraftChange: (value: string) => void;
23
- onImageDataChange: (value: string | null) => void;
24
- onUploadDocument: (file: File) => void;
25
- onSend: () => void;
26
- }) {
27
- return (
28
- <div className="brain-composer">
29
- <textarea
30
- value={draft}
31
- onChange={(event) => onDraftChange(event.target.value)}
32
- onKeyDown={(event) => {
33
- if (event.key === "Enter" && !event.shiftKey) {
34
- event.preventDefault();
35
- onSend();
36
- }
37
- }}
38
- placeholder={t(language, "brain.placeholder")}
39
- />
40
- <div className="brain-composer-actions">
41
- <label className={`brain-document-input ${uploadingDocument ? "is-disabled" : ""}`}>
42
- <FileUp className="h-3.5 w-3.5" />
43
- <span>{uploadingDocument ? t(language, "brain.upload.uploading") : t(language, "brain.upload.ctaShort")}</span>
44
- <input
45
- type="file"
46
- accept=".pdf,.docx,.xlsx,.pptx,.txt,.md,.csv,application/pdf,text/plain,text/markdown,text/csv"
47
- className="sr-only"
48
- disabled={uploadingDocument}
49
- onChange={(event) => {
50
- const file = event.target.files?.[0];
51
- event.currentTarget.value = "";
52
- if (file) onUploadDocument(file);
53
- }}
54
- />
55
- </label>
56
- <label className="brain-image-input">
57
- <ImagePlus className="h-3.5 w-3.5" />
58
- <span>{t(language, "brain.image")}</span>
59
- <input
60
- type="file"
61
- accept="image/*"
62
- className="sr-only"
63
- onChange={async (event) => {
64
- const file = event.target.files?.[0];
65
- if (file) onImageDataChange(await fileToDataUrl(file));
66
- }}
67
- />
68
- </label>
69
- {imageData ? <span className="brain-quiet-success">{t(language, "brain.imageAttached")}</span> : null}
70
- <Button onClick={onSend} disabled={!draft.trim() || streaming} className="rounded-full px-5">
71
- <Send className="h-4 w-4" /> {t(language, "brain.send")}
72
- </Button>
73
- </div>
74
- </div>
75
- );
76
- }
77
-
78
- function fileToDataUrl(file: File) {
79
- return new Promise<string>((resolve, reject) => {
80
- const reader = new FileReader();
81
- reader.onload = () => resolve(String(reader.result || ""));
82
- reader.onerror = () => reject(reader.error);
83
- reader.readAsDataURL(file);
84
- });
85
- }