ltcai 6.2.0 → 6.3.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 (50) hide show
  1. package/README.md +18 -15
  2. package/docs/CHANGELOG.md +42 -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 +3 -1
  23. package/frontend/src/features/review/ReviewInbox.tsx +11 -1
  24. package/frontend/src/i18n.ts +92 -0
  25. package/frontend/src/pages/Brain.tsx +63 -5
  26. package/frontend/src/pages/Capture.tsx +102 -13
  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 +31 -39
  32. package/latticeai/core/marketplace.py +1 -1
  33. package/latticeai/core/workspace_os.py +1 -1
  34. package/latticeai/runtime/chat_wiring.py +119 -0
  35. package/latticeai/runtime/model_wiring.py +40 -0
  36. package/latticeai/runtime/platform_runtime_wiring.py +2 -3
  37. package/latticeai/runtime/review_wiring.py +37 -0
  38. package/latticeai/runtime/tail_wiring.py +21 -0
  39. package/package.json +2 -2
  40. package/scripts/check_i18n_literals.mjs +52 -0
  41. package/src-tauri/Cargo.lock +1 -1
  42. package/src-tauri/Cargo.toml +1 -1
  43. package/src-tauri/tauri.conf.json +1 -1
  44. package/static/app/asset-manifest.json +5 -5
  45. package/static/app/assets/{index-D91Rz5--.js → index-D76dWuQk.js} +2 -2
  46. package/static/app/assets/index-D76dWuQk.js.map +1 -0
  47. package/static/app/assets/index-Div5vMlq.css +2 -0
  48. package/static/app/index.html +2 -2
  49. package/static/app/assets/index-B2-1Gm0q.css +0 -2
  50. 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.0/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.0/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.0/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.0/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.0/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.0/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.0 Living Brain walkthrough](output/release/v6.3.0/gifs/v6.3.0-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.0/SCREENSHOT_INDEX.md](output/release/v6.3.0/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.0 Product Hardening Completion**:
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,17 @@ 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
+ - 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.
220
223
 
221
- Expected artifacts for 6.2.0 release must use exact filenames:
224
+ Expected artifacts for 6.3.0 release must use exact filenames:
222
225
 
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`
226
+ - `dist/ltcai-6.3.0-py3-none-any.whl`
227
+ - `dist/ltcai-6.3.0.tar.gz`
228
+ - `ltcai-6.3.0.tgz`
229
+ - `dist/ltcai-6.3.0.vsix`
230
+ - `src-tauri/target/release/bundle/dmg/Lattice AI_6.3.0_aarch64.dmg`
228
231
 
229
232
  Do not upload `dist/*`. Package registry publishing remains owner-run.
230
233
 
package/docs/CHANGELOG.md CHANGED
@@ -3,6 +3,48 @@
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.0] - 2026-06-16
7
+
8
+ > Product Hardening Completion. Completes the 6.2 decomposition follow-up by
9
+ > tightening Brain archive, provenance, ingestion, Review Center, local model
10
+ > runtime, app-factory wiring, compatibility shim, i18n, and release-validation
11
+ > surfaces.
12
+
13
+ ### Changed
14
+ - Brain Home and onboarding screens are split into finer feature-owned modules
15
+ while preserving the existing routes, CSS surface, and visual behavior.
16
+ - Platform and automation runtime wiring moved behind a dedicated app-factory
17
+ seam, keeping construction side-effect free and route order unchanged.
18
+ - Admin, Brain, and onboarding localized aria/placeholder surfaces now use the
19
+ shared i18n map.
20
+ - Brain archive care now validates archive paths, confirms passphrases, gives
21
+ strength guidance, and summarizes inspect/restore-preview results without
22
+ dumping raw JSON.
23
+ - Document capture now exposes a drop/choose ingest queue with progress,
24
+ failure reason, retry, source metadata, and post-ingest Brain/Graph
25
+ confirmation.
26
+ - Brain memory and graph surfaces now show provenance/source type, created time,
27
+ source path/title/conversation fallback, and copy-source affordances.
28
+ - Review Center cards now keep visible action result/error feedback for
29
+ run-now/approve/dismiss/snooze flows while preserving run-now as
30
+ preview/regenerate rather than approval.
31
+ - Local model setup now reports loaded model, engine state, cache/storage path,
32
+ download/load progress, reload/unload controls, and an honest no-model state.
33
+ - App factory model/chat/review-tail wiring moved into focused runtime wiring
34
+ modules while preserving route order.
35
+ - Review Center run-now backend wiring moved into `latticeai.runtime.review_wiring`
36
+ and is covered by scoped runner/status-preservation tests.
37
+ - Legacy root shim import smoke now covers server, knowledge graph, CLI,
38
+ Telegram, and P-Reinforce compatibility paths.
39
+ - Package/runtime/static metadata is synchronized to 6.3.0; package publish and
40
+ deployment remain owner-run only.
41
+
42
+ ### Added
43
+ - Frontend i18n literal check for localized aria/placeholder props in Brain,
44
+ Admin, and onboarding TSX surfaces.
45
+ - Focused runtime wiring modules for model runtime, chat/interaction contexts,
46
+ and review/Brain tail registration.
47
+
6
48
  ## [6.2.0] - 2026-06-16
7
49
 
8
50
  > 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
+ }
@@ -0,0 +1,131 @@
1
+ import * as React from "react";
2
+ import { latticeApi } from "@/api/client";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Input } from "@/components/ui/input";
5
+ import { t } from "@/i18n";
6
+ import { useAppStore } from "@/store/appStore";
7
+
8
+ const FLOW_USER_KEY = "lattice.productFlow.user";
9
+
10
+ function readSavedFlowUser(): { email?: string; name?: string } | null {
11
+ try {
12
+ const raw = localStorage.getItem(FLOW_USER_KEY);
13
+ return raw ? JSON.parse(raw) : null;
14
+ } catch {}
15
+ return null;
16
+ }
17
+
18
+ export function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
19
+ const language = useAppStore((state) => state.language);
20
+ const [email, setEmail] = React.useState(() => {
21
+ return readSavedFlowUser()?.email || "you@local";
22
+ });
23
+ const [password, setPassword] = React.useState("");
24
+ const [name, setName] = React.useState(() => readSavedFlowUser()?.name || "You");
25
+ const [busy, setBusy] = React.useState(false);
26
+ const [error, setError] = React.useState<string | null>(null);
27
+
28
+ async function submit(event: React.FormEvent) {
29
+ event.preventDefault();
30
+ const cleanEmail = email.trim();
31
+ const cleanPassword = password.trim();
32
+ const cleanName = name.trim() || cleanEmail.split("@")[0] || "You";
33
+ if (!cleanEmail || !cleanPassword) {
34
+ setError(t(language, "flow.login.missing"));
35
+ return;
36
+ }
37
+ setBusy(true);
38
+ setError(null);
39
+ const savedUser = readSavedFlowUser();
40
+ let result = await latticeApi.login(cleanEmail, cleanPassword);
41
+ if (!result.ok) {
42
+ const profile = await latticeApi.profile();
43
+ if (profile.ok && (!savedUser?.email || savedUser.email === cleanEmail)) {
44
+ try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
45
+ setBusy(false);
46
+ onSuccess();
47
+ return;
48
+ }
49
+ if (savedUser?.email && savedUser.email !== cleanEmail) {
50
+ setBusy(false);
51
+ setError(t(language, "flow.login.otherEmail"));
52
+ return;
53
+ }
54
+ if (savedUser?.email === cleanEmail) {
55
+ setBusy(false);
56
+ setError(t(language, "flow.login.wrongPassword"));
57
+ return;
58
+ }
59
+ const registered = await latticeApi.register({
60
+ email: cleanEmail,
61
+ password: cleanPassword,
62
+ name: cleanName,
63
+ nickname: cleanName,
64
+ });
65
+ if (registered.ok) result = await latticeApi.login(cleanEmail, cleanPassword);
66
+ }
67
+ if (!result.ok) {
68
+ setBusy(false);
69
+ setError(t(language, "flow.login.unavailable"));
70
+ return;
71
+ }
72
+ try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
73
+ setBusy(false);
74
+ onSuccess();
75
+ }
76
+
77
+ return (
78
+ <div>
79
+ <div className="ritual-title">{t(language, "flow.login.title")}</div>
80
+ <div className="ritual-subtitle">{t(language, "flow.login.body")}</div>
81
+
82
+ <ProductPromise />
83
+
84
+ <form onSubmit={submit} className="ritual-card ritual-form">
85
+ <div className="ritual-field-stack">
86
+ <div>
87
+ <div className="ritual-field-label">{t(language, "flow.name")}</div>
88
+ <Input value={name} onChange={(e) => setName(e.target.value)} placeholder={t(language, "flow.name.placeholder")} />
89
+ </div>
90
+ <div>
91
+ <div className="ritual-field-label">{t(language, "flow.email")}</div>
92
+ <Input value={email} onChange={(e) => setEmail(e.target.value)} type="email" placeholder={t(language, "flow.email.placeholder")} />
93
+ </div>
94
+ <div>
95
+ <div className="ritual-field-label">{t(language, "flow.password")}</div>
96
+ <Input value={password} onChange={(e) => setPassword(e.target.value)} type="password" placeholder={t(language, "flow.password.placeholder")} />
97
+ </div>
98
+ </div>
99
+
100
+ {error && <div className="ritual-error" role="alert">{error}</div>}
101
+
102
+ <Button type="submit" disabled={busy || !email.trim() || !password.trim()} className="ritual-full-button">
103
+ {busy ? t(language, "flow.login.busy") : t(language, "flow.login.submit")}
104
+ </Button>
105
+ <div className="ritual-note">
106
+ {t(language, "flow.login.note")}
107
+ </div>
108
+ </form>
109
+ </div>
110
+ );
111
+ }
112
+
113
+ function ProductPromise() {
114
+ const language = useAppStore((state) => state.language);
115
+ return (
116
+ <div className="ritual-promise" aria-label={t(language, "flow.promise.aria")}>
117
+ <div>
118
+ <span>{t(language, "flow.promise.memory.k")}</span>
119
+ <strong>{t(language, "flow.promise.memory.v")}</strong>
120
+ </div>
121
+ <div>
122
+ <span>{t(language, "flow.promise.model.k")}</span>
123
+ <strong>{t(language, "flow.promise.model.v")}</strong>
124
+ </div>
125
+ <div>
126
+ <span>{t(language, "flow.promise.ownership.k")}</span>
127
+ <strong>{t(language, "flow.promise.ownership.v")}</strong>
128
+ </div>
129
+ </div>
130
+ );
131
+ }