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.
- package/README.md +33 -72
- package/docs/CHANGELOG.md +52 -874
- package/docs/DEVELOPMENT.md +26 -3
- package/docs/LEGACY_COMPATIBILITY.md +3 -1
- package/docs/TRUST_MODEL.md +26 -26
- package/docs/WHY_LATTICE.md +28 -25
- package/docs/kg-schema.md +6 -4
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/embeddings.py +9 -1
- package/lattice_brain/graph/schema.py +6 -0
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/computer_use.py +2 -0
- package/latticeai/api/memory.py +13 -0
- package/latticeai/app_factory.py +43 -162
- package/latticeai/brain/__init__.py +1 -1
- package/latticeai/core/config.py +10 -0
- package/latticeai/core/local_embeddings.py +8 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/mcp_registry.py +140 -1
- package/latticeai/core/tool_registry.py +10 -0
- package/latticeai/core/workspace_graph_trace.py +132 -0
- package/latticeai/core/workspace_memory.py +75 -0
- package/latticeai/core/workspace_os.py +135 -1283
- package/latticeai/core/workspace_os_utils.py +132 -0
- package/latticeai/core/workspace_permissions.py +99 -0
- package/latticeai/core/workspace_plugins.py +97 -0
- package/latticeai/core/workspace_runs.py +612 -0
- package/latticeai/core/workspace_skills.py +114 -0
- package/latticeai/core/workspace_snapshots.py +195 -0
- package/latticeai/core/workspace_timeline.py +107 -0
- package/latticeai/runtime/audit_runtime.py +64 -0
- package/latticeai/runtime/config_runtime.py +4 -0
- package/latticeai/runtime/sso_runtime.py +52 -0
- package/latticeai/services/architecture_readiness.py +6 -5
- package/latticeai/services/memory_service.py +215 -7
- package/latticeai/services/model_capability_registry.py +68 -0
- package/latticeai/services/model_engines.py +603 -0
- package/latticeai/services/model_loading.py +482 -0
- package/latticeai/services/model_recommendation.py +5 -0
- package/latticeai/services/model_runtime.py +146 -837
- package/latticeai/services/product_readiness.py +19 -18
- package/package.json +2 -2
- package/scripts/build_frontend_assets.mjs +11 -2
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +11 -102
- package/static/app/assets/Act-D9jIknFd.js +1 -0
- package/static/app/assets/Brain-CFOtWbPN.js +321 -0
- package/static/app/assets/{Capture-B9Tlhzqr.js → Capture-Q4WYzwr5.js} +1 -2
- package/static/app/assets/Library-C5Q2yWee.js +1 -0
- package/static/app/assets/System-BLbjdr1_.js +1 -0
- package/static/app/assets/core-CwxXejkd.js +1 -2
- package/static/app/assets/index-BqammyNu.js +16 -0
- package/static/app/assets/index-ty1iGgZu.css +2 -0
- package/static/app/assets/primitives-Br8uSfZ4.js +1 -0
- package/static/app/assets/{textarea-BZk6ybp5.js → textarea-BnhNs1_X.js} +1 -2
- package/static/app/index.html +3 -10
- package/static/app/theme-boot.js +8 -0
- package/static/sw.js +1 -1
- package/tools/__init__.py +2 -1
- package/tools/computer.py +21 -0
- package/docs/CARRYOVER_AUDIT_v3.6.0.md +0 -61
- package/docs/HANDOVER_v3.6.0.md +0 -46
- package/docs/RUNTIME_HOOK_COVERAGE_v3.5.0.md +0 -56
- package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +0 -49
- package/docs/V2_ARCHITECTURE.md +0 -561
- package/docs/V3_2_AUDIT.md +0 -82
- package/docs/V3_BACKEND_ARCHITECTURE.md +0 -138
- package/docs/V3_FRONTEND.md +0 -140
- package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +0 -65
- package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +0 -70
- package/docs/V4_1_VALIDATION_REPORT.md +0 -47
- package/docs/V4_2_BRAIN_CORE_ARCHITECTURE.md +0 -97
- package/docs/V4_2_STORAGE_MIGRATION_REPORT.md +0 -91
- package/docs/V4_2_VALIDATION_REPORT.md +0 -89
- package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +0 -174
- package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +0 -81
- package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +0 -75
- package/docs/V4_3_2_GRAPH_UX_REPORT.md +0 -48
- package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +0 -209
- package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +0 -57
- package/docs/V4_3_2_SELF_AUDIT_REPORT.md +0 -63
- package/docs/V4_3_2_VALIDATION_REPORT.md +0 -97
- package/docs/V4_3_3_VALIDATION_REPORT.md +0 -46
- package/docs/V4_3_PORTABILITY_ARCHITECTURE.md +0 -69
- package/docs/V4_3_PRIVACY_AUDIT.md +0 -60
- package/docs/V4_3_PRODUCT_HARDENING_REPORT.md +0 -53
- package/docs/V4_3_VALIDATION_REPORT.md +0 -58
- package/docs/V4_4_0_EXTRACTION_REPORT.md +0 -239
- package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +0 -49
- package/docs/V4_5_0_GRAPH_UX_REPORT.md +0 -34
- package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +0 -40
- package/docs/V4_5_0_ONBOARDING_REPORT.md +0 -31
- package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +0 -49
- package/docs/V4_5_0_VALIDATION_REPORT.md +0 -60
- package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +0 -33
- package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +0 -37
- package/docs/V4_5_1_NAVIGATION_REPORT.md +0 -37
- package/docs/V4_5_1_ONBOARDING_REPORT.md +0 -29
- package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +0 -61
- package/docs/V4_5_1_RC_ARTIFACTS.md +0 -44
- package/docs/V4_5_1_UX_REPORT.md +0 -45
- package/docs/V4_5_1_VALIDATION_REPORT.md +0 -55
- package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +0 -30
- package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +0 -72
- package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +0 -42
- package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +0 -42
- package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +0 -49
- package/docs/V4_7_2_INTUITIVE_BRAIN_UX_REPORT.md +0 -62
- package/docs/V4_BRAIN_ARCHITECTURE.md +0 -322
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +0 -555
- package/docs/V4_IMPLEMENTATION_PLAN.md +0 -470
- package/frontend/index.html +0 -24
- package/frontend/openapi.json +0 -15425
- package/frontend/src/App.tsx +0 -243
- package/frontend/src/api/client.ts +0 -580
- package/frontend/src/api/openapi.ts +0 -17892
- package/frontend/src/components/AdminAccessGate.tsx +0 -70
- package/frontend/src/components/FeedbackState.tsx +0 -45
- package/frontend/src/components/LanguageSwitcher.tsx +0 -23
- package/frontend/src/components/LivingBrain.tsx +0 -220
- package/frontend/src/components/ProductFlow.tsx +0 -171
- package/frontend/src/components/WorkspaceProfileSwitcher.tsx +0 -176
- package/frontend/src/components/onboarding/AnalysisScreen.tsx +0 -135
- package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +0 -29
- package/frontend/src/components/onboarding/InstallScreen.tsx +0 -263
- package/frontend/src/components/onboarding/LanguageChooser.tsx +0 -23
- package/frontend/src/components/onboarding/LoginScreen.tsx +0 -131
- package/frontend/src/components/onboarding/ProductFlowScreens.tsx +0 -13
- package/frontend/src/components/onboarding/RecommendationScreen.tsx +0 -116
- package/frontend/src/components/onboarding/recommendationModel.ts +0 -189
- package/frontend/src/components/primitives.tsx +0 -392
- package/frontend/src/components/ui/badge.tsx +0 -27
- package/frontend/src/components/ui/button.tsx +0 -37
- package/frontend/src/components/ui/card.tsx +0 -22
- package/frontend/src/components/ui/input.tsx +0 -16
- package/frontend/src/components/ui/textarea.tsx +0 -16
- package/frontend/src/features/admin/AdminConsole.tsx +0 -334
- package/frontend/src/features/brain/BrainCarePanel.tsx +0 -254
- package/frontend/src/features/brain/BrainComposer.tsx +0 -85
- package/frontend/src/features/brain/BrainConversation.tsx +0 -688
- package/frontend/src/features/brain/BrainGraphLayer.tsx +0 -365
- package/frontend/src/features/brain/BrainHome.tsx +0 -624
- package/frontend/src/features/brain/BrainMemoryLayer.tsx +0 -45
- package/frontend/src/features/brain/BrainOverviewPanel.tsx +0 -149
- package/frontend/src/features/brain/BrainRelationshipLayer.tsx +0 -43
- package/frontend/src/features/brain/DepthEmergence.tsx +0 -53
- package/frontend/src/features/brain/brainData.ts +0 -246
- package/frontend/src/features/brain/graphLayout.ts +0 -37
- package/frontend/src/features/brain/types.ts +0 -150
- package/frontend/src/features/review/ReviewCard.tsx +0 -100
- package/frontend/src/features/review/ReviewInbox.tsx +0 -127
- package/frontend/src/features/review/reviewHelpers.ts +0 -69
- package/frontend/src/i18n.ts +0 -1303
- package/frontend/src/lib/utils.ts +0 -33
- package/frontend/src/main.tsx +0 -23
- package/frontend/src/pages/Act.tsx +0 -458
- package/frontend/src/pages/Ask.tsx +0 -14
- package/frontend/src/pages/Brain.tsx +0 -914
- package/frontend/src/pages/Capture.tsx +0 -258
- package/frontend/src/pages/Library.tsx +0 -486
- package/frontend/src/pages/System.tsx +0 -621
- package/frontend/src/routes.ts +0 -92
- package/frontend/src/store/appStore.ts +0 -94
- package/frontend/src/styles.css +0 -6579
- package/static/app/assets/Act-DOvf59ru.js +0 -2
- package/static/app/assets/Act-DOvf59ru.js.map +0 -1
- package/static/app/assets/Brain-C7_0mEiI.js +0 -322
- package/static/app/assets/Brain-C7_0mEiI.js.map +0 -1
- package/static/app/assets/Capture-B9Tlhzqr.js.map +0 -1
- package/static/app/assets/Library-BJPEEm5O.js +0 -2
- package/static/app/assets/Library-BJPEEm5O.js.map +0 -1
- package/static/app/assets/System-D6t9jo9V.js +0 -2
- package/static/app/assets/System-D6t9jo9V.js.map +0 -1
- package/static/app/assets/core-CwxXejkd.js.map +0 -1
- package/static/app/assets/index-C7g26IF6.css +0 -2
- package/static/app/assets/index-DbcEYJQ2.js +0 -17
- package/static/app/assets/index-DbcEYJQ2.js.map +0 -1
- package/static/app/assets/primitives-CD38lt4n.js +0 -2
- package/static/app/assets/primitives-CD38lt4n.js.map +0 -1
- package/static/app/assets/textarea-BZk6ybp5.js.map +0 -1
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
-
import { Check, ChevronDown, UserCircle, Building2, ArrowRightLeft } from "lucide-react";
|
|
4
|
-
import { latticeApi } from "@/api/client";
|
|
5
|
-
import { t, type Language } from "@/i18n";
|
|
6
|
-
import { useAppStore } from "@/store/appStore";
|
|
7
|
-
import { asArray } from "@/lib/utils";
|
|
8
|
-
|
|
9
|
-
type WorkspaceRow = Record<string, unknown>;
|
|
10
|
-
|
|
11
|
-
function workspaceId(row: WorkspaceRow): string {
|
|
12
|
-
return String(row.workspace_id || row.id || "");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function workspaceName(language: Language, row: WorkspaceRow): string {
|
|
16
|
-
const name = row.name;
|
|
17
|
-
if (typeof name === "string" && name.trim()) return name;
|
|
18
|
-
const id = workspaceId(row);
|
|
19
|
-
return id || t(language, "shell.workspace.personal");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function ownerEmail(profile: unknown): string | null {
|
|
23
|
-
if (!profile || typeof profile !== "object") return null;
|
|
24
|
-
const record = profile as Record<string, unknown>;
|
|
25
|
-
const candidate = record.email || record.owner_email || record.username || record.name;
|
|
26
|
-
return typeof candidate === "string" && candidate.trim() ? candidate : null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Discoverable workspace + profile switcher for the Brain shell.
|
|
31
|
-
* Surfaces the active workspace and signed-in owner, and lets the user
|
|
32
|
-
* switch workspace or jump to account/workspace settings without hunting
|
|
33
|
-
* through the Settings tabs.
|
|
34
|
-
*/
|
|
35
|
-
export function WorkspaceProfileSwitcher({ language }: { language: Language }) {
|
|
36
|
-
const qc = useQueryClient();
|
|
37
|
-
const workspaceIdState = useAppStore((state) => state.workspaceId);
|
|
38
|
-
const setWorkspaceId = useAppStore((state) => state.setWorkspaceId);
|
|
39
|
-
const [open, setOpen] = React.useState(false);
|
|
40
|
-
const containerRef = React.useRef<HTMLDivElement | null>(null);
|
|
41
|
-
|
|
42
|
-
const profile = useQuery({ queryKey: ["profile"], queryFn: latticeApi.profile });
|
|
43
|
-
const registry = useQuery({ queryKey: ["workspaceRegistry"], queryFn: latticeApi.workspaceRegistry });
|
|
44
|
-
|
|
45
|
-
const workspaces = asArray<WorkspaceRow>(
|
|
46
|
-
(registry.data?.data as Record<string, unknown> | undefined)?.workspaces,
|
|
47
|
-
);
|
|
48
|
-
const owner = ownerEmail(profile.data?.data);
|
|
49
|
-
|
|
50
|
-
const activeWorkspace = workspaces.find((row) => workspaceId(row) === workspaceIdState);
|
|
51
|
-
const activeLabel = activeWorkspace
|
|
52
|
-
? workspaceName(language, activeWorkspace)
|
|
53
|
-
: workspaceIdState || t(language, "shell.workspace.personal");
|
|
54
|
-
|
|
55
|
-
React.useEffect(() => {
|
|
56
|
-
if (!open) return;
|
|
57
|
-
const onPointer = (event: MouseEvent) => {
|
|
58
|
-
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
59
|
-
setOpen(false);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
const onKey = (event: KeyboardEvent) => {
|
|
63
|
-
if (event.key === "Escape") setOpen(false);
|
|
64
|
-
};
|
|
65
|
-
window.addEventListener("mousedown", onPointer);
|
|
66
|
-
window.addEventListener("keydown", onKey);
|
|
67
|
-
return () => {
|
|
68
|
-
window.removeEventListener("mousedown", onPointer);
|
|
69
|
-
window.removeEventListener("keydown", onKey);
|
|
70
|
-
};
|
|
71
|
-
}, [open]);
|
|
72
|
-
|
|
73
|
-
const handleSwitch = (id: string) => {
|
|
74
|
-
if (id && id !== workspaceIdState) {
|
|
75
|
-
setWorkspaceId(id);
|
|
76
|
-
// Workspace scoping changes server responses; drop cached views.
|
|
77
|
-
qc.invalidateQueries();
|
|
78
|
-
}
|
|
79
|
-
setOpen(false);
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<div className="workspace-profile-switcher" ref={containerRef}>
|
|
84
|
-
<button
|
|
85
|
-
type="button"
|
|
86
|
-
className="workspace-profile-trigger"
|
|
87
|
-
aria-haspopup="dialog"
|
|
88
|
-
aria-expanded={open}
|
|
89
|
-
aria-label={t(language, "shell.workspace.label")}
|
|
90
|
-
onClick={() => setOpen((value) => !value)}
|
|
91
|
-
>
|
|
92
|
-
<Building2 className="h-3.5 w-3.5" aria-hidden="true" />
|
|
93
|
-
<span className="workspace-profile-trigger-text">
|
|
94
|
-
<span className="workspace-profile-trigger-name">{activeLabel}</span>
|
|
95
|
-
<span className="workspace-profile-trigger-owner">{owner || t(language, "shell.profile.signedOut")}</span>
|
|
96
|
-
</span>
|
|
97
|
-
<ChevronDown className="h-3.5 w-3.5" aria-hidden="true" />
|
|
98
|
-
</button>
|
|
99
|
-
|
|
100
|
-
{open ? (
|
|
101
|
-
<div className="workspace-profile-popover" role="dialog" aria-label={t(language, "shell.workspace.label")}>
|
|
102
|
-
<section className="workspace-profile-section" aria-label={t(language, "shell.profile.label")}>
|
|
103
|
-
<div className="workspace-profile-section-head">
|
|
104
|
-
<UserCircle className="h-3.5 w-3.5" aria-hidden="true" />
|
|
105
|
-
<span>{t(language, "shell.profile.label")}</span>
|
|
106
|
-
</div>
|
|
107
|
-
<div className="workspace-profile-owner-row">
|
|
108
|
-
<span className="workspace-profile-owner-label">{t(language, "shell.profile.owner")}</span>
|
|
109
|
-
<span className="workspace-profile-owner-value">{owner || t(language, "shell.profile.signedOut")}</span>
|
|
110
|
-
</div>
|
|
111
|
-
<button
|
|
112
|
-
type="button"
|
|
113
|
-
className="workspace-profile-link"
|
|
114
|
-
onClick={() => {
|
|
115
|
-
setOpen(false);
|
|
116
|
-
window.location.hash = "/account";
|
|
117
|
-
}}
|
|
118
|
-
>
|
|
119
|
-
{t(language, "shell.profile.manage")}
|
|
120
|
-
</button>
|
|
121
|
-
</section>
|
|
122
|
-
|
|
123
|
-
<section className="workspace-profile-section" aria-label={t(language, "shell.workspace.current")}>
|
|
124
|
-
<div className="workspace-profile-section-head">
|
|
125
|
-
<Building2 className="h-3.5 w-3.5" aria-hidden="true" />
|
|
126
|
-
<span>{t(language, "shell.workspace.current")}</span>
|
|
127
|
-
</div>
|
|
128
|
-
{workspaces.length === 0 ? (
|
|
129
|
-
<p className="workspace-profile-empty">{t(language, "shell.workspace.empty")}</p>
|
|
130
|
-
) : (
|
|
131
|
-
<ul className="workspace-profile-list">
|
|
132
|
-
{workspaces.map((row) => {
|
|
133
|
-
const id = workspaceId(row);
|
|
134
|
-
const isActive = id === workspaceIdState;
|
|
135
|
-
return (
|
|
136
|
-
<li key={id}>
|
|
137
|
-
<button
|
|
138
|
-
type="button"
|
|
139
|
-
className={`workspace-profile-item ${isActive ? "is-active" : ""}`}
|
|
140
|
-
aria-current={isActive ? "true" : undefined}
|
|
141
|
-
onClick={() => handleSwitch(id)}
|
|
142
|
-
>
|
|
143
|
-
<span className="workspace-profile-item-name">{workspaceName(language, row)}</span>
|
|
144
|
-
{isActive ? (
|
|
145
|
-
<span className="workspace-profile-item-flag">
|
|
146
|
-
<Check className="h-3 w-3" aria-hidden="true" />
|
|
147
|
-
{t(language, "shell.workspace.active")}
|
|
148
|
-
</span>
|
|
149
|
-
) : (
|
|
150
|
-
<span className="workspace-profile-item-switch">
|
|
151
|
-
<ArrowRightLeft className="h-3 w-3" aria-hidden="true" />
|
|
152
|
-
{t(language, "shell.workspace.switch")}
|
|
153
|
-
</span>
|
|
154
|
-
)}
|
|
155
|
-
</button>
|
|
156
|
-
</li>
|
|
157
|
-
);
|
|
158
|
-
})}
|
|
159
|
-
</ul>
|
|
160
|
-
)}
|
|
161
|
-
<button
|
|
162
|
-
type="button"
|
|
163
|
-
className="workspace-profile-link"
|
|
164
|
-
onClick={() => {
|
|
165
|
-
setOpen(false);
|
|
166
|
-
window.location.hash = "/workspace-admin";
|
|
167
|
-
}}
|
|
168
|
-
>
|
|
169
|
-
{t(language, "shell.workspace.manageSpaces")}
|
|
170
|
-
</button>
|
|
171
|
-
</section>
|
|
172
|
-
</div>
|
|
173
|
-
) : null}
|
|
174
|
-
</div>
|
|
175
|
-
);
|
|
176
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { Sparkles } from "lucide-react";
|
|
2
|
-
import { Button } from "@/components/ui/button";
|
|
3
|
-
import { asArray } from "@/lib/utils";
|
|
4
|
-
import { t, type Language } from "@/i18n";
|
|
5
|
-
import { useAppStore } from "@/store/appStore";
|
|
6
|
-
import { asRecord, friendlyModelName, type FlowAnalysis } from "./recommendationModel";
|
|
7
|
-
|
|
8
|
-
export function AnalysisScreen({
|
|
9
|
-
analysis,
|
|
10
|
-
error,
|
|
11
|
-
onContinue,
|
|
12
|
-
}: {
|
|
13
|
-
analysis: FlowAnalysis | null;
|
|
14
|
-
error: string | null;
|
|
15
|
-
onContinue: () => void;
|
|
16
|
-
}) {
|
|
17
|
-
const language = useAppStore((state) => state.language);
|
|
18
|
-
const detected = buildDetectedFacts(analysis, language);
|
|
19
|
-
return (
|
|
20
|
-
<div>
|
|
21
|
-
<div className="ritual-title">{t(language, "flow.analysis.title")}</div>
|
|
22
|
-
<div className="ritual-subtitle">{t(language, "flow.analysis.body")}</div>
|
|
23
|
-
|
|
24
|
-
<div className="ritual-fact-grid">
|
|
25
|
-
{detected.map((item, idx) => (
|
|
26
|
-
<div key={idx} className="ritual-fact ritual-fact--visual">
|
|
27
|
-
<div className="ritual-fact-head">
|
|
28
|
-
<span className="ritual-hardware-icon" aria-hidden="true">{item.icon}</span>
|
|
29
|
-
<div className="ritual-fact-label">{item.label}</div>
|
|
30
|
-
</div>
|
|
31
|
-
<div className="ritual-fact-value">{item.value}</div>
|
|
32
|
-
<div className="ritual-fact-detail">{item.detail}</div>
|
|
33
|
-
</div>
|
|
34
|
-
))}
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
<div className="ritual-card ritual-analysis-card">
|
|
38
|
-
<div className="ritual-inline-row">
|
|
39
|
-
<Sparkles className="ritual-core-icon" />
|
|
40
|
-
<div>
|
|
41
|
-
<div className="ritual-strong-text">{analysis ? recommendedSummary(analysis, language) : t(language, "flow.analysis.finding")}</div>
|
|
42
|
-
<div className="ritual-muted-text">
|
|
43
|
-
{analysis ? t(language, "flow.analysis.ready") : t(language, "flow.analysis.wait")}
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
|
|
49
|
-
{error && <div className="ritual-card ritual-error-card" role="alert">{error}</div>}
|
|
50
|
-
|
|
51
|
-
<div className="ritual-centered-actions">
|
|
52
|
-
<Button onClick={onContinue} disabled={!analysis && !error} className="ritual-wide-button">
|
|
53
|
-
{t(language, "flow.analysis.continue")}
|
|
54
|
-
</Button>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function buildDetectedFacts(analysis: FlowAnalysis | null, language: Language) {
|
|
61
|
-
if (!analysis) {
|
|
62
|
-
return [
|
|
63
|
-
{ icon: t(language, "flow.analysis.hardware.icon.chip"), label: t(language, "flow.analysis.hardware.label.chip"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.computerDetail") },
|
|
64
|
-
{ icon: t(language, "flow.analysis.hardware.icon.ram"), label: t(language, "flow.analysis.hardware.label.ram"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.memoryDetail") },
|
|
65
|
-
{ icon: t(language, "flow.analysis.hardware.icon.gpu"), label: t(language, "flow.analysis.hardware.label.gpu"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.graphicsDetail") },
|
|
66
|
-
{ icon: t(language, "flow.analysis.hardware.icon.support"), label: t(language, "flow.analysis.hardware.label.support"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.supportDetail") },
|
|
67
|
-
{ icon: t(language, "flow.analysis.hardware.icon.models"), label: t(language, "flow.analysis.hardware.label.models"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.modelsDetail") },
|
|
68
|
-
];
|
|
69
|
-
}
|
|
70
|
-
const setupEnv = asRecord(analysis.setup?.environment);
|
|
71
|
-
const recProfile = asRecord(analysis.recommendations?.profile);
|
|
72
|
-
const recs = asRecord(analysis.recommendations?.recommendations);
|
|
73
|
-
const models = asRecord(analysis.models);
|
|
74
|
-
const sysinfo = asRecord(analysis.sysinfo);
|
|
75
|
-
const profile = { ...setupEnv, ...recProfile };
|
|
76
|
-
const ramGb = Number(recs.ram_gb || Number(profile.ram_mb || 0) / 1024 || 0);
|
|
77
|
-
const appleSilicon = Boolean(recs.apple_silicon || String(profile.arch || "").includes("arm"));
|
|
78
|
-
const loadedModels = asArray(models.loaded);
|
|
79
|
-
const gpu = asRecord(profile.gpu);
|
|
80
|
-
const installedRuntimes = [
|
|
81
|
-
...asArray(setupEnv.installed_runtimes),
|
|
82
|
-
...asArray(profile.installed_runtimes),
|
|
83
|
-
...asArray(recs.installed_runtimes),
|
|
84
|
-
];
|
|
85
|
-
return [
|
|
86
|
-
{
|
|
87
|
-
icon: t(language, "flow.analysis.hardware.icon.chip"),
|
|
88
|
-
label: t(language, "flow.analysis.hardware.label.chip"),
|
|
89
|
-
value: appleSilicon ? t(language, "flow.analysis.apple") : friendlyOs(profile.os, language),
|
|
90
|
-
detail: t(language, "flow.analysis.readyDetail"),
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
icon: t(language, "flow.analysis.hardware.icon.ram"),
|
|
94
|
-
label: t(language, "flow.analysis.hardware.label.ram"),
|
|
95
|
-
value: ramGb ? `${Math.round(ramGb)} GB` : t(language, "flow.analysis.detected"),
|
|
96
|
-
detail: t(language, "flow.analysis.memoryReadyDetail"),
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
icon: t(language, "flow.analysis.hardware.icon.gpu"),
|
|
100
|
-
label: t(language, "flow.analysis.hardware.label.gpu"),
|
|
101
|
-
value: gpu.vendor || sysinfo.gpu_mem_gb ? t(language, "flow.analysis.localReady") : t(language, "flow.analysis.standardLocal"),
|
|
102
|
-
detail: t(language, "flow.analysis.graphicsReadyDetail"),
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
icon: t(language, "flow.analysis.hardware.icon.support"),
|
|
106
|
-
label: t(language, "flow.analysis.hardware.label.support"),
|
|
107
|
-
value: installedRuntimes.length ? t(language, "flow.analysis.supportReady") : t(language, "flow.analysis.supportInstall"),
|
|
108
|
-
detail: installedRuntimes.length ? t(language, "flow.analysis.supportReadyDetail") : t(language, "flow.analysis.supportInstallDetail"),
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
icon: t(language, "flow.analysis.hardware.icon.models"),
|
|
112
|
-
label: t(language, "flow.analysis.hardware.label.models"),
|
|
113
|
-
value: loadedModels.length ? t(language, "flow.analysis.modelsInstalled", { count: loadedModels.length }) : t(language, "flow.analysis.noModels"),
|
|
114
|
-
detail: loadedModels.length ? t(language, "flow.analysis.modelsReadyDetail") : t(language, "flow.analysis.modelsInstallDetail"),
|
|
115
|
-
},
|
|
116
|
-
];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function recommendedSummary(analysis: FlowAnalysis, language: Language) {
|
|
120
|
-
const recs = asRecord(analysis.recommendations?.recommendations);
|
|
121
|
-
const topPick = asRecord(recs.top_pick);
|
|
122
|
-
if (topPick.name || topPick.id) {
|
|
123
|
-
const model = friendlyModelName(String(topPick.name || topPick.id));
|
|
124
|
-
return t(language, "flow.analysis.bestFit", { model });
|
|
125
|
-
}
|
|
126
|
-
return t(language, "flow.analysis.privateRecommended");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function friendlyOs(value: unknown, language: Language) {
|
|
130
|
-
const text = String(value || t(language, "flow.analysis.fact.computer"));
|
|
131
|
-
if (/darwin|mac/i.test(text)) return "Mac";
|
|
132
|
-
if (/win/i.test(text)) return t(language, "flow.analysis.os.windows");
|
|
133
|
-
if (/linux/i.test(text)) return t(language, "flow.analysis.os.linux");
|
|
134
|
-
return t(language, "flow.analysis.fact.computer");
|
|
135
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { t } from "@/i18n";
|
|
2
|
-
import { useAppStore } from "@/store/appStore";
|
|
3
|
-
import { type RecommendedModel } from "./recommendationModel";
|
|
4
|
-
|
|
5
|
-
export function DownloadConsentPanel({ model }: { model: RecommendedModel }) {
|
|
6
|
-
const language = useAppStore((state) => state.language);
|
|
7
|
-
const items = [
|
|
8
|
-
{ label: t(language, "flow.consent.size"), value: model.downloadSize || t(language, "flow.consent.sizeUnknown") },
|
|
9
|
-
{ label: t(language, "flow.consent.location"), value: model.storageLocation },
|
|
10
|
-
{ label: t(language, "flow.consent.external"), value: model.externalHost || t(language, "flow.consent.externalNone") },
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<section className="ritual-consent-panel" aria-label={t(language, "flow.consent.title")}>
|
|
15
|
-
<div>
|
|
16
|
-
<strong>{t(language, "flow.consent.title")}</strong>
|
|
17
|
-
<p>{model.downloadRequired ? t(language, "flow.consent.body") : t(language, "flow.consent.ready")}</p>
|
|
18
|
-
</div>
|
|
19
|
-
<dl>
|
|
20
|
-
{items.map((item) => (
|
|
21
|
-
<div key={item.label}>
|
|
22
|
-
<dt>{item.label}</dt>
|
|
23
|
-
<dd>{item.value}</dd>
|
|
24
|
-
</div>
|
|
25
|
-
))}
|
|
26
|
-
</dl>
|
|
27
|
-
</section>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { CheckCircle2 } from "lucide-react";
|
|
3
|
-
import { latticeApi } from "@/api/client";
|
|
4
|
-
import { type BrainState, LivingBrain } from "@/components/LivingBrain";
|
|
5
|
-
import { Button } from "@/components/ui/button";
|
|
6
|
-
import { asArray } from "@/lib/utils";
|
|
7
|
-
import { t, type Language } from "@/i18n";
|
|
8
|
-
import { useAppStore } from "@/store/appStore";
|
|
9
|
-
import { DownloadConsentPanel } from "./DownloadConsentPanel";
|
|
10
|
-
import { asRecord, type ApiData, type RecommendedModel } from "./recommendationModel";
|
|
11
|
-
|
|
12
|
-
type InstallStage = "idle" | "install" | "download" | "validate" | "load" | "done" | "error";
|
|
13
|
-
|
|
14
|
-
export function InstallScreen({
|
|
15
|
-
model,
|
|
16
|
-
onBack,
|
|
17
|
-
onComplete,
|
|
18
|
-
onLater,
|
|
19
|
-
}: {
|
|
20
|
-
model: RecommendedModel;
|
|
21
|
-
onBack: () => void;
|
|
22
|
-
onComplete: () => void;
|
|
23
|
-
onLater: () => void;
|
|
24
|
-
}) {
|
|
25
|
-
const language = useAppStore((state) => state.language);
|
|
26
|
-
const [busy, setBusy] = React.useState(false);
|
|
27
|
-
const [stage, setStage] = React.useState<InstallStage>("idle");
|
|
28
|
-
const [percent, setPercent] = React.useState(0);
|
|
29
|
-
const [message, setMessage] = React.useState(t(language, "flow.install.wait"));
|
|
30
|
-
const [error, setError] = React.useState<string | null>(null);
|
|
31
|
-
|
|
32
|
-
async function start() {
|
|
33
|
-
setBusy(true);
|
|
34
|
-
setError(null);
|
|
35
|
-
setStage("install");
|
|
36
|
-
setPercent(8);
|
|
37
|
-
setMessage(t(language, "flow.install.prepare"));
|
|
38
|
-
const result = await latticeApi.streamModelPrepare(
|
|
39
|
-
{ model: model.loadId, engine: model.engine || "local_mlx", allow_download: true },
|
|
40
|
-
{
|
|
41
|
-
onProgress: (event) => {
|
|
42
|
-
const nextStage = friendlyInstallStage(String(event.stage || ""));
|
|
43
|
-
setStage(nextStage);
|
|
44
|
-
setPercent(Number(event.percent || percentForStage(nextStage)));
|
|
45
|
-
setMessage(friendlyInstallMessage(event, nextStage, language));
|
|
46
|
-
},
|
|
47
|
-
onDone: () => {
|
|
48
|
-
setStage("done");
|
|
49
|
-
setPercent(100);
|
|
50
|
-
setMessage(t(language, "flow.install.done"));
|
|
51
|
-
},
|
|
52
|
-
onError: (event) => {
|
|
53
|
-
setStage("error");
|
|
54
|
-
setError(consumerError(event));
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
);
|
|
58
|
-
setBusy(false);
|
|
59
|
-
if (result.ok) {
|
|
60
|
-
setStage("done");
|
|
61
|
-
setPercent(100);
|
|
62
|
-
setMessage(t(language, "flow.install.done"));
|
|
63
|
-
window.setTimeout(onComplete, 700);
|
|
64
|
-
} else {
|
|
65
|
-
setStage("error");
|
|
66
|
-
setError(consumerError(result.data as ApiData));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const brainStateForStage: BrainState =
|
|
71
|
-
stage === "download" ? "thinking" :
|
|
72
|
-
stage === "validate" ? "recalling" :
|
|
73
|
-
stage === "load" ? "synthesizing" :
|
|
74
|
-
stage === "done" ? "idle" : "listening";
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<div>
|
|
78
|
-
<div className="ritual-title">{t(language, "flow.install.title")}</div>
|
|
79
|
-
<div className="ritual-subtitle">
|
|
80
|
-
<strong>{model.shortName}</strong> — {model.reason}.<br />
|
|
81
|
-
{t(language, "flow.install.body")}
|
|
82
|
-
</div>
|
|
83
|
-
|
|
84
|
-
<section className="ritual-card ritual-expected-card" aria-label={t(language, "flow.install.expectedTitle")}>
|
|
85
|
-
<div className="ritual-fact-label">{t(language, "flow.install.expectedTitle")}</div>
|
|
86
|
-
<div className="ritual-time-estimate ritual-expected-line">{expectedLine(model, language)}</div>
|
|
87
|
-
<ol className="ritual-timeline">
|
|
88
|
-
{timelineSteps(model, language).map((step) => (
|
|
89
|
-
<li key={step.key} className="ritual-timeline-step">
|
|
90
|
-
<span className="ritual-timeline-name">{step.name}</span>
|
|
91
|
-
<span className="ritual-timeline-time">{step.time}</span>
|
|
92
|
-
</li>
|
|
93
|
-
))}
|
|
94
|
-
</ol>
|
|
95
|
-
<div className="ritual-muted-hint">{t(language, "flow.install.expectedCompletion")}</div>
|
|
96
|
-
</section>
|
|
97
|
-
|
|
98
|
-
<div className="ritual-install-brain">
|
|
99
|
-
<LivingBrain
|
|
100
|
-
state={brainStateForStage}
|
|
101
|
-
intensity={stage === "download" || stage === "load" ? 0.96 : 0.82}
|
|
102
|
-
size="normal"
|
|
103
|
-
/>
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
|
-
<DownloadConsentPanel model={model} />
|
|
107
|
-
|
|
108
|
-
<div className="ritual-progress">
|
|
109
|
-
<div className="ritual-stage-list">
|
|
110
|
-
{(["install", "download", "validate", "load"] as const).map((item) => (
|
|
111
|
-
<div key={item} className={`ritual-stage ${installStepState(stage, item)}`}>
|
|
112
|
-
<CheckCircle2 className="ritual-stage-icon" />
|
|
113
|
-
<span>{installLabel(item, language)}</span>
|
|
114
|
-
</div>
|
|
115
|
-
))}
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
|
-
<div className="ritual-bar">
|
|
119
|
-
<span className={`ritual-bar-fill ${progressClass(percent)}`} />
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
<div className="ritual-status">{message}</div>
|
|
124
|
-
<div className="ritual-card ritual-status-card">
|
|
125
|
-
{t(language, "flow.install.note")}
|
|
126
|
-
</div>
|
|
127
|
-
|
|
128
|
-
{error && (
|
|
129
|
-
<div className="ritual-card ritual-error-card ritual-install-error" role="alert">
|
|
130
|
-
{error}
|
|
131
|
-
<div className="ritual-error-detail">{t(language, "flow.install.retry")}</div>
|
|
132
|
-
</div>
|
|
133
|
-
)}
|
|
134
|
-
|
|
135
|
-
<div className="ritual-button-row">
|
|
136
|
-
<Button variant="ghost" onClick={onBack} disabled={busy}>{t(language, "flow.install.back")}</Button>
|
|
137
|
-
<Button variant="outline" onClick={onLater} disabled={busy}>{t(language, "flow.install.later")}</Button>
|
|
138
|
-
|
|
139
|
-
{stage !== "done" ? (
|
|
140
|
-
<Button
|
|
141
|
-
onClick={start}
|
|
142
|
-
disabled={busy || !model.supported}
|
|
143
|
-
>
|
|
144
|
-
{busy ? t(language, "flow.install.busy") : t(language, "flow.install.start")}
|
|
145
|
-
</Button>
|
|
146
|
-
) : (
|
|
147
|
-
<Button onClick={onComplete}>{t(language, "flow.install.enter")}</Button>
|
|
148
|
-
)}
|
|
149
|
-
</div>
|
|
150
|
-
|
|
151
|
-
<div className="ritual-local-note">
|
|
152
|
-
{t(language, "flow.install.local")}
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function expectedLine(model: RecommendedModel, language: Language) {
|
|
159
|
-
const response = model.estimatedFirstResponseSeconds;
|
|
160
|
-
if (!model.downloadRequired || model.estimatedDownloadMinutes === 0) {
|
|
161
|
-
return t(language, "flow.install.expected.ready", { response });
|
|
162
|
-
}
|
|
163
|
-
if (model.estimatedDownloadMinutes === null) {
|
|
164
|
-
return t(language, "flow.install.expected.unknown", { response });
|
|
165
|
-
}
|
|
166
|
-
return t(language, "flow.install.expected", {
|
|
167
|
-
download: t(language, "flow.recommend.minutes", { count: model.estimatedDownloadMinutes }),
|
|
168
|
-
response,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function timelineSteps(model: RecommendedModel, language: Language) {
|
|
173
|
-
const downloadTime = model.downloadRequired && model.estimatedDownloadMinutes
|
|
174
|
-
? t(language, "flow.install.timelineApprox", {
|
|
175
|
-
time: t(language, "flow.recommend.minutes", { count: model.estimatedDownloadMinutes }),
|
|
176
|
-
})
|
|
177
|
-
: t(language, "flow.install.timelineQuick");
|
|
178
|
-
return [
|
|
179
|
-
{
|
|
180
|
-
key: "download",
|
|
181
|
-
name: t(language, "flow.install.timelineStep.download"),
|
|
182
|
-
time: downloadTime,
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
key: "validate",
|
|
186
|
-
name: t(language, "flow.install.timelineStep.validate"),
|
|
187
|
-
time: t(language, "flow.install.timelineQuick"),
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
key: "load",
|
|
191
|
-
name: t(language, "flow.install.timelineStep.load"),
|
|
192
|
-
time: t(language, "flow.install.timelineApprox", {
|
|
193
|
-
time: `${model.estimatedFirstResponseSeconds}s`,
|
|
194
|
-
}),
|
|
195
|
-
},
|
|
196
|
-
];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function friendlyInstallStage(stage: string): InstallStage {
|
|
200
|
-
if (/download|pull|weights/i.test(stage)) return "download";
|
|
201
|
-
if (/smoke|validate|verify|test/i.test(stage)) return "validate";
|
|
202
|
-
if (/load|ready/i.test(stage)) return "load";
|
|
203
|
-
if (/done|complete/i.test(stage)) return "done";
|
|
204
|
-
return "install";
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function percentForStage(stage: InstallStage) {
|
|
208
|
-
if (stage === "install") return 20;
|
|
209
|
-
if (stage === "download") return 55;
|
|
210
|
-
if (stage === "validate") return 82;
|
|
211
|
-
if (stage === "load") return 94;
|
|
212
|
-
if (stage === "done") return 100;
|
|
213
|
-
return 8;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function friendlyInstallMessage(event: ApiData, stage: InstallStage, language: Language) {
|
|
217
|
-
const fallback = {
|
|
218
|
-
install: t(language, "flow.install.prepare"),
|
|
219
|
-
download: t(language, "flow.install.stage.download"),
|
|
220
|
-
validate: t(language, "flow.install.stage.validate"),
|
|
221
|
-
load: t(language, "flow.install.stage.load"),
|
|
222
|
-
done: t(language, "flow.install.done"),
|
|
223
|
-
idle: t(language, "flow.install.wait"),
|
|
224
|
-
error: t(language, "flow.install.stage.error"),
|
|
225
|
-
}[stage];
|
|
226
|
-
return cleanConsumerText(String(event.user_message || event.message || fallback));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function installLabel(stage: "install" | "download" | "validate" | "load", language: Language) {
|
|
230
|
-
return t(language, `flow.install.step.${stage}`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function progressClass(percent: number) {
|
|
234
|
-
const step = Math.max(0, Math.min(100, Math.round(percent / 10) * 10));
|
|
235
|
-
return `progress-${step}`;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function installStepState(current: InstallStage, item: "install" | "download" | "validate" | "load") {
|
|
239
|
-
const order: InstallStage[] = ["idle", "install", "download", "validate", "load", "done"];
|
|
240
|
-
const currentIndex = order.indexOf(current);
|
|
241
|
-
const itemIndex = order.indexOf(item);
|
|
242
|
-
if (current === "error") return "is-error";
|
|
243
|
-
if (current === "done" || currentIndex > itemIndex) return "is-done";
|
|
244
|
-
if (current === item) return "is-active";
|
|
245
|
-
return "";
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function consumerError(data: ApiData | unknown) {
|
|
249
|
-
const record = asRecord(data);
|
|
250
|
-
const guidance = asArray<string>(record.recovery_guidance).map(cleanConsumerText).filter(Boolean);
|
|
251
|
-
const message = cleanConsumerText(String(record.user_message || record.reason || record.error || "The selected model could not be loaded."));
|
|
252
|
-
return [message, ...guidance.slice(0, 2)].join(" ");
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function cleanConsumerText(value: string) {
|
|
256
|
-
return String(value || "")
|
|
257
|
-
.replace(/gemma4_unified/gi, "this model format")
|
|
258
|
-
.replace(/mlx[-_ ]?vlm|mlx[-_ ]?lm|local_mlx|\bmlx\b|\bgguf\b|\bollama\b|huggingface|hugging face/gi, "local model support")
|
|
259
|
-
.replace(/runtime/gi, "model support")
|
|
260
|
-
.replace(/No module named ['\"][^'\"]+['\"]/gi, "A local support component is missing")
|
|
261
|
-
.replace(/\s+/g, " ")
|
|
262
|
-
.trim();
|
|
263
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { LANGUAGE_LABELS, t, type Language } from "@/i18n";
|
|
2
|
-
import { useAppStore } from "@/store/appStore";
|
|
3
|
-
|
|
4
|
-
export function LanguageChooser() {
|
|
5
|
-
const language = useAppStore((state) => state.language);
|
|
6
|
-
const setLanguage = useAppStore((state) => state.setLanguage);
|
|
7
|
-
|
|
8
|
-
return (
|
|
9
|
-
<div className="language-switcher ritual-language" aria-label={t(language, "language.label")}>
|
|
10
|
-
{(["ko", "en"] as Language[]).map((item) => (
|
|
11
|
-
<button
|
|
12
|
-
key={item}
|
|
13
|
-
type="button"
|
|
14
|
-
className={language === item ? "is-active" : ""}
|
|
15
|
-
onClick={() => setLanguage(item)}
|
|
16
|
-
aria-pressed={language === item}
|
|
17
|
-
>
|
|
18
|
-
{LANGUAGE_LABELS[item]}
|
|
19
|
-
</button>
|
|
20
|
-
))}
|
|
21
|
-
</div>
|
|
22
|
-
);
|
|
23
|
-
}
|