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.
- package/README.md +18 -15
- package/docs/CHANGELOG.md +42 -0
- package/frontend/src/components/onboarding/AnalysisScreen.tsx +127 -0
- package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +29 -0
- package/frontend/src/components/onboarding/InstallScreen.tsx +208 -0
- package/frontend/src/components/onboarding/LanguageChooser.tsx +23 -0
- package/frontend/src/components/onboarding/LoginScreen.tsx +131 -0
- package/frontend/src/components/onboarding/ProductFlowScreens.tsx +13 -688
- package/frontend/src/components/onboarding/RecommendationScreen.tsx +59 -0
- package/frontend/src/components/onboarding/recommendationModel.ts +132 -0
- package/frontend/src/features/admin/AdminConsole.tsx +1 -1
- package/frontend/src/features/brain/BrainCarePanel.tsx +254 -0
- package/frontend/src/features/brain/BrainComposer.tsx +66 -0
- package/frontend/src/features/brain/BrainConversation.tsx +131 -0
- package/frontend/src/features/brain/BrainGraphLayer.tsx +132 -0
- package/frontend/src/features/brain/BrainHome.tsx +28 -795
- package/frontend/src/features/brain/BrainMemoryLayer.tsx +38 -0
- package/frontend/src/features/brain/BrainOverviewPanel.tsx +74 -0
- package/frontend/src/features/brain/BrainRelationshipLayer.tsx +43 -0
- package/frontend/src/features/brain/DepthEmergence.tsx +53 -0
- package/frontend/src/features/brain/types.ts +6 -6
- package/frontend/src/features/review/ReviewCard.tsx +3 -1
- package/frontend/src/features/review/ReviewInbox.tsx +11 -1
- package/frontend/src/i18n.ts +92 -0
- package/frontend/src/pages/Brain.tsx +63 -5
- package/frontend/src/pages/Capture.tsx +102 -13
- package/frontend/src/pages/Library.tsx +72 -6
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/app_factory.py +31 -39
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/chat_wiring.py +119 -0
- package/latticeai/runtime/model_wiring.py +40 -0
- package/latticeai/runtime/platform_runtime_wiring.py +2 -3
- package/latticeai/runtime/review_wiring.py +37 -0
- package/latticeai/runtime/tail_wiring.py +21 -0
- package/package.json +2 -2
- package/scripts/check_i18n_literals.mjs +52 -0
- 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 +5 -5
- package/static/app/assets/{index-D91Rz5--.js → index-D76dWuQk.js} +2 -2
- package/static/app/assets/index-D76dWuQk.js.map +1 -0
- package/static/app/assets/index-Div5vMlq.css +2 -0
- package/static/app/index.html +2 -2
- package/static/app/assets/index-B2-1Gm0q.css +0 -2
- 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
|
-

|
|
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
|
-

|
|
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
|
-

|
|
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
|
-

|
|
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
|
-

|
|
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
|
-

|
|
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
|
-

|
|
124
124
|
|
|
125
125
|
Screenshot index and capture notes:
|
|
126
|
-
[output/release/v6.
|
|
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.
|
|
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.
|
|
224
|
+
Expected artifacts for 6.3.0 release must use exact filenames:
|
|
222
225
|
|
|
223
|
-
- `dist/ltcai-6.
|
|
224
|
-
- `dist/ltcai-6.
|
|
225
|
-
- `ltcai-6.
|
|
226
|
-
- `dist/ltcai-6.
|
|
227
|
-
- `src-tauri/target/release/bundle/dmg/Lattice AI_6.
|
|
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
|
+
}
|