ltcai 6.2.0 → 6.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +29 -17
  2. package/docs/CHANGELOG.md +66 -0
  3. package/frontend/src/components/onboarding/AnalysisScreen.tsx +127 -0
  4. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +29 -0
  5. package/frontend/src/components/onboarding/InstallScreen.tsx +208 -0
  6. package/frontend/src/components/onboarding/LanguageChooser.tsx +23 -0
  7. package/frontend/src/components/onboarding/LoginScreen.tsx +131 -0
  8. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +13 -688
  9. package/frontend/src/components/onboarding/RecommendationScreen.tsx +59 -0
  10. package/frontend/src/components/onboarding/recommendationModel.ts +132 -0
  11. package/frontend/src/features/admin/AdminConsole.tsx +1 -1
  12. package/frontend/src/features/brain/BrainCarePanel.tsx +254 -0
  13. package/frontend/src/features/brain/BrainComposer.tsx +66 -0
  14. package/frontend/src/features/brain/BrainConversation.tsx +131 -0
  15. package/frontend/src/features/brain/BrainGraphLayer.tsx +132 -0
  16. package/frontend/src/features/brain/BrainHome.tsx +28 -795
  17. package/frontend/src/features/brain/BrainMemoryLayer.tsx +38 -0
  18. package/frontend/src/features/brain/BrainOverviewPanel.tsx +74 -0
  19. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +43 -0
  20. package/frontend/src/features/brain/DepthEmergence.tsx +53 -0
  21. package/frontend/src/features/brain/types.ts +6 -6
  22. package/frontend/src/features/review/ReviewCard.tsx +14 -10
  23. package/frontend/src/features/review/ReviewInbox.tsx +24 -9
  24. package/frontend/src/i18n.ts +208 -0
  25. package/frontend/src/pages/Brain.tsx +63 -5
  26. package/frontend/src/pages/Capture.tsx +139 -43
  27. package/frontend/src/pages/Library.tsx +72 -6
  28. package/lattice_brain/__init__.py +1 -1
  29. package/lattice_brain/runtime/multi_agent.py +1 -1
  30. package/latticeai/__init__.py +1 -1
  31. package/latticeai/app_factory.py +48 -106
  32. package/latticeai/core/marketplace.py +1 -1
  33. package/latticeai/core/workspace_os.py +1 -1
  34. package/latticeai/runtime/access_runtime.py +97 -0
  35. package/latticeai/runtime/chat_wiring.py +119 -0
  36. package/latticeai/runtime/model_wiring.py +40 -0
  37. package/latticeai/runtime/platform_runtime_wiring.py +2 -3
  38. package/latticeai/runtime/review_wiring.py +37 -0
  39. package/latticeai/runtime/tail_wiring.py +21 -0
  40. package/package.json +2 -2
  41. package/scripts/check_i18n_literals.mjs +52 -0
  42. package/src-tauri/Cargo.lock +1 -1
  43. package/src-tauri/Cargo.toml +1 -1
  44. package/src-tauri/tauri.conf.json +1 -1
  45. package/static/app/asset-manifest.json +5 -5
  46. package/static/app/assets/index-Div5vMlq.css +2 -0
  47. package/static/app/assets/{index-D91Rz5--.js → index-t1jx1BR9.js} +2 -2
  48. package/static/app/assets/index-t1jx1BR9.js.map +1 -0
  49. package/static/app/index.html +2 -2
  50. package/static/app/assets/index-B2-1Gm0q.css +0 -2
  51. package/static/app/assets/index-D91Rz5--.js.map +0 -1
package/README.md CHANGED
@@ -69,42 +69,42 @@ You need Lattice AI when:
69
69
  Choose the owner of the Brain. The profile is not a SaaS account by default; it
70
70
  is the local identity for the knowledge you keep.
71
71
 
72
- ![Login](output/release/v6.2.0/screenshots/01-login.png)
72
+ ![Login](output/release/v6.3.1/screenshots/01-login.png)
73
73
 
74
74
  ### 2. Environment Analysis
75
75
 
76
76
  See what kind of local AI experience this computer can support before choosing a
77
77
  model.
78
78
 
79
- ![Environment Analysis](output/release/v6.2.0/screenshots/02-environment-analysis.png)
79
+ ![Environment Analysis](output/release/v6.3.1/screenshots/02-environment-analysis.png)
80
80
 
81
81
  ### 3. Recommended Models
82
82
 
83
83
  Start with a short list: safest recommendation, faster model, stronger model.
84
84
  Advanced details stay available without overwhelming first-time users.
85
85
 
86
- ![Recommended Models](output/release/v6.2.0/screenshots/03-recommended-models.png)
86
+ ![Recommended Models](output/release/v6.3.1/screenshots/03-recommended-models.png)
87
87
 
88
88
  ### 4. Install And Load
89
89
 
90
90
  Download and load only after consent. Lattice explains model size, local
91
91
  execution, and network use before work starts.
92
92
 
93
- ![Install and Load](output/release/v6.2.0/screenshots/04-install-load-progress.png)
93
+ ![Install and Load](output/release/v6.3.1/screenshots/04-install-load-progress.png)
94
94
 
95
95
  ### 5. Brain Chat
96
96
 
97
97
  Talk normally. Useful decisions and context become memory, then appear later as
98
98
  topics, relationships, and graph structure.
99
99
 
100
- ![Brain Chat Home](output/release/v6.2.0/screenshots/05-brain-chat-home.png)
100
+ ![Brain Chat Home](output/release/v6.3.1/screenshots/05-brain-chat-home.png)
101
101
 
102
102
  ### 6. Review Center
103
103
 
104
104
  Automation results are staged for review before they become durable decisions.
105
105
  Snooze, unsnooze, run now, approve, and dismiss actions stay explicit.
106
106
 
107
- ![Review Center](output/release/v6.2.0/screenshots/13-review-center.png)
107
+ ![Review Center](output/release/v6.3.1/screenshots/13-review-center.png)
108
108
 
109
109
  ## Brain Depths
110
110
 
@@ -120,10 +120,10 @@ The user travels inward from everyday memory to deeper structure:
120
120
 
121
121
  Walkthrough:
122
122
 
123
- ![v6.2.0 Living Brain walkthrough](output/release/v6.2.0/gifs/v6.2.0-living-brain-walkthrough.gif)
123
+ ![v6.3.1 Living Brain walkthrough](output/release/v6.3.1/gifs/v6.3.1-living-brain-walkthrough.gif)
124
124
 
125
125
  Screenshot index and capture notes:
126
- [output/release/v6.2.0/SCREENSHOT_INDEX.md](output/release/v6.2.0/SCREENSHOT_INDEX.md)
126
+ [output/release/v6.3.1/SCREENSHOT_INDEX.md](output/release/v6.3.1/SCREENSHOT_INDEX.md)
127
127
 
128
128
  ## Install
129
129
 
@@ -200,7 +200,7 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for developer workflow details.
200
200
 
201
201
  ## Current Release Preparation
202
202
 
203
- The current development target is **6.2.0 Product Decomposition / Release Smoke Automation**:
203
+ The current development target is **6.3.1 Access Runtime / i18n Follow-up**:
204
204
 
205
205
  - `App.tsx` now delegates the main Brain and Admin surfaces to focused feature
206
206
  modules instead of owning the full product shell.
@@ -217,14 +217,22 @@ The current development target is **6.2.0 Product Decomposition / Release Smoke
217
217
  app-factory parameter sprawl while preserving route order.
218
218
  - Release smoke automation now checks wheel install, npm tgz contents, static
219
219
  assets, and Tauri artifacts after exact-version artifact validation.
220
-
221
- Expected artifacts for 6.2.0 release must use exact filenames:
222
-
223
- - `dist/ltcai-6.2.0-py3-none-any.whl`
224
- - `dist/ltcai-6.2.0.tar.gz`
225
- - `ltcai-6.2.0.tgz`
226
- - `dist/ltcai-6.2.0.vsix`
227
- - `src-tauri/target/release/bundle/dmg/Lattice AI_6.2.0_aarch64.dmg`
220
+ - Brain archive, memory provenance, document ingestion, Review Center Run Now,
221
+ local model runtime, root-shim smoke, and app-factory review wiring now have
222
+ dedicated UX/backend hardening coverage for the 6.3 line.
223
+ - Access-control helper closures are extracted from `app_factory.py` into a
224
+ focused runtime module with direct unit coverage for role lookup, token
225
+ extraction, user enforcement, admin enforcement, and public user projection.
226
+ - Capture and Review Center copy now route through the shared Korean/English
227
+ i18n map for tabs, placeholders, action labels, empty states, and feedback.
228
+
229
+ Expected artifacts for 6.3.1 release must use exact filenames:
230
+
231
+ - `dist/ltcai-6.3.1-py3-none-any.whl`
232
+ - `dist/ltcai-6.3.1.tar.gz`
233
+ - `ltcai-6.3.1.tgz`
234
+ - `dist/ltcai-6.3.1.vsix`
235
+ - `src-tauri/target/release/bundle/dmg/Lattice AI_6.3.1_aarch64.dmg`
228
236
 
229
237
  Do not upload `dist/*`. Package registry publishing remains owner-run.
230
238
 
@@ -243,6 +251,10 @@ Do not upload `dist/*`. Package registry publishing remains owner-run.
243
251
 
244
252
  | Version | Theme |
245
253
  | --- | --- |
254
+ | 6.3.1 | Access Runtime / i18n Follow-up: app-factory access-control extraction, focused access runtime tests, and Capture/Review Center i18n coverage |
255
+ | 6.3.0 | Product Hardening Completion: Brain archive/provenance/ingestion UX polish, Review Center Run Now contract hardening, local model runtime status, app-factory review wiring, shim smoke, i18n guard, and exact release artifacts |
256
+ | 6.2.0 | Product Decomposition / Release Smoke Automation: App and ProductFlow feature extraction, legacy root shim shrink, model download consent UX, typed router contexts, localized smoke tests, and wheel/npm/static/Tauri release smoke |
257
+ | 6.1.0 | Product Hardening / Digital Brain Completion: local Brain entry without a loaded model, Brain memory/backup loop, Review Center semantics, ToolRegistry authorization boundary, and root CLI/runtime compatibility |
246
258
  | 6.0.0 | Product Reset / Review Center Completion: Snoozed filter, Unsnooze, OpenAPI-derived Review typing, Review feature extraction, v6 docs and scorecard |
247
259
  | 5.6.0 | Brain Automation Review Center: workspace-scoped automation review inbox, source-aware provenance, guarded approve/dismiss/snooze/run_now actions, and Act Review tab |
248
260
  | 5.5.0 | Release Coordination: synchronized package/runtime/static versions and release docs for the 5.5.0 line while preserving v5.4.0 Brain Automation Scheduler behavior |
package/docs/CHANGELOG.md CHANGED
@@ -3,6 +3,72 @@
3
3
  The top entry is the current release-preparation target. Older entries are
4
4
  historical and may describe behavior as it existed at that release.
5
5
 
6
+ ## [6.3.1] - 2026-06-17
7
+
8
+ > Access Runtime / i18n Follow-up. Closes the next app-factory decomposition
9
+ > slice and finishes Capture/Review Center localization coverage on top of the
10
+ > v6.3.0 product-hardening line.
11
+
12
+ ### Changed
13
+ - Moved app-factory access-control helper closures into
14
+ `latticeai.runtime.access_runtime` while preserving role lookup, token
15
+ extraction, user enforcement, admin enforcement, and public user projection
16
+ call signatures.
17
+ - Capture tabs, headings, placeholders, action labels, upload details, folder
18
+ connection copy, browser capture copy, and pipeline controls now use the
19
+ shared Korean/English i18n map.
20
+ - Review Center inbox/card copy, status badges, empty states, Run Now feedback,
21
+ snooze/unsnooze labels, and action aria labels now use the shared i18n map.
22
+ - README release evidence screenshots and walkthrough GIF are refreshed under
23
+ `output/release/v6.3.1/`.
24
+ - Package/runtime/static metadata is synchronized to 6.3.1; package publish and
25
+ deployment remain owner-run only.
26
+
27
+ ### Added
28
+ - Focused unit coverage for the extracted access runtime auth/admin behavior.
29
+
30
+ ## [6.3.0] - 2026-06-16
31
+
32
+ > Product Hardening Completion. Completes the 6.2 decomposition follow-up by
33
+ > tightening Brain archive, provenance, ingestion, Review Center, local model
34
+ > runtime, app-factory wiring, compatibility shim, i18n, and release-validation
35
+ > surfaces.
36
+
37
+ ### Changed
38
+ - Brain Home and onboarding screens are split into finer feature-owned modules
39
+ while preserving the existing routes, CSS surface, and visual behavior.
40
+ - Platform and automation runtime wiring moved behind a dedicated app-factory
41
+ seam, keeping construction side-effect free and route order unchanged.
42
+ - Admin, Brain, and onboarding localized aria/placeholder surfaces now use the
43
+ shared i18n map.
44
+ - Brain archive care now validates archive paths, confirms passphrases, gives
45
+ strength guidance, and summarizes inspect/restore-preview results without
46
+ dumping raw JSON.
47
+ - Document capture now exposes a drop/choose ingest queue with progress,
48
+ failure reason, retry, source metadata, and post-ingest Brain/Graph
49
+ confirmation.
50
+ - Brain memory and graph surfaces now show provenance/source type, created time,
51
+ source path/title/conversation fallback, and copy-source affordances.
52
+ - Review Center cards now keep visible action result/error feedback for
53
+ run-now/approve/dismiss/snooze flows while preserving run-now as
54
+ preview/regenerate rather than approval.
55
+ - Local model setup now reports loaded model, engine state, cache/storage path,
56
+ download/load progress, reload/unload controls, and an honest no-model state.
57
+ - App factory model/chat/review-tail wiring moved into focused runtime wiring
58
+ modules while preserving route order.
59
+ - Review Center run-now backend wiring moved into `latticeai.runtime.review_wiring`
60
+ and is covered by scoped runner/status-preservation tests.
61
+ - Legacy root shim import smoke now covers server, knowledge graph, CLI,
62
+ Telegram, and P-Reinforce compatibility paths.
63
+ - Package/runtime/static metadata is synchronized to 6.3.0; package publish and
64
+ deployment remain owner-run only.
65
+
66
+ ### Added
67
+ - Frontend i18n literal check for localized aria/placeholder props in Brain,
68
+ Admin, and onboarding TSX surfaces.
69
+ - Focused runtime wiring modules for model runtime, chat/interaction contexts,
70
+ and review/Brain tail registration.
71
+
6
72
  ## [6.2.0] - 2026-06-16
7
73
 
8
74
  > Product Decomposition / Release Smoke Automation. Splits the largest product
@@ -0,0 +1,127 @@
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">
27
+ <div className="ritual-fact-label">{item.label}</div>
28
+ <div className="ritual-fact-value">{item.value}</div>
29
+ <div className="ritual-fact-detail">{item.detail}</div>
30
+ </div>
31
+ ))}
32
+ </div>
33
+
34
+ <div className="ritual-card ritual-analysis-card">
35
+ <div className="ritual-inline-row">
36
+ <Sparkles className="ritual-core-icon" />
37
+ <div>
38
+ <div className="ritual-strong-text">{analysis ? recommendedSummary(analysis, language) : t(language, "flow.analysis.finding")}</div>
39
+ <div className="ritual-muted-text">
40
+ {analysis ? t(language, "flow.analysis.ready") : t(language, "flow.analysis.wait")}
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ {error && <div className="ritual-card ritual-error-card" role="alert">{error}</div>}
47
+
48
+ <div className="ritual-centered-actions">
49
+ <Button onClick={onContinue} disabled={!analysis && !error} className="ritual-wide-button">
50
+ {t(language, "flow.analysis.continue")}
51
+ </Button>
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ function buildDetectedFacts(analysis: FlowAnalysis | null, language: Language) {
58
+ if (!analysis) {
59
+ return [
60
+ { label: t(language, "flow.analysis.fact.computer"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.computerDetail") },
61
+ { label: t(language, "flow.analysis.fact.memory"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.memoryDetail") },
62
+ { label: t(language, "flow.analysis.fact.graphics"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.graphicsDetail") },
63
+ { label: t(language, "flow.analysis.fact.support"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.supportDetail") },
64
+ { label: t(language, "flow.analysis.fact.models"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.modelsDetail") },
65
+ ];
66
+ }
67
+ const setupEnv = asRecord(analysis.setup?.environment);
68
+ const recProfile = asRecord(analysis.recommendations?.profile);
69
+ const recs = asRecord(analysis.recommendations?.recommendations);
70
+ const models = asRecord(analysis.models);
71
+ const sysinfo = asRecord(analysis.sysinfo);
72
+ const profile = { ...setupEnv, ...recProfile };
73
+ const ramGb = Number(recs.ram_gb || Number(profile.ram_mb || 0) / 1024 || 0);
74
+ const appleSilicon = Boolean(recs.apple_silicon || String(profile.arch || "").includes("arm"));
75
+ const loadedModels = asArray(models.loaded);
76
+ const gpu = asRecord(profile.gpu);
77
+ const installedRuntimes = [
78
+ ...asArray(setupEnv.installed_runtimes),
79
+ ...asArray(profile.installed_runtimes),
80
+ ...asArray(recs.installed_runtimes),
81
+ ];
82
+ return [
83
+ {
84
+ label: t(language, "flow.analysis.fact.computer"),
85
+ value: appleSilicon ? t(language, "flow.analysis.apple") : friendlyOs(profile.os, language),
86
+ detail: t(language, "flow.analysis.readyDetail"),
87
+ },
88
+ {
89
+ label: t(language, "flow.analysis.fact.memory"),
90
+ value: ramGb ? `${Math.round(ramGb)} GB` : t(language, "flow.analysis.detected"),
91
+ detail: t(language, "flow.analysis.memoryReadyDetail"),
92
+ },
93
+ {
94
+ label: t(language, "flow.analysis.fact.graphics"),
95
+ value: gpu.vendor || sysinfo.gpu_mem_gb ? t(language, "flow.analysis.localReady") : t(language, "flow.analysis.standardLocal"),
96
+ detail: t(language, "flow.analysis.graphicsReadyDetail"),
97
+ },
98
+ {
99
+ label: t(language, "flow.analysis.fact.support"),
100
+ value: installedRuntimes.length ? t(language, "flow.analysis.supportReady") : t(language, "flow.analysis.supportInstall"),
101
+ detail: installedRuntimes.length ? t(language, "flow.analysis.supportReadyDetail") : t(language, "flow.analysis.supportInstallDetail"),
102
+ },
103
+ {
104
+ label: t(language, "flow.analysis.fact.models"),
105
+ value: loadedModels.length ? t(language, "flow.analysis.modelsInstalled", { count: loadedModels.length }) : t(language, "flow.analysis.noModels"),
106
+ detail: loadedModels.length ? t(language, "flow.analysis.modelsReadyDetail") : t(language, "flow.analysis.modelsInstallDetail"),
107
+ },
108
+ ];
109
+ }
110
+
111
+ function recommendedSummary(analysis: FlowAnalysis, language: Language) {
112
+ const recs = asRecord(analysis.recommendations?.recommendations);
113
+ const topPick = asRecord(recs.top_pick);
114
+ if (topPick.name || topPick.id) {
115
+ const model = friendlyModelName(String(topPick.name || topPick.id));
116
+ return t(language, "flow.analysis.bestFit", { model });
117
+ }
118
+ return t(language, "flow.analysis.privateRecommended");
119
+ }
120
+
121
+ function friendlyOs(value: unknown, language: Language) {
122
+ const text = String(value || t(language, "flow.analysis.fact.computer"));
123
+ if (/darwin|mac/i.test(text)) return "Mac";
124
+ if (/win/i.test(text)) return t(language, "flow.analysis.os.windows");
125
+ if (/linux/i.test(text)) return t(language, "flow.analysis.os.linux");
126
+ return t(language, "flow.analysis.fact.computer");
127
+ }
@@ -0,0 +1,29 @@
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
+ }
@@ -0,0 +1,208 @@
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
+ <div className="ritual-install-brain">
85
+ <LivingBrain
86
+ state={brainStateForStage}
87
+ intensity={stage === "download" || stage === "load" ? 0.96 : 0.82}
88
+ size="normal"
89
+ />
90
+ </div>
91
+
92
+ <DownloadConsentPanel model={model} />
93
+
94
+ <div className="ritual-progress">
95
+ <div className="ritual-stage-list">
96
+ {(["install", "download", "validate", "load"] as const).map((item) => (
97
+ <div key={item} className={`ritual-stage ${installStepState(stage, item)}`}>
98
+ <CheckCircle2 className="ritual-stage-icon" />
99
+ <span>{installLabel(item, language)}</span>
100
+ </div>
101
+ ))}
102
+ </div>
103
+
104
+ <div className="ritual-bar">
105
+ <span className={`ritual-bar-fill ${progressClass(percent)}`} />
106
+ </div>
107
+ </div>
108
+
109
+ <div className="ritual-status">{message}</div>
110
+ <div className="ritual-card ritual-status-card">
111
+ {t(language, "flow.install.note")}
112
+ </div>
113
+
114
+ {error && (
115
+ <div className="ritual-card ritual-error-card ritual-install-error" role="alert">
116
+ {error}
117
+ <div className="ritual-error-detail">{t(language, "flow.install.retry")}</div>
118
+ </div>
119
+ )}
120
+
121
+ <div className="ritual-button-row">
122
+ <Button variant="ghost" onClick={onBack} disabled={busy}>{t(language, "flow.install.back")}</Button>
123
+ <Button variant="outline" onClick={onLater} disabled={busy}>{t(language, "flow.install.later")}</Button>
124
+
125
+ {stage !== "done" ? (
126
+ <Button
127
+ onClick={start}
128
+ disabled={busy || !model.supported}
129
+ >
130
+ {busy ? t(language, "flow.install.busy") : t(language, "flow.install.start")}
131
+ </Button>
132
+ ) : (
133
+ <Button onClick={onComplete}>{t(language, "flow.install.enter")}</Button>
134
+ )}
135
+ </div>
136
+
137
+ <div className="ritual-local-note">
138
+ {t(language, "flow.install.local")}
139
+ </div>
140
+ </div>
141
+ );
142
+ }
143
+
144
+ function friendlyInstallStage(stage: string): InstallStage {
145
+ if (/download|pull|weights/i.test(stage)) return "download";
146
+ if (/smoke|validate|verify|test/i.test(stage)) return "validate";
147
+ if (/load|ready/i.test(stage)) return "load";
148
+ if (/done|complete/i.test(stage)) return "done";
149
+ return "install";
150
+ }
151
+
152
+ function percentForStage(stage: InstallStage) {
153
+ if (stage === "install") return 20;
154
+ if (stage === "download") return 55;
155
+ if (stage === "validate") return 82;
156
+ if (stage === "load") return 94;
157
+ if (stage === "done") return 100;
158
+ return 8;
159
+ }
160
+
161
+ function friendlyInstallMessage(event: ApiData, stage: InstallStage, language: Language) {
162
+ const fallback = {
163
+ install: t(language, "flow.install.prepare"),
164
+ download: t(language, "flow.install.stage.download"),
165
+ validate: t(language, "flow.install.stage.validate"),
166
+ load: t(language, "flow.install.stage.load"),
167
+ done: t(language, "flow.install.done"),
168
+ idle: t(language, "flow.install.wait"),
169
+ error: t(language, "flow.install.stage.error"),
170
+ }[stage];
171
+ return cleanConsumerText(String(event.user_message || event.message || fallback));
172
+ }
173
+
174
+ function installLabel(stage: "install" | "download" | "validate" | "load", language: Language) {
175
+ return t(language, `flow.install.step.${stage}`);
176
+ }
177
+
178
+ function progressClass(percent: number) {
179
+ const step = Math.max(0, Math.min(100, Math.round(percent / 10) * 10));
180
+ return `progress-${step}`;
181
+ }
182
+
183
+ function installStepState(current: InstallStage, item: "install" | "download" | "validate" | "load") {
184
+ const order: InstallStage[] = ["idle", "install", "download", "validate", "load", "done"];
185
+ const currentIndex = order.indexOf(current);
186
+ const itemIndex = order.indexOf(item);
187
+ if (current === "error") return "is-error";
188
+ if (current === "done" || currentIndex > itemIndex) return "is-done";
189
+ if (current === item) return "is-active";
190
+ return "";
191
+ }
192
+
193
+ function consumerError(data: ApiData | unknown) {
194
+ const record = asRecord(data);
195
+ const guidance = asArray<string>(record.recovery_guidance).map(cleanConsumerText).filter(Boolean);
196
+ const message = cleanConsumerText(String(record.user_message || record.reason || record.error || "The selected model could not be loaded."));
197
+ return [message, ...guidance.slice(0, 2)].join(" ");
198
+ }
199
+
200
+ function cleanConsumerText(value: string) {
201
+ return String(value || "")
202
+ .replace(/gemma4_unified/gi, "this model format")
203
+ .replace(/mlx[-_ ]?vlm|mlx[-_ ]?lm|local_mlx|\bmlx\b|\bgguf\b|\bollama\b|huggingface|hugging face/gi, "local model support")
204
+ .replace(/runtime/gi, "model support")
205
+ .replace(/No module named ['\"][^'\"]+['\"]/gi, "A local support component is missing")
206
+ .replace(/\s+/g, " ")
207
+ .trim();
208
+ }
@@ -0,0 +1,23 @@
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
+ }