ltcai 6.1.0 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -38
- package/docs/CHANGELOG.md +33 -1
- package/frontend/src/App.tsx +3 -1286
- package/frontend/src/components/LanguageSwitcher.tsx +23 -0
- package/frontend/src/components/ProductFlow.tsx +32 -669
- package/frontend/src/components/onboarding/ProductFlowScreens.tsx +688 -0
- package/frontend/src/features/admin/AdminConsole.tsx +294 -0
- package/frontend/src/features/brain/BrainHome.tsx +999 -0
- package/frontend/src/features/brain/brainData.ts +98 -0
- package/frontend/src/features/brain/graphLayout.ts +26 -0
- package/frontend/src/features/brain/types.ts +44 -0
- package/frontend/src/i18n.ts +198 -0
- package/frontend/src/styles.css +220 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/tools.py +50 -23
- package/latticeai/app_factory.py +36 -45
- package/latticeai/cli/entrypoint.py +283 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/integrations/__init__.py +0 -0
- package/latticeai/integrations/telegram_bot.py +1009 -0
- package/latticeai/runtime/lifespan_runtime.py +1 -1
- package/latticeai/runtime/platform_runtime_wiring.py +87 -0
- package/latticeai/runtime/router_registration.py +49 -30
- package/latticeai/services/p_reinforce.py +258 -0
- package/latticeai/services/router_context.py +52 -0
- package/ltcai_cli.py +7 -279
- package/p_reinforce.py +4 -255
- package/package.json +2 -1
- package/scripts/release_smoke.py +133 -0
- package/scripts/wheel_smoke.py +4 -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-B744yblP.css → index-B2-1Gm0q.css} +1 -1
- package/static/app/assets/index-D91Rz5--.js +16 -0
- package/static/app/assets/index-D91Rz5--.js.map +1 -0
- package/static/app/index.html +2 -2
- package/telegram_bot.py +9 -1002
- package/static/app/assets/index-DYaUKNfl.js +0 -16
- package/static/app/assets/index-DYaUKNfl.js.map +0 -1
|
@@ -1,42 +1,22 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { CheckCircle2, ChevronRight, Cpu, Download, LockKeyhole, MonitorCog, Sparkles } from "lucide-react";
|
|
3
2
|
import { latticeApi } from "@/api/client";
|
|
4
|
-
import { LivingBrain } from "@/components/LivingBrain";
|
|
5
|
-
import {
|
|
6
|
-
import { Button } from "@/components/ui/button";
|
|
7
|
-
import { Input } from "@/components/ui/input";
|
|
8
|
-
import { cn, asArray } from "@/lib/utils";
|
|
9
|
-
import { LANGUAGE_LABELS, t, type Language } from "@/i18n";
|
|
3
|
+
import { type BrainState, LivingBrain } from "@/components/LivingBrain";
|
|
4
|
+
import { t } from "@/i18n";
|
|
10
5
|
import { useAppStore } from "@/store/appStore";
|
|
6
|
+
import {
|
|
7
|
+
AnalysisScreen,
|
|
8
|
+
InstallScreen,
|
|
9
|
+
LanguageChooser,
|
|
10
|
+
LoginScreen,
|
|
11
|
+
RecommendationScreen,
|
|
12
|
+
buildRecommendations,
|
|
13
|
+
fallbackModel,
|
|
14
|
+
type FlowAnalysis,
|
|
15
|
+
type FlowStep,
|
|
16
|
+
type RecommendedModel,
|
|
17
|
+
} from "@/components/onboarding/ProductFlowScreens";
|
|
11
18
|
|
|
12
19
|
const FLOW_COMPLETE_KEY = "lattice.productFlow.complete";
|
|
13
|
-
const FLOW_USER_KEY = "lattice.productFlow.user";
|
|
14
|
-
|
|
15
|
-
type FlowStep = "login" | "analysis" | "recommend" | "install";
|
|
16
|
-
type ApiData = Record<string, unknown>;
|
|
17
|
-
|
|
18
|
-
type FlowAnalysis = {
|
|
19
|
-
setup?: ApiData | null;
|
|
20
|
-
models?: ApiData | null;
|
|
21
|
-
recommendations?: ApiData | null;
|
|
22
|
-
sysinfo?: ApiData | null;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
type RecommendedModel = {
|
|
26
|
-
id: string;
|
|
27
|
-
loadId: string;
|
|
28
|
-
engine: string;
|
|
29
|
-
name: string;
|
|
30
|
-
shortName: string;
|
|
31
|
-
family: string;
|
|
32
|
-
size: string;
|
|
33
|
-
role: "best" | "faster" | "advanced";
|
|
34
|
-
reason: string;
|
|
35
|
-
supported: boolean;
|
|
36
|
-
downloadRequired: boolean;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
type InstallStage = "idle" | "install" | "download" | "validate" | "load" | "done" | "error";
|
|
40
20
|
|
|
41
21
|
export function readProductFlowComplete() {
|
|
42
22
|
try {
|
|
@@ -45,14 +25,6 @@ export function readProductFlowComplete() {
|
|
|
45
25
|
return false;
|
|
46
26
|
}
|
|
47
27
|
|
|
48
|
-
function readSavedFlowUser(): { email?: string; name?: string } | null {
|
|
49
|
-
try {
|
|
50
|
-
const raw = localStorage.getItem(FLOW_USER_KEY);
|
|
51
|
-
return raw ? JSON.parse(raw) : null;
|
|
52
|
-
} catch {}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
28
|
export function ProductFlow({ onComplete }: { onComplete: () => void }) {
|
|
57
29
|
const language = useAppStore((state) => state.language);
|
|
58
30
|
const [step, setStep] = React.useState<FlowStep>("login");
|
|
@@ -75,10 +47,10 @@ export function ProductFlow({ onComplete }: { onComplete: () => void }) {
|
|
|
75
47
|
]);
|
|
76
48
|
if (cancelled) return;
|
|
77
49
|
setAnalysis({
|
|
78
|
-
setup: setup.ok ? setup.data as
|
|
79
|
-
recommendations: recommendationsResult.ok ? recommendationsResult.data as
|
|
80
|
-
models: models.ok ? models.data as
|
|
81
|
-
sysinfo: sysinfo.ok ? sysinfo.data as
|
|
50
|
+
setup: setup.ok ? setup.data as Record<string, unknown> : null,
|
|
51
|
+
recommendations: recommendationsResult.ok ? recommendationsResult.data as Record<string, unknown> : null,
|
|
52
|
+
models: models.ok ? models.data as Record<string, unknown> : null,
|
|
53
|
+
sysinfo: sysinfo.ok ? sysinfo.data as Record<string, unknown> : null,
|
|
82
54
|
});
|
|
83
55
|
if (!setup.ok && !recommendationsResult.ok && !models.ok) {
|
|
84
56
|
setAnalysisError(t(language, "flow.analysis.error"));
|
|
@@ -92,41 +64,21 @@ export function ProductFlow({ onComplete }: { onComplete: () => void }) {
|
|
|
92
64
|
<div className="ritual-shell" aria-label={t(language, "flow.shell")}>
|
|
93
65
|
<div className="ritual-container">
|
|
94
66
|
<LanguageChooser />
|
|
95
|
-
{/* The living presence participates in the ritual at every step */}
|
|
96
67
|
<div className="ritual-brain">
|
|
97
|
-
<LivingBrain
|
|
98
|
-
state={
|
|
99
|
-
step === "login" ? "idle" :
|
|
100
|
-
step === "analysis" ? "listening" :
|
|
101
|
-
step === "recommend" ? "recalling" :
|
|
102
|
-
"thinking"
|
|
103
|
-
}
|
|
104
|
-
intensity={step === "install" ? 0.92 : 0.7}
|
|
105
|
-
size="large"
|
|
106
|
-
showLabel={false}
|
|
107
|
-
/>
|
|
68
|
+
<LivingBrain state={brainStateForStep(step)} intensity={step === "install" ? 0.92 : 0.7} size="large" showLabel={false} />
|
|
108
69
|
</div>
|
|
109
70
|
|
|
110
|
-
{step === "login" && (
|
|
111
|
-
<LoginScreen onSuccess={() => setStep("analysis")} />
|
|
112
|
-
)}
|
|
71
|
+
{step === "login" && <LoginScreen onSuccess={() => setStep("analysis")} />}
|
|
113
72
|
|
|
114
73
|
{step === "analysis" && (
|
|
115
|
-
<AnalysisScreen
|
|
116
|
-
analysis={analysis}
|
|
117
|
-
error={analysisError}
|
|
118
|
-
onContinue={() => setStep("recommend")}
|
|
119
|
-
/>
|
|
74
|
+
<AnalysisScreen analysis={analysis} error={analysisError} onContinue={() => setStep("recommend")} />
|
|
120
75
|
)}
|
|
121
76
|
|
|
122
77
|
{step === "recommend" && (
|
|
123
78
|
<RecommendationScreen
|
|
124
79
|
recommendations={recommendations}
|
|
125
80
|
onBack={() => setStep("analysis")}
|
|
126
|
-
onSkipModel={() =>
|
|
127
|
-
try { localStorage.setItem(FLOW_COMPLETE_KEY, "true"); } catch {}
|
|
128
|
-
onComplete();
|
|
129
|
-
}}
|
|
81
|
+
onSkipModel={() => completeFlow(onComplete)}
|
|
130
82
|
onSelect={(model) => {
|
|
131
83
|
setSelected(model);
|
|
132
84
|
setStep("install");
|
|
@@ -138,10 +90,8 @@ export function ProductFlow({ onComplete }: { onComplete: () => void }) {
|
|
|
138
90
|
<InstallScreen
|
|
139
91
|
model={selected || recommendations[0] || fallbackModel()}
|
|
140
92
|
onBack={() => setStep("recommend")}
|
|
141
|
-
onComplete={() =>
|
|
142
|
-
|
|
143
|
-
onComplete();
|
|
144
|
-
}}
|
|
93
|
+
onComplete={() => completeFlow(onComplete)}
|
|
94
|
+
onLater={() => completeFlow(onComplete)}
|
|
145
95
|
/>
|
|
146
96
|
)}
|
|
147
97
|
</div>
|
|
@@ -149,601 +99,14 @@ export function ProductFlow({ onComplete }: { onComplete: () => void }) {
|
|
|
149
99
|
);
|
|
150
100
|
}
|
|
151
101
|
|
|
152
|
-
function
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return (
|
|
157
|
-
<div className="language-switcher ritual-language" aria-label={t(language, "language.label")}>
|
|
158
|
-
{(["ko", "en"] as Language[]).map((item) => (
|
|
159
|
-
<button
|
|
160
|
-
key={item}
|
|
161
|
-
type="button"
|
|
162
|
-
className={language === item ? "is-active" : ""}
|
|
163
|
-
onClick={() => setLanguage(item)}
|
|
164
|
-
aria-pressed={language === item}
|
|
165
|
-
>
|
|
166
|
-
{LANGUAGE_LABELS[item]}
|
|
167
|
-
</button>
|
|
168
|
-
))}
|
|
169
|
-
</div>
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
|
|
174
|
-
const language = useAppStore((state) => state.language);
|
|
175
|
-
const [email, setEmail] = React.useState(() => {
|
|
176
|
-
return readSavedFlowUser()?.email || "you@local";
|
|
177
|
-
});
|
|
178
|
-
const [password, setPassword] = React.useState("");
|
|
179
|
-
const [name, setName] = React.useState(() => readSavedFlowUser()?.name || "You");
|
|
180
|
-
const [busy, setBusy] = React.useState(false);
|
|
181
|
-
const [error, setError] = React.useState<string | null>(null);
|
|
182
|
-
|
|
183
|
-
async function submit(event: React.FormEvent) {
|
|
184
|
-
event.preventDefault();
|
|
185
|
-
const cleanEmail = email.trim();
|
|
186
|
-
const cleanPassword = password.trim();
|
|
187
|
-
const cleanName = name.trim() || cleanEmail.split("@")[0] || "You";
|
|
188
|
-
if (!cleanEmail || !cleanPassword) {
|
|
189
|
-
setError(t(language, "flow.login.missing"));
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
setBusy(true);
|
|
193
|
-
setError(null);
|
|
194
|
-
const savedUser = readSavedFlowUser();
|
|
195
|
-
let result = await latticeApi.login(cleanEmail, cleanPassword);
|
|
196
|
-
if (!result.ok) {
|
|
197
|
-
const profile = await latticeApi.profile();
|
|
198
|
-
if (profile.ok && (!savedUser?.email || savedUser.email === cleanEmail)) {
|
|
199
|
-
try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
|
|
200
|
-
setBusy(false);
|
|
201
|
-
onSuccess();
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (savedUser?.email && savedUser.email !== cleanEmail) {
|
|
205
|
-
setBusy(false);
|
|
206
|
-
setError(t(language, "flow.login.otherEmail"));
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
if (savedUser?.email === cleanEmail) {
|
|
210
|
-
setBusy(false);
|
|
211
|
-
setError(t(language, "flow.login.wrongPassword"));
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
const registered = await latticeApi.register({
|
|
215
|
-
email: cleanEmail,
|
|
216
|
-
password: cleanPassword,
|
|
217
|
-
name: cleanName,
|
|
218
|
-
nickname: cleanName,
|
|
219
|
-
});
|
|
220
|
-
if (registered.ok) result = await latticeApi.login(cleanEmail, cleanPassword);
|
|
221
|
-
}
|
|
222
|
-
if (!result.ok) {
|
|
223
|
-
setBusy(false);
|
|
224
|
-
setError(t(language, "flow.login.unavailable"));
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
|
|
228
|
-
setBusy(false);
|
|
229
|
-
onSuccess();
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return (
|
|
233
|
-
<div>
|
|
234
|
-
<div className="ritual-title">{t(language, "flow.login.title")}</div>
|
|
235
|
-
<div className="ritual-subtitle">{t(language, "flow.login.body")}</div>
|
|
236
|
-
|
|
237
|
-
<ProductPromise />
|
|
238
|
-
|
|
239
|
-
<form onSubmit={submit} className="ritual-card" style={{ maxWidth: 420, margin: "0 auto" }}>
|
|
240
|
-
<div style={{ display: "grid", gap: "0.85rem" }}>
|
|
241
|
-
<div>
|
|
242
|
-
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>{t(language, "flow.name")}</div>
|
|
243
|
-
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="You" />
|
|
244
|
-
</div>
|
|
245
|
-
<div>
|
|
246
|
-
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>{t(language, "flow.email")}</div>
|
|
247
|
-
<Input value={email} onChange={(e) => setEmail(e.target.value)} type="email" placeholder="you@local" />
|
|
248
|
-
</div>
|
|
249
|
-
<div>
|
|
250
|
-
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>{t(language, "flow.password")}</div>
|
|
251
|
-
<Input value={password} onChange={(e) => setPassword(e.target.value)} type="password" placeholder={t(language, "flow.password.placeholder")} />
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
|
-
|
|
255
|
-
{error && <div style={{ marginTop: "0.85rem", padding: "0.6rem 0.85rem", background: "hsl(var(--destructive)/0.12)", border: "1px solid hsl(var(--destructive)/0.4)", borderRadius: 10, fontSize: "0.9rem" }}>{error}</div>}
|
|
256
|
-
|
|
257
|
-
<Button type="submit" disabled={busy || !email.trim() || !password.trim()} style={{ width: "100%", marginTop: "1rem" }}>
|
|
258
|
-
{busy ? t(language, "flow.login.busy") : t(language, "flow.login.submit")}
|
|
259
|
-
</Button>
|
|
260
|
-
<div style={{ fontSize: "0.75rem", color: "hsl(var(--fg-muted))", marginTop: "0.6rem" }}>
|
|
261
|
-
{t(language, "flow.login.note")}
|
|
262
|
-
</div>
|
|
263
|
-
</form>
|
|
264
|
-
</div>
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function ProductPromise() {
|
|
269
|
-
const language = useAppStore((state) => state.language);
|
|
270
|
-
return (
|
|
271
|
-
<div className="ritual-promise" aria-label="Lattice AI product promise">
|
|
272
|
-
<div>
|
|
273
|
-
<span>{t(language, "flow.promise.memory.k")}</span>
|
|
274
|
-
<strong>{t(language, "flow.promise.memory.v")}</strong>
|
|
275
|
-
</div>
|
|
276
|
-
<div>
|
|
277
|
-
<span>{t(language, "flow.promise.model.k")}</span>
|
|
278
|
-
<strong>{t(language, "flow.promise.model.v")}</strong>
|
|
279
|
-
</div>
|
|
280
|
-
<div>
|
|
281
|
-
<span>{t(language, "flow.promise.ownership.k")}</span>
|
|
282
|
-
<strong>{t(language, "flow.promise.ownership.v")}</strong>
|
|
283
|
-
</div>
|
|
284
|
-
</div>
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function AnalysisScreen({
|
|
289
|
-
analysis,
|
|
290
|
-
error,
|
|
291
|
-
onContinue,
|
|
292
|
-
}: {
|
|
293
|
-
analysis: FlowAnalysis | null;
|
|
294
|
-
error: string | null;
|
|
295
|
-
onContinue: () => void;
|
|
296
|
-
}) {
|
|
297
|
-
const language = useAppStore((state) => state.language);
|
|
298
|
-
const detected = buildDetectedFacts(analysis);
|
|
299
|
-
return (
|
|
300
|
-
<div>
|
|
301
|
-
<div className="ritual-title">{t(language, "flow.analysis.title")}</div>
|
|
302
|
-
<div className="ritual-subtitle">{t(language, "flow.analysis.body")}</div>
|
|
303
|
-
|
|
304
|
-
<div className="ritual-fact-grid">
|
|
305
|
-
{detected.map((item, idx) => (
|
|
306
|
-
<div key={idx} className="ritual-fact">
|
|
307
|
-
<div className="ritual-fact-label">{item.label}</div>
|
|
308
|
-
<div className="ritual-fact-value">{item.value}</div>
|
|
309
|
-
<div style={{ fontSize: "0.8rem", color: "hsl(var(--fg-muted))", marginTop: 3 }}>{item.detail}</div>
|
|
310
|
-
</div>
|
|
311
|
-
))}
|
|
312
|
-
</div>
|
|
313
|
-
|
|
314
|
-
<div className="ritual-card" style={{ marginTop: "1rem" }}>
|
|
315
|
-
<div style={{ display: "flex", alignItems: "center", gap: "0.6rem" }}>
|
|
316
|
-
<Sparkles style={{ color: "hsl(var(--brain-core))" }} />
|
|
317
|
-
<div>
|
|
318
|
-
<div style={{ fontWeight: 620 }}>{analysis ? recommendedSummary(analysis, language) : t(language, "flow.analysis.finding")}</div>
|
|
319
|
-
<div style={{ fontSize: "0.9rem", color: "hsl(var(--fg-muted))" }}>
|
|
320
|
-
{analysis ? t(language, "flow.analysis.ready") : t(language, "flow.analysis.wait")}
|
|
321
|
-
</div>
|
|
322
|
-
</div>
|
|
323
|
-
</div>
|
|
324
|
-
</div>
|
|
325
|
-
|
|
326
|
-
{error && <div className="ritual-card" style={{ borderColor: "hsl(var(--destructive)/0.4)", background: "hsl(var(--destructive)/0.06)" }}>{error}</div>}
|
|
327
|
-
|
|
328
|
-
<div style={{ marginTop: "1.25rem" }}>
|
|
329
|
-
<Button onClick={onContinue} disabled={!analysis && !error} style={{ minWidth: 260 }}>
|
|
330
|
-
{t(language, "flow.analysis.continue")}
|
|
331
|
-
</Button>
|
|
332
|
-
</div>
|
|
333
|
-
</div>
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function RecommendationScreen({
|
|
338
|
-
recommendations,
|
|
339
|
-
onBack,
|
|
340
|
-
onSkipModel,
|
|
341
|
-
onSelect,
|
|
342
|
-
}: {
|
|
343
|
-
recommendations: RecommendedModel[];
|
|
344
|
-
onBack: () => void;
|
|
345
|
-
onSkipModel: () => void;
|
|
346
|
-
onSelect: (model: RecommendedModel) => void;
|
|
347
|
-
}) {
|
|
348
|
-
const language = useAppStore((state) => state.language);
|
|
349
|
-
const items = recommendations.length ? recommendations : [fallbackModel()];
|
|
350
|
-
return (
|
|
351
|
-
<div>
|
|
352
|
-
<div className="ritual-title">{t(language, "flow.recommend.title")}</div>
|
|
353
|
-
<div className="ritual-subtitle">{t(language, "flow.recommend.body")}</div>
|
|
354
|
-
|
|
355
|
-
<div style={{ maxWidth: 560, margin: "0 auto" }}>
|
|
356
|
-
{items[0]?.supported ? (
|
|
357
|
-
<Button onClick={() => onSelect(items[0])} style={{ width: "100%", marginBottom: "0.85rem" }}>
|
|
358
|
-
{t(language, "flow.recommend.primary")}
|
|
359
|
-
</Button>
|
|
360
|
-
) : null}
|
|
361
|
-
{items.slice(0, 3).map((model, index) => (
|
|
362
|
-
<button
|
|
363
|
-
key={`${model.role}-${model.id}`}
|
|
364
|
-
className="ritual-model-card"
|
|
365
|
-
onClick={() => model.supported && onSelect(model)}
|
|
366
|
-
disabled={!model.supported}
|
|
367
|
-
style={{ width: "100%" }}
|
|
368
|
-
>
|
|
369
|
-
<div className="role">{rankLabel(model.role, index, language)}</div>
|
|
370
|
-
<div className="name">{model.shortName}</div>
|
|
371
|
-
<div className="reason">{model.reason} · {model.size || "ready"}</div>
|
|
372
|
-
{!model.supported && <div style={{ color: "hsl(var(--destructive))", marginTop: 6, fontSize: "0.85rem" }}>{t(language, "flow.recommend.unsupported")}</div>}
|
|
373
|
-
</button>
|
|
374
|
-
))}
|
|
375
|
-
</div>
|
|
376
|
-
|
|
377
|
-
<div style={{ marginTop: "1.1rem", display: "flex", justifyContent: "center", gap: "1rem", alignItems: "center" }}>
|
|
378
|
-
<Button variant="ghost" onClick={onBack}>{t(language, "flow.recommend.back")}</Button>
|
|
379
|
-
<Button variant="outline" onClick={onSkipModel}>{t(language, "flow.recommend.skip")}</Button>
|
|
380
|
-
<div style={{ fontSize: "0.82rem", color: "hsl(var(--fg-muted))" }}>{t(language, "flow.recommend.hint")}</div>
|
|
381
|
-
</div>
|
|
382
|
-
</div>
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function InstallScreen({
|
|
387
|
-
model,
|
|
388
|
-
onBack,
|
|
389
|
-
onComplete,
|
|
390
|
-
}: {
|
|
391
|
-
model: RecommendedModel;
|
|
392
|
-
onBack: () => void;
|
|
393
|
-
onComplete: () => void;
|
|
394
|
-
}) {
|
|
395
|
-
const language = useAppStore((state) => state.language);
|
|
396
|
-
const [busy, setBusy] = React.useState(false);
|
|
397
|
-
const [stage, setStage] = React.useState<InstallStage>("idle");
|
|
398
|
-
const [percent, setPercent] = React.useState(0);
|
|
399
|
-
const [message, setMessage] = React.useState(t(language, "flow.install.wait"));
|
|
400
|
-
const [error, setError] = React.useState<string | null>(null);
|
|
401
|
-
|
|
402
|
-
async function start() {
|
|
403
|
-
setBusy(true);
|
|
404
|
-
setError(null);
|
|
405
|
-
setStage("install");
|
|
406
|
-
setPercent(8);
|
|
407
|
-
setMessage(t(language, "flow.install.prepare"));
|
|
408
|
-
const result = await latticeApi.streamModelPrepare(
|
|
409
|
-
{ model: model.loadId, engine: model.engine || "local_mlx", allow_download: true },
|
|
410
|
-
{
|
|
411
|
-
onProgress: (event) => {
|
|
412
|
-
const nextStage = friendlyInstallStage(String(event.stage || ""));
|
|
413
|
-
setStage(nextStage);
|
|
414
|
-
setPercent(Number(event.percent || percentForStage(nextStage)));
|
|
415
|
-
setMessage(friendlyInstallMessage(event, nextStage, language));
|
|
416
|
-
},
|
|
417
|
-
onDone: () => {
|
|
418
|
-
setStage("done");
|
|
419
|
-
setPercent(100);
|
|
420
|
-
setMessage(t(language, "flow.install.done"));
|
|
421
|
-
},
|
|
422
|
-
onError: (event) => {
|
|
423
|
-
setStage("error");
|
|
424
|
-
setError(consumerError(event));
|
|
425
|
-
},
|
|
426
|
-
},
|
|
427
|
-
);
|
|
428
|
-
setBusy(false);
|
|
429
|
-
if (result.ok) {
|
|
430
|
-
setStage("done");
|
|
431
|
-
setPercent(100);
|
|
432
|
-
setMessage(t(language, "flow.install.done"));
|
|
433
|
-
window.setTimeout(onComplete, 700);
|
|
434
|
-
} else {
|
|
435
|
-
setStage("error");
|
|
436
|
-
setError(consumerError(result.data as ApiData));
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const brainStateForStage: any =
|
|
441
|
-
stage === "download" ? "thinking" :
|
|
442
|
-
stage === "validate" ? "recalling" :
|
|
443
|
-
stage === "load" ? "synthesizing" :
|
|
444
|
-
stage === "done" ? "idle" : "listening";
|
|
445
|
-
|
|
446
|
-
return (
|
|
447
|
-
<div>
|
|
448
|
-
<div className="ritual-title">{t(language, "flow.install.title")}</div>
|
|
449
|
-
<div className="ritual-subtitle">
|
|
450
|
-
<strong>{model.shortName}</strong> — {model.reason}.<br />
|
|
451
|
-
{t(language, "flow.install.body")}
|
|
452
|
-
</div>
|
|
453
|
-
|
|
454
|
-
{/* Living Brain reacts to the ceremony of installation */}
|
|
455
|
-
<div style={{ margin: "0.6rem auto 1rem" }}>
|
|
456
|
-
<LivingBrain
|
|
457
|
-
state={brainStateForStage}
|
|
458
|
-
intensity={stage === "download" || stage === "load" ? 0.96 : 0.82}
|
|
459
|
-
size="normal"
|
|
460
|
-
/>
|
|
461
|
-
</div>
|
|
462
|
-
|
|
463
|
-
<div className="ritual-progress">
|
|
464
|
-
<div className="ritual-stage-list">
|
|
465
|
-
{(["install", "download", "validate", "load"] as const).map((item) => (
|
|
466
|
-
<div key={item} className={`ritual-stage ${installStepState(stage, item)}`}>
|
|
467
|
-
<CheckCircle2 style={{ width: 15, height: 15 }} />
|
|
468
|
-
<span>{installLabel(item, language)}</span>
|
|
469
|
-
</div>
|
|
470
|
-
))}
|
|
471
|
-
</div>
|
|
472
|
-
|
|
473
|
-
<div className="ritual-bar">
|
|
474
|
-
<span style={{ width: `${Math.max(4, Math.min(100, percent))}%` }} />
|
|
475
|
-
</div>
|
|
476
|
-
</div>
|
|
477
|
-
|
|
478
|
-
<div className="ritual-status">{message}</div>
|
|
479
|
-
<div className="ritual-card" style={{ margin: "0.8rem auto 0", maxWidth: 540, fontSize: "0.86rem", color: "hsl(var(--fg-muted))" }}>
|
|
480
|
-
{t(language, "flow.install.note")}
|
|
481
|
-
</div>
|
|
482
|
-
|
|
483
|
-
{error && (
|
|
484
|
-
<div className="ritual-card" style={{ borderColor: "hsl(var(--destructive)/0.45)", background: "hsl(var(--destructive)/0.07)", marginBottom: "1rem" }}>
|
|
485
|
-
{error}
|
|
486
|
-
<div style={{ marginTop: "0.5rem", fontSize: "0.85rem" }}>{t(language, "flow.install.retry")}</div>
|
|
487
|
-
</div>
|
|
488
|
-
)}
|
|
489
|
-
|
|
490
|
-
<div style={{ display: "flex", gap: "0.75rem", justifyContent: "center", marginTop: "1rem" }}>
|
|
491
|
-
<Button variant="ghost" onClick={onBack} disabled={busy}>{t(language, "flow.install.back")}</Button>
|
|
492
|
-
|
|
493
|
-
{stage !== "done" ? (
|
|
494
|
-
<Button
|
|
495
|
-
onClick={start}
|
|
496
|
-
disabled={busy || !model.supported}
|
|
497
|
-
>
|
|
498
|
-
{busy ? t(language, "flow.install.busy") : t(language, "flow.install.start")}
|
|
499
|
-
</Button>
|
|
500
|
-
) : (
|
|
501
|
-
<Button onClick={onComplete}>{t(language, "flow.install.enter")}</Button>
|
|
502
|
-
)}
|
|
503
|
-
</div>
|
|
504
|
-
|
|
505
|
-
<div style={{ fontSize: "0.72rem", color: "hsl(var(--fg-muted))", marginTop: "0.9rem" }}>
|
|
506
|
-
{t(language, "flow.install.local")}
|
|
507
|
-
</div>
|
|
508
|
-
</div>
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function buildDetectedFacts(analysis: FlowAnalysis | null) {
|
|
513
|
-
if (!analysis) {
|
|
514
|
-
return [
|
|
515
|
-
{ label: "Computer", value: "Checking", detail: "Operating system and chip" },
|
|
516
|
-
{ label: "Memory", value: "Checking", detail: "Available room for local thinking" },
|
|
517
|
-
{ label: "Graphics", value: "Checking", detail: "Local acceleration support" },
|
|
518
|
-
{ label: "Local Support", value: "Checking", detail: "Installed model helpers" },
|
|
519
|
-
{ label: "Models", value: "Checking", detail: "Installed local Brains" },
|
|
520
|
-
];
|
|
521
|
-
}
|
|
522
|
-
const setupEnv = asRecord(analysis.setup?.environment);
|
|
523
|
-
const recProfile = asRecord(analysis.recommendations?.profile);
|
|
524
|
-
const recs = asRecord(analysis.recommendations?.recommendations);
|
|
525
|
-
const models = asRecord(analysis.models);
|
|
526
|
-
const sysinfo = asRecord(analysis.sysinfo);
|
|
527
|
-
const profile = { ...setupEnv, ...recProfile };
|
|
528
|
-
const ramGb = Number(recs.ram_gb || Number(profile.ram_mb || 0) / 1024 || 0);
|
|
529
|
-
const appleSilicon = Boolean(recs.apple_silicon || String(profile.arch || "").includes("arm"));
|
|
530
|
-
const loadedModels = asArray(models.loaded);
|
|
531
|
-
const gpu = asRecord(profile.gpu);
|
|
532
|
-
const installedRuntimes = [
|
|
533
|
-
...asArray(setupEnv.installed_runtimes),
|
|
534
|
-
...asArray(profile.installed_runtimes),
|
|
535
|
-
...asArray(recs.installed_runtimes),
|
|
536
|
-
];
|
|
537
|
-
return [
|
|
538
|
-
{
|
|
539
|
-
label: "Computer",
|
|
540
|
-
value: appleSilicon ? "Apple Silicon Mac" : friendlyOs(profile.os),
|
|
541
|
-
detail: "Ready for local Digital Brain use",
|
|
542
|
-
},
|
|
543
|
-
{
|
|
544
|
-
label: "Memory",
|
|
545
|
-
value: ramGb ? `${Math.round(ramGb)} GB` : "Detected",
|
|
546
|
-
detail: "Enough context for the recommended model",
|
|
547
|
-
},
|
|
548
|
-
{
|
|
549
|
-
label: "Graphics",
|
|
550
|
-
value: gpu.vendor || sysinfo.gpu_mem_gb ? "Local acceleration ready" : "Standard local mode",
|
|
551
|
-
detail: "Lattice will choose the best available path",
|
|
552
|
-
},
|
|
553
|
-
{
|
|
554
|
-
label: "Local Support",
|
|
555
|
-
value: installedRuntimes.length ? "Ready" : "Will be prepared",
|
|
556
|
-
detail: installedRuntimes.length ? "Installed model helpers detected" : "Lattice will add what is needed",
|
|
557
|
-
},
|
|
558
|
-
{
|
|
559
|
-
label: "Models",
|
|
560
|
-
value: loadedModels.length ? `${loadedModels.length} already installed` : "None installed yet",
|
|
561
|
-
detail: loadedModels.length ? "One can be loaded immediately" : "Lattice will guide the first install",
|
|
562
|
-
},
|
|
563
|
-
];
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function buildRecommendations(analysis: FlowAnalysis | null): RecommendedModel[] {
|
|
567
|
-
const models = asRecord(analysis?.models);
|
|
568
|
-
const modelRows = [
|
|
569
|
-
...asArray<ApiData>(models.recommended),
|
|
570
|
-
...asArray<ApiData>(models.catalog),
|
|
571
|
-
];
|
|
572
|
-
const recommendationRoot = asRecord(analysis?.recommendations?.recommendations);
|
|
573
|
-
const recRows = asArray<ApiData>(recommendationRoot.models);
|
|
574
|
-
const topPick = asRecord(recommendationRoot.top_pick);
|
|
575
|
-
const merged = new Map<string, ApiData>();
|
|
576
|
-
for (const row of [...recRows, ...modelRows]) {
|
|
577
|
-
const id = String(row.id || row.model_id || row.recommended_load_id || "");
|
|
578
|
-
if (!id) continue;
|
|
579
|
-
merged.set(id, { ...(merged.get(id) || {}), ...row });
|
|
580
|
-
}
|
|
581
|
-
if (topPick.id && !merged.has(String(topPick.id))) merged.set(String(topPick.id), topPick);
|
|
582
|
-
const all = Array.from(merged.values()).map(toRecommendedModel).filter((item) => item.id);
|
|
583
|
-
const supported = all.filter((item) => item.supported);
|
|
584
|
-
const pool = supported.length ? supported : all;
|
|
585
|
-
const byName = (pattern: RegExp) => pool.find((item) => pattern.test(`${item.name} ${item.id}`));
|
|
586
|
-
const byId = (id?: unknown) => pool.find((item) => item.id === String(id));
|
|
587
|
-
const best = byId(topPick.id) || byName(/gemma.*12|12b/i) || pool[0];
|
|
588
|
-
const faster = pool.find((item) => item.id !== best?.id && /qwen|8b|7b/i.test(`${item.name} ${item.id}`)) || pool.find((item) => item.id !== best?.id);
|
|
589
|
-
const advanced = pool.find((item) => item.id !== best?.id && item.id !== faster?.id && /26b|32b|70b|advanced/i.test(`${item.name} ${item.id}`))
|
|
590
|
-
|| pool.find((item) => item.id !== best?.id && item.id !== faster?.id);
|
|
591
|
-
return [
|
|
592
|
-
best ? { ...best, role: "best" as const, reason: "Best Experience" } : null,
|
|
593
|
-
faster ? { ...faster, role: "faster" as const, reason: "Faster" } : null,
|
|
594
|
-
advanced ? { ...advanced, role: "advanced" as const, reason: "Advanced" } : null,
|
|
595
|
-
].filter(Boolean) as RecommendedModel[];
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function toRecommendedModel(row: ApiData): RecommendedModel {
|
|
599
|
-
const compatibility = asRecord(row.runtime_compatibility);
|
|
600
|
-
const id = String(row.id || row.model_id || row.recommended_load_id || "");
|
|
601
|
-
const loadId = String(row.recommended_load_id || row.load_id || id);
|
|
602
|
-
const name = String(row.display_name || row.name || id || "Recommended Brain");
|
|
603
|
-
const supported = row.load_status !== "unsupported"
|
|
604
|
-
&& row.load_status !== "runtime_update_needed"
|
|
605
|
-
&& row.status !== "not_recommended"
|
|
606
|
-
&& compatibility.supported !== false;
|
|
607
|
-
return {
|
|
608
|
-
id,
|
|
609
|
-
loadId,
|
|
610
|
-
engine: String(row.recommended_engine || row.engine || "local_mlx"),
|
|
611
|
-
name,
|
|
612
|
-
shortName: friendlyModelName(name || id),
|
|
613
|
-
family: friendlyModelName(String(row.family || name || "Local Brain")),
|
|
614
|
-
size: String(row.size || ""),
|
|
615
|
-
role: "best",
|
|
616
|
-
reason: String(row.reason || "Recommended"),
|
|
617
|
-
supported,
|
|
618
|
-
downloadRequired: Boolean(row.download_required),
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function fallbackModel(): RecommendedModel {
|
|
623
|
-
return {
|
|
624
|
-
id: "mlx-community/Qwen3-VL-8B-Instruct-4bit",
|
|
625
|
-
loadId: "mlx-community/Qwen3-VL-8B-Instruct-4bit",
|
|
626
|
-
engine: "local_mlx",
|
|
627
|
-
name: "Qwen3-VL 8B",
|
|
628
|
-
shortName: "Qwen 3",
|
|
629
|
-
family: "Qwen 3",
|
|
630
|
-
size: "ready",
|
|
631
|
-
role: "best",
|
|
632
|
-
reason: "Best Experience",
|
|
633
|
-
supported: true,
|
|
634
|
-
downloadRequired: false,
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function rankLabel(role: RecommendedModel["role"], index: number, language: Language) {
|
|
639
|
-
if (role === "best") return language === "ko" ? "추천" : "Best Experience";
|
|
640
|
-
if (role === "faster") return language === "ko" ? "빠른 선택" : "Faster";
|
|
641
|
-
if (role === "advanced") return language === "ko" ? "고급 선택" : "Advanced";
|
|
642
|
-
return `Choice ${index + 1}`;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
function recommendedSummary(analysis: FlowAnalysis, language: Language) {
|
|
646
|
-
const recs = asRecord(analysis.recommendations?.recommendations);
|
|
647
|
-
const topPick = asRecord(recs.top_pick);
|
|
648
|
-
if (topPick.name || topPick.id) {
|
|
649
|
-
const model = friendlyModelName(String(topPick.name || topPick.id));
|
|
650
|
-
return language === "ko" ? `${model}이 이 컴퓨터에 가장 잘 맞습니다.` : `${model} looks like the best fit.`;
|
|
651
|
-
}
|
|
652
|
-
return language === "ko" ? "이 컴퓨터에는 개인 로컬 Brain을 추천합니다." : "A private local Brain is recommended for this computer.";
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function friendlyModelName(value: string) {
|
|
656
|
-
return String(value || "Recommended Brain")
|
|
657
|
-
.replace(/^mlx-community\//i, "")
|
|
658
|
-
.replace(/[-_]?Instruct/gi, "")
|
|
659
|
-
.replace(/[-_]?4bit/gi, "")
|
|
660
|
-
.replace(/Qwen3[-_ ]?VL/gi, "Qwen 3")
|
|
661
|
-
.replace(/Qwen3/gi, "Qwen 3")
|
|
662
|
-
.replace(/Gemma[-_ ]?4/gi, "Gemma 4")
|
|
663
|
-
.replace(/A4B/gi, "")
|
|
664
|
-
.replace(/[-_]+/g, " ")
|
|
665
|
-
.replace(/\s+/g, " ")
|
|
666
|
-
.trim();
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function friendlyOs(value: unknown) {
|
|
670
|
-
const text = String(value || "Computer");
|
|
671
|
-
if (/darwin|mac/i.test(text)) return "Mac";
|
|
672
|
-
if (/win/i.test(text)) return "Windows PC";
|
|
673
|
-
if (/linux/i.test(text)) return "Linux computer";
|
|
674
|
-
return "Computer";
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function friendlyInstallStage(stage: string): InstallStage {
|
|
678
|
-
if (/download|pull|weights/i.test(stage)) return "download";
|
|
679
|
-
if (/smoke|validate|verify|test/i.test(stage)) return "validate";
|
|
680
|
-
if (/load|ready/i.test(stage)) return "load";
|
|
681
|
-
if (/done|complete/i.test(stage)) return "done";
|
|
682
|
-
return "install";
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function percentForStage(stage: InstallStage) {
|
|
686
|
-
if (stage === "install") return 20;
|
|
687
|
-
if (stage === "download") return 55;
|
|
688
|
-
if (stage === "validate") return 82;
|
|
689
|
-
if (stage === "load") return 94;
|
|
690
|
-
if (stage === "done") return 100;
|
|
691
|
-
return 8;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function friendlyInstallMessage(event: ApiData, stage: InstallStage, language: Language) {
|
|
695
|
-
const fallback = {
|
|
696
|
-
install: t(language, "flow.install.prepare"),
|
|
697
|
-
download: language === "ko" ? "모델 파일을 받는 중입니다." : "Getting the model files.",
|
|
698
|
-
validate: language === "ko" ? "Brain이 응답할 수 있는지 확인 중입니다." : "Checking that the Brain can answer.",
|
|
699
|
-
load: language === "ko" ? "Brain을 불러오는 중입니다." : "Loading the Brain.",
|
|
700
|
-
done: t(language, "flow.install.done"),
|
|
701
|
-
idle: t(language, "flow.install.wait"),
|
|
702
|
-
error: language === "ko" ? "확인이 필요한 일이 있습니다." : "Something needs attention.",
|
|
703
|
-
}[stage];
|
|
704
|
-
return cleanConsumerText(String(event.user_message || event.message || fallback));
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function installLabel(stage: "install" | "download" | "validate" | "load", language: Language) {
|
|
708
|
-
if (language === "ko") {
|
|
709
|
-
if (stage === "install") return "준비";
|
|
710
|
-
if (stage === "download") return "다운로드";
|
|
711
|
-
if (stage === "validate") return "확인";
|
|
712
|
-
return "로드";
|
|
713
|
-
}
|
|
714
|
-
if (stage === "install") return "Install";
|
|
715
|
-
if (stage === "download") return "Download";
|
|
716
|
-
if (stage === "validate") return "Validate";
|
|
717
|
-
return "Load";
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
function installStepState(current: InstallStage, item: "install" | "download" | "validate" | "load") {
|
|
721
|
-
const order: InstallStage[] = ["idle", "install", "download", "validate", "load", "done"];
|
|
722
|
-
const currentIndex = order.indexOf(current);
|
|
723
|
-
const itemIndex = order.indexOf(item);
|
|
724
|
-
if (current === "error") return "is-error";
|
|
725
|
-
if (current === "done" || currentIndex > itemIndex) return "is-done";
|
|
726
|
-
if (current === item) return "is-active";
|
|
727
|
-
return "";
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function consumerError(data: ApiData | unknown) {
|
|
731
|
-
const record = asRecord(data);
|
|
732
|
-
const guidance = asArray<string>(record.recovery_guidance).map(cleanConsumerText).filter(Boolean);
|
|
733
|
-
const message = cleanConsumerText(String(record.user_message || record.reason || record.error || "The selected model could not be loaded."));
|
|
734
|
-
return [message, ...guidance.slice(0, 2)].join(" ");
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
function cleanConsumerText(value: string) {
|
|
738
|
-
return String(value || "")
|
|
739
|
-
.replace(/gemma4_unified/gi, "this model format")
|
|
740
|
-
.replace(/mlx[-_ ]?vlm|mlx[-_ ]?lm|local_mlx|\bmlx\b|\bgguf\b|\bollama\b|huggingface|hugging face/gi, "local model support")
|
|
741
|
-
.replace(/runtime/gi, "model support")
|
|
742
|
-
.replace(/No module named ['\"][^'\"]+['\"]/gi, "A local support component is missing")
|
|
743
|
-
.replace(/\s+/g, " ")
|
|
744
|
-
.trim();
|
|
102
|
+
function completeFlow(onComplete: () => void) {
|
|
103
|
+
try { localStorage.setItem(FLOW_COMPLETE_KEY, "true"); } catch {}
|
|
104
|
+
onComplete();
|
|
745
105
|
}
|
|
746
106
|
|
|
747
|
-
function
|
|
748
|
-
|
|
107
|
+
function brainStateForStep(step: FlowStep): BrainState {
|
|
108
|
+
if (step === "analysis") return "listening";
|
|
109
|
+
if (step === "recommend") return "recalling";
|
|
110
|
+
if (step === "install") return "thinking";
|
|
111
|
+
return "idle";
|
|
749
112
|
}
|