ltcai 4.7.0 → 5.0.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 +49 -38
- package/docs/CHANGELOG.md +119 -0
- package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +49 -0
- package/docs/V4_7_2_INTUITIVE_BRAIN_UX_REPORT.md +62 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +16 -14
- package/frontend/src/App.tsx +275 -64
- package/frontend/src/api/client.ts +9 -1
- package/frontend/src/components/ProductFlow.tsx +150 -85
- package/frontend/src/i18n.ts +245 -0
- package/frontend/src/pages/System.tsx +1 -1
- package/frontend/src/store/appStore.ts +18 -0
- package/frontend/src/styles.css +241 -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/admin.py +112 -3
- package/latticeai/api/chat.py +11 -3
- package/latticeai/app_factory.py +0 -2
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/package.json +2 -1
- 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-DFmuiJ6t.css → index-DuYYT2oh.css} +1 -1
- package/static/app/assets/index-FR1UZkCD.js +16 -0
- package/static/app/assets/index-FR1UZkCD.js.map +1 -0
- package/static/app/index.html +2 -2
- package/scripts/launch-pts-grok.sh +0 -56
- package/static/app/assets/index-DwX3rNfA.js +0 -16
- package/static/app/assets/index-DwX3rNfA.js.map +0 -1
|
@@ -6,6 +6,8 @@ import { Badge } from "@/components/ui/badge";
|
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
import { Input } from "@/components/ui/input";
|
|
8
8
|
import { cn, asArray } from "@/lib/utils";
|
|
9
|
+
import { LANGUAGE_LABELS, t, type Language } from "@/i18n";
|
|
10
|
+
import { useAppStore } from "@/store/appStore";
|
|
9
11
|
|
|
10
12
|
const FLOW_COMPLETE_KEY = "lattice.productFlow.complete";
|
|
11
13
|
const FLOW_USER_KEY = "lattice.productFlow.user";
|
|
@@ -43,7 +45,16 @@ export function readProductFlowComplete() {
|
|
|
43
45
|
return false;
|
|
44
46
|
}
|
|
45
47
|
|
|
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
|
+
|
|
46
56
|
export function ProductFlow({ onComplete }: { onComplete: () => void }) {
|
|
57
|
+
const language = useAppStore((state) => state.language);
|
|
47
58
|
const [step, setStep] = React.useState<FlowStep>("login");
|
|
48
59
|
const [analysis, setAnalysis] = React.useState<FlowAnalysis | null>(null);
|
|
49
60
|
const [analysisError, setAnalysisError] = React.useState<string | null>(null);
|
|
@@ -70,16 +81,17 @@ export function ProductFlow({ onComplete }: { onComplete: () => void }) {
|
|
|
70
81
|
sysinfo: sysinfo.ok ? sysinfo.data as ApiData : null,
|
|
71
82
|
});
|
|
72
83
|
if (!setup.ok && !recommendationsResult.ok && !models.ok) {
|
|
73
|
-
setAnalysisError("
|
|
84
|
+
setAnalysisError(t(language, "flow.analysis.error"));
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
87
|
void runAnalysis();
|
|
77
88
|
return () => { cancelled = true; };
|
|
78
|
-
}, [analysis, step]);
|
|
89
|
+
}, [analysis, language, step]);
|
|
79
90
|
|
|
80
91
|
return (
|
|
81
|
-
<div className="ritual-shell" aria-label=
|
|
92
|
+
<div className="ritual-shell" aria-label={t(language, "flow.shell")}>
|
|
82
93
|
<div className="ritual-container">
|
|
94
|
+
<LanguageChooser />
|
|
83
95
|
{/* The living presence participates in the ritual at every step */}
|
|
84
96
|
<div className="ritual-brain">
|
|
85
97
|
<LivingBrain
|
|
@@ -133,80 +145,116 @@ export function ProductFlow({ onComplete }: { onComplete: () => void }) {
|
|
|
133
145
|
);
|
|
134
146
|
}
|
|
135
147
|
|
|
148
|
+
function LanguageChooser() {
|
|
149
|
+
const language = useAppStore((state) => state.language);
|
|
150
|
+
const setLanguage = useAppStore((state) => state.setLanguage);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="language-switcher ritual-language" aria-label={t(language, "language.label")}>
|
|
154
|
+
{(["ko", "en"] as Language[]).map((item) => (
|
|
155
|
+
<button
|
|
156
|
+
key={item}
|
|
157
|
+
type="button"
|
|
158
|
+
className={language === item ? "is-active" : ""}
|
|
159
|
+
onClick={() => setLanguage(item)}
|
|
160
|
+
aria-pressed={language === item}
|
|
161
|
+
>
|
|
162
|
+
{LANGUAGE_LABELS[item]}
|
|
163
|
+
</button>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
136
169
|
function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
|
|
170
|
+
const language = useAppStore((state) => state.language);
|
|
137
171
|
const [email, setEmail] = React.useState(() => {
|
|
138
|
-
|
|
139
|
-
const saved = localStorage.getItem(FLOW_USER_KEY);
|
|
140
|
-
return saved ? JSON.parse(saved).email || "you@local" : "you@local";
|
|
141
|
-
} catch {
|
|
142
|
-
return "you@local";
|
|
143
|
-
}
|
|
172
|
+
return readSavedFlowUser()?.email || "you@local";
|
|
144
173
|
});
|
|
145
174
|
const [password, setPassword] = React.useState("");
|
|
146
|
-
const [name, setName] = React.useState("You");
|
|
175
|
+
const [name, setName] = React.useState(() => readSavedFlowUser()?.name || "You");
|
|
147
176
|
const [busy, setBusy] = React.useState(false);
|
|
148
177
|
const [error, setError] = React.useState<string | null>(null);
|
|
149
178
|
|
|
150
179
|
async function submit(event: React.FormEvent) {
|
|
151
180
|
event.preventDefault();
|
|
181
|
+
const cleanEmail = email.trim();
|
|
182
|
+
const cleanPassword = password.trim();
|
|
183
|
+
const cleanName = name.trim() || cleanEmail.split("@")[0] || "You";
|
|
184
|
+
if (!cleanEmail || !cleanPassword) {
|
|
185
|
+
setError(t(language, "flow.login.missing"));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
152
188
|
setBusy(true);
|
|
153
189
|
setError(null);
|
|
154
|
-
const
|
|
155
|
-
let result = await latticeApi.login(
|
|
156
|
-
if (!result.ok) {
|
|
157
|
-
const registered = await latticeApi.register({
|
|
158
|
-
email,
|
|
159
|
-
password: safePassword,
|
|
160
|
-
name: name || email.split("@")[0] || "You",
|
|
161
|
-
nickname: name || "You",
|
|
162
|
-
});
|
|
163
|
-
if (registered.ok) result = await latticeApi.login(email, safePassword);
|
|
164
|
-
}
|
|
190
|
+
const savedUser = readSavedFlowUser();
|
|
191
|
+
let result = await latticeApi.login(cleanEmail, cleanPassword);
|
|
165
192
|
if (!result.ok) {
|
|
166
193
|
const profile = await latticeApi.profile();
|
|
167
|
-
if (
|
|
194
|
+
if (profile.ok && (!savedUser?.email || savedUser.email === cleanEmail)) {
|
|
195
|
+
try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
|
|
196
|
+
setBusy(false);
|
|
197
|
+
onSuccess();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (savedUser?.email && savedUser.email !== cleanEmail) {
|
|
168
201
|
setBusy(false);
|
|
169
|
-
setError("
|
|
202
|
+
setError(t(language, "flow.login.otherEmail"));
|
|
170
203
|
return;
|
|
171
204
|
}
|
|
205
|
+
if (savedUser?.email === cleanEmail) {
|
|
206
|
+
setBusy(false);
|
|
207
|
+
setError(t(language, "flow.login.wrongPassword"));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const registered = await latticeApi.register({
|
|
211
|
+
email: cleanEmail,
|
|
212
|
+
password: cleanPassword,
|
|
213
|
+
name: cleanName,
|
|
214
|
+
nickname: cleanName,
|
|
215
|
+
});
|
|
216
|
+
if (registered.ok) result = await latticeApi.login(cleanEmail, cleanPassword);
|
|
217
|
+
}
|
|
218
|
+
if (!result.ok) {
|
|
219
|
+
setBusy(false);
|
|
220
|
+
setError(t(language, "flow.login.unavailable"));
|
|
221
|
+
return;
|
|
172
222
|
}
|
|
173
|
-
try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email, name })); } catch {}
|
|
223
|
+
try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
|
|
174
224
|
setBusy(false);
|
|
175
225
|
onSuccess();
|
|
176
226
|
}
|
|
177
227
|
|
|
178
228
|
return (
|
|
179
229
|
<div>
|
|
180
|
-
<div className="ritual-title">
|
|
181
|
-
<div className="ritual-subtitle">
|
|
182
|
-
Models will change. Your knowledge should not. Lattice keeps your documents, conversations, decisions, and context together as a private Brain you own.
|
|
183
|
-
</div>
|
|
230
|
+
<div className="ritual-title">{t(language, "flow.login.title")}</div>
|
|
231
|
+
<div className="ritual-subtitle">{t(language, "flow.login.body")}</div>
|
|
184
232
|
|
|
185
233
|
<ProductPromise />
|
|
186
234
|
|
|
187
235
|
<form onSubmit={submit} className="ritual-card" style={{ maxWidth: 420, margin: "0 auto" }}>
|
|
188
236
|
<div style={{ display: "grid", gap: "0.85rem" }}>
|
|
189
237
|
<div>
|
|
190
|
-
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>
|
|
238
|
+
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>{t(language, "flow.name")}</div>
|
|
191
239
|
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="You" />
|
|
192
240
|
</div>
|
|
193
241
|
<div>
|
|
194
|
-
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>
|
|
242
|
+
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>{t(language, "flow.email")}</div>
|
|
195
243
|
<Input value={email} onChange={(e) => setEmail(e.target.value)} type="email" placeholder="you@local" />
|
|
196
244
|
</div>
|
|
197
245
|
<div>
|
|
198
|
-
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>
|
|
199
|
-
<Input value={password} onChange={(e) => setPassword(e.target.value)} type="password" placeholder="
|
|
246
|
+
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>{t(language, "flow.password")}</div>
|
|
247
|
+
<Input value={password} onChange={(e) => setPassword(e.target.value)} type="password" placeholder={t(language, "flow.password.placeholder")} />
|
|
200
248
|
</div>
|
|
201
249
|
</div>
|
|
202
250
|
|
|
203
251
|
{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>}
|
|
204
252
|
|
|
205
|
-
<Button type="submit" disabled={busy || !email.trim()} style={{ width: "100%", marginTop: "1rem" }}>
|
|
206
|
-
{busy ?
|
|
253
|
+
<Button type="submit" disabled={busy || !email.trim() || !password.trim()} style={{ width: "100%", marginTop: "1rem" }}>
|
|
254
|
+
{busy ? t(language, "flow.login.busy") : t(language, "flow.login.submit")}
|
|
207
255
|
</Button>
|
|
208
256
|
<div style={{ fontSize: "0.75rem", color: "hsl(var(--fg-muted))", marginTop: "0.6rem" }}>
|
|
209
|
-
|
|
257
|
+
{t(language, "flow.login.note")}
|
|
210
258
|
</div>
|
|
211
259
|
</form>
|
|
212
260
|
</div>
|
|
@@ -214,19 +262,20 @@ function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
|
|
|
214
262
|
}
|
|
215
263
|
|
|
216
264
|
function ProductPromise() {
|
|
265
|
+
const language = useAppStore((state) => state.language);
|
|
217
266
|
return (
|
|
218
267
|
<div className="ritual-promise" aria-label="Lattice AI product promise">
|
|
219
268
|
<div>
|
|
220
|
-
<span>
|
|
221
|
-
<strong>
|
|
269
|
+
<span>{t(language, "flow.promise.memory.k")}</span>
|
|
270
|
+
<strong>{t(language, "flow.promise.memory.v")}</strong>
|
|
222
271
|
</div>
|
|
223
272
|
<div>
|
|
224
|
-
<span>
|
|
225
|
-
<strong>
|
|
273
|
+
<span>{t(language, "flow.promise.model.k")}</span>
|
|
274
|
+
<strong>{t(language, "flow.promise.model.v")}</strong>
|
|
226
275
|
</div>
|
|
227
276
|
<div>
|
|
228
|
-
<span>
|
|
229
|
-
<strong>
|
|
277
|
+
<span>{t(language, "flow.promise.ownership.k")}</span>
|
|
278
|
+
<strong>{t(language, "flow.promise.ownership.v")}</strong>
|
|
230
279
|
</div>
|
|
231
280
|
</div>
|
|
232
281
|
);
|
|
@@ -241,13 +290,12 @@ function AnalysisScreen({
|
|
|
241
290
|
error: string | null;
|
|
242
291
|
onContinue: () => void;
|
|
243
292
|
}) {
|
|
293
|
+
const language = useAppStore((state) => state.language);
|
|
244
294
|
const detected = buildDetectedFacts(analysis);
|
|
245
295
|
return (
|
|
246
296
|
<div>
|
|
247
|
-
<div className="ritual-title">
|
|
248
|
-
<div className="ritual-subtitle">
|
|
249
|
-
Lattice is checking what this computer can support so your Brain can run locally instead of turning your memories into a cloud dependency.
|
|
250
|
-
</div>
|
|
297
|
+
<div className="ritual-title">{t(language, "flow.analysis.title")}</div>
|
|
298
|
+
<div className="ritual-subtitle">{t(language, "flow.analysis.body")}</div>
|
|
251
299
|
|
|
252
300
|
<div className="ritual-fact-grid">
|
|
253
301
|
{detected.map((item, idx) => (
|
|
@@ -263,9 +311,9 @@ function AnalysisScreen({
|
|
|
263
311
|
<div style={{ display: "flex", alignItems: "center", gap: "0.6rem" }}>
|
|
264
312
|
<Sparkles style={{ color: "hsl(var(--brain-core))" }} />
|
|
265
313
|
<div>
|
|
266
|
-
<div style={{ fontWeight: 620 }}>{analysis ? recommendedSummary(analysis) :
|
|
314
|
+
<div style={{ fontWeight: 620 }}>{analysis ? recommendedSummary(analysis, language) : t(language, "flow.analysis.finding")}</div>
|
|
267
315
|
<div style={{ fontSize: "0.9rem", color: "hsl(var(--fg-muted))" }}>
|
|
268
|
-
{analysis ?
|
|
316
|
+
{analysis ? t(language, "flow.analysis.ready") : t(language, "flow.analysis.wait")}
|
|
269
317
|
</div>
|
|
270
318
|
</div>
|
|
271
319
|
</div>
|
|
@@ -275,7 +323,7 @@ function AnalysisScreen({
|
|
|
275
323
|
|
|
276
324
|
<div style={{ marginTop: "1.25rem" }}>
|
|
277
325
|
<Button onClick={onContinue} disabled={!analysis && !error} style={{ minWidth: 260 }}>
|
|
278
|
-
|
|
326
|
+
{t(language, "flow.analysis.continue")}
|
|
279
327
|
</Button>
|
|
280
328
|
</div>
|
|
281
329
|
</div>
|
|
@@ -291,15 +339,19 @@ function RecommendationScreen({
|
|
|
291
339
|
onBack: () => void;
|
|
292
340
|
onSelect: (model: RecommendedModel) => void;
|
|
293
341
|
}) {
|
|
342
|
+
const language = useAppStore((state) => state.language);
|
|
294
343
|
const items = recommendations.length ? recommendations : [fallbackModel()];
|
|
295
344
|
return (
|
|
296
345
|
<div>
|
|
297
|
-
<div className="ritual-title">
|
|
298
|
-
<div className="ritual-subtitle">
|
|
299
|
-
The model is the current voice of your Brain. You can replace it later; your knowledge stays.
|
|
300
|
-
</div>
|
|
346
|
+
<div className="ritual-title">{t(language, "flow.recommend.title")}</div>
|
|
347
|
+
<div className="ritual-subtitle">{t(language, "flow.recommend.body")}</div>
|
|
301
348
|
|
|
302
349
|
<div style={{ maxWidth: 560, margin: "0 auto" }}>
|
|
350
|
+
{items[0]?.supported ? (
|
|
351
|
+
<Button onClick={() => onSelect(items[0])} style={{ width: "100%", marginBottom: "0.85rem" }}>
|
|
352
|
+
{t(language, "flow.recommend.primary")}
|
|
353
|
+
</Button>
|
|
354
|
+
) : null}
|
|
303
355
|
{items.slice(0, 3).map((model, index) => (
|
|
304
356
|
<button
|
|
305
357
|
key={`${model.role}-${model.id}`}
|
|
@@ -308,17 +360,17 @@ function RecommendationScreen({
|
|
|
308
360
|
disabled={!model.supported}
|
|
309
361
|
style={{ width: "100%" }}
|
|
310
362
|
>
|
|
311
|
-
<div className="role">{rankLabel(model.role, index)}</div>
|
|
363
|
+
<div className="role">{rankLabel(model.role, index, language)}</div>
|
|
312
364
|
<div className="name">{model.shortName}</div>
|
|
313
365
|
<div className="reason">{model.reason} · {model.size || "ready"}</div>
|
|
314
|
-
{!model.supported && <div style={{ color: "hsl(var(--destructive))", marginTop: 6, fontSize: "0.85rem" }}>
|
|
366
|
+
{!model.supported && <div style={{ color: "hsl(var(--destructive))", marginTop: 6, fontSize: "0.85rem" }}>{t(language, "flow.recommend.unsupported")}</div>}
|
|
315
367
|
</button>
|
|
316
368
|
))}
|
|
317
369
|
</div>
|
|
318
370
|
|
|
319
371
|
<div style={{ marginTop: "1.1rem", display: "flex", justifyContent: "center", gap: "1rem", alignItems: "center" }}>
|
|
320
|
-
<Button variant="ghost" onClick={onBack}>
|
|
321
|
-
<div style={{ fontSize: "0.82rem", color: "hsl(var(--fg-muted))" }}>
|
|
372
|
+
<Button variant="ghost" onClick={onBack}>{t(language, "flow.recommend.back")}</Button>
|
|
373
|
+
<div style={{ fontSize: "0.82rem", color: "hsl(var(--fg-muted))" }}>{t(language, "flow.recommend.hint")}</div>
|
|
322
374
|
</div>
|
|
323
375
|
</div>
|
|
324
376
|
);
|
|
@@ -333,10 +385,11 @@ function InstallScreen({
|
|
|
333
385
|
onBack: () => void;
|
|
334
386
|
onComplete: () => void;
|
|
335
387
|
}) {
|
|
388
|
+
const language = useAppStore((state) => state.language);
|
|
336
389
|
const [busy, setBusy] = React.useState(false);
|
|
337
390
|
const [stage, setStage] = React.useState<InstallStage>("idle");
|
|
338
391
|
const [percent, setPercent] = React.useState(0);
|
|
339
|
-
const [message, setMessage] = React.useState("
|
|
392
|
+
const [message, setMessage] = React.useState(t(language, "flow.install.wait"));
|
|
340
393
|
const [error, setError] = React.useState<string | null>(null);
|
|
341
394
|
|
|
342
395
|
async function start() {
|
|
@@ -344,7 +397,7 @@ function InstallScreen({
|
|
|
344
397
|
setError(null);
|
|
345
398
|
setStage("install");
|
|
346
399
|
setPercent(8);
|
|
347
|
-
setMessage("
|
|
400
|
+
setMessage(t(language, "flow.install.prepare"));
|
|
348
401
|
const result = await latticeApi.streamModelPrepare(
|
|
349
402
|
{ model: model.loadId, engine: model.engine || "local_mlx", allow_download: true },
|
|
350
403
|
{
|
|
@@ -352,12 +405,12 @@ function InstallScreen({
|
|
|
352
405
|
const nextStage = friendlyInstallStage(String(event.stage || ""));
|
|
353
406
|
setStage(nextStage);
|
|
354
407
|
setPercent(Number(event.percent || percentForStage(nextStage)));
|
|
355
|
-
setMessage(friendlyInstallMessage(event, nextStage));
|
|
408
|
+
setMessage(friendlyInstallMessage(event, nextStage, language));
|
|
356
409
|
},
|
|
357
410
|
onDone: () => {
|
|
358
411
|
setStage("done");
|
|
359
412
|
setPercent(100);
|
|
360
|
-
setMessage("
|
|
413
|
+
setMessage(t(language, "flow.install.done"));
|
|
361
414
|
},
|
|
362
415
|
onError: (event) => {
|
|
363
416
|
setStage("error");
|
|
@@ -369,7 +422,7 @@ function InstallScreen({
|
|
|
369
422
|
if (result.ok) {
|
|
370
423
|
setStage("done");
|
|
371
424
|
setPercent(100);
|
|
372
|
-
setMessage("
|
|
425
|
+
setMessage(t(language, "flow.install.done"));
|
|
373
426
|
window.setTimeout(onComplete, 700);
|
|
374
427
|
} else {
|
|
375
428
|
setStage("error");
|
|
@@ -385,10 +438,10 @@ function InstallScreen({
|
|
|
385
438
|
|
|
386
439
|
return (
|
|
387
440
|
<div>
|
|
388
|
-
<div className="ritual-title">
|
|
441
|
+
<div className="ritual-title">{t(language, "flow.install.title")}</div>
|
|
389
442
|
<div className="ritual-subtitle">
|
|
390
443
|
<strong>{model.shortName}</strong> — {model.reason}.<br />
|
|
391
|
-
|
|
444
|
+
{t(language, "flow.install.body")}
|
|
392
445
|
</div>
|
|
393
446
|
|
|
394
447
|
{/* Living Brain reacts to the ceremony of installation */}
|
|
@@ -405,7 +458,7 @@ function InstallScreen({
|
|
|
405
458
|
{(["install", "download", "validate", "load"] as const).map((item) => (
|
|
406
459
|
<div key={item} className={`ritual-stage ${installStepState(stage, item)}`}>
|
|
407
460
|
<CheckCircle2 style={{ width: 15, height: 15 }} />
|
|
408
|
-
<span>{installLabel(item)}</span>
|
|
461
|
+
<span>{installLabel(item, language)}</span>
|
|
409
462
|
</div>
|
|
410
463
|
))}
|
|
411
464
|
</div>
|
|
@@ -416,31 +469,34 @@ function InstallScreen({
|
|
|
416
469
|
</div>
|
|
417
470
|
|
|
418
471
|
<div className="ritual-status">{message}</div>
|
|
472
|
+
<div className="ritual-card" style={{ margin: "0.8rem auto 0", maxWidth: 540, fontSize: "0.86rem", color: "hsl(var(--fg-muted))" }}>
|
|
473
|
+
{t(language, "flow.install.note")}
|
|
474
|
+
</div>
|
|
419
475
|
|
|
420
476
|
{error && (
|
|
421
477
|
<div className="ritual-card" style={{ borderColor: "hsl(var(--destructive)/0.45)", background: "hsl(var(--destructive)/0.07)", marginBottom: "1rem" }}>
|
|
422
478
|
{error}
|
|
423
|
-
<div style={{ marginTop: "0.5rem", fontSize: "0.85rem" }}>
|
|
479
|
+
<div style={{ marginTop: "0.5rem", fontSize: "0.85rem" }}>{t(language, "flow.install.retry")}</div>
|
|
424
480
|
</div>
|
|
425
481
|
)}
|
|
426
482
|
|
|
427
483
|
<div style={{ display: "flex", gap: "0.75rem", justifyContent: "center", marginTop: "1rem" }}>
|
|
428
|
-
<Button variant="ghost" onClick={onBack} disabled={busy}>
|
|
484
|
+
<Button variant="ghost" onClick={onBack} disabled={busy}>{t(language, "flow.install.back")}</Button>
|
|
429
485
|
|
|
430
486
|
{stage !== "done" ? (
|
|
431
487
|
<Button
|
|
432
488
|
onClick={start}
|
|
433
489
|
disabled={busy || !model.supported}
|
|
434
490
|
>
|
|
435
|
-
{busy ?
|
|
491
|
+
{busy ? t(language, "flow.install.busy") : t(language, "flow.install.start")}
|
|
436
492
|
</Button>
|
|
437
493
|
) : (
|
|
438
|
-
<Button onClick={onComplete}>
|
|
494
|
+
<Button onClick={onComplete}>{t(language, "flow.install.enter")}</Button>
|
|
439
495
|
)}
|
|
440
496
|
</div>
|
|
441
497
|
|
|
442
498
|
<div style={{ fontSize: "0.72rem", color: "hsl(var(--fg-muted))", marginTop: "0.9rem" }}>
|
|
443
|
-
|
|
499
|
+
{t(language, "flow.install.local")}
|
|
444
500
|
</div>
|
|
445
501
|
</div>
|
|
446
502
|
);
|
|
@@ -572,18 +628,21 @@ function fallbackModel(): RecommendedModel {
|
|
|
572
628
|
};
|
|
573
629
|
}
|
|
574
630
|
|
|
575
|
-
function rankLabel(role: RecommendedModel["role"], index: number) {
|
|
576
|
-
if (role === "best") return "Best Experience";
|
|
577
|
-
if (role === "faster") return "Faster";
|
|
578
|
-
if (role === "advanced") return "Advanced";
|
|
631
|
+
function rankLabel(role: RecommendedModel["role"], index: number, language: Language) {
|
|
632
|
+
if (role === "best") return language === "ko" ? "추천" : "Best Experience";
|
|
633
|
+
if (role === "faster") return language === "ko" ? "빠른 선택" : "Faster";
|
|
634
|
+
if (role === "advanced") return language === "ko" ? "고급 선택" : "Advanced";
|
|
579
635
|
return `Choice ${index + 1}`;
|
|
580
636
|
}
|
|
581
637
|
|
|
582
|
-
function recommendedSummary(analysis: FlowAnalysis) {
|
|
638
|
+
function recommendedSummary(analysis: FlowAnalysis, language: Language) {
|
|
583
639
|
const recs = asRecord(analysis.recommendations?.recommendations);
|
|
584
640
|
const topPick = asRecord(recs.top_pick);
|
|
585
|
-
if (topPick.name || topPick.id)
|
|
586
|
-
|
|
641
|
+
if (topPick.name || topPick.id) {
|
|
642
|
+
const model = friendlyModelName(String(topPick.name || topPick.id));
|
|
643
|
+
return language === "ko" ? `${model}이 이 컴퓨터에 가장 잘 맞습니다.` : `${model} looks like the best fit.`;
|
|
644
|
+
}
|
|
645
|
+
return language === "ko" ? "이 컴퓨터에는 개인 로컬 Brain을 추천합니다." : "A private local Brain is recommended for this computer.";
|
|
587
646
|
}
|
|
588
647
|
|
|
589
648
|
function friendlyModelName(value: string) {
|
|
@@ -625,20 +684,26 @@ function percentForStage(stage: InstallStage) {
|
|
|
625
684
|
return 8;
|
|
626
685
|
}
|
|
627
686
|
|
|
628
|
-
function friendlyInstallMessage(event: ApiData, stage: InstallStage) {
|
|
687
|
+
function friendlyInstallMessage(event: ApiData, stage: InstallStage, language: Language) {
|
|
629
688
|
const fallback = {
|
|
630
|
-
install: "
|
|
631
|
-
download: "Getting the model files.",
|
|
632
|
-
validate: "Checking that the Brain can answer.",
|
|
633
|
-
load: "Loading the Brain.",
|
|
634
|
-
done: "
|
|
635
|
-
idle: "
|
|
636
|
-
error: "Something needs attention.",
|
|
689
|
+
install: t(language, "flow.install.prepare"),
|
|
690
|
+
download: language === "ko" ? "모델 파일을 받는 중입니다." : "Getting the model files.",
|
|
691
|
+
validate: language === "ko" ? "Brain이 응답할 수 있는지 확인 중입니다." : "Checking that the Brain can answer.",
|
|
692
|
+
load: language === "ko" ? "Brain을 불러오는 중입니다." : "Loading the Brain.",
|
|
693
|
+
done: t(language, "flow.install.done"),
|
|
694
|
+
idle: t(language, "flow.install.wait"),
|
|
695
|
+
error: language === "ko" ? "확인이 필요한 일이 있습니다." : "Something needs attention.",
|
|
637
696
|
}[stage];
|
|
638
697
|
return cleanConsumerText(String(event.user_message || event.message || fallback));
|
|
639
698
|
}
|
|
640
699
|
|
|
641
|
-
function installLabel(stage: "install" | "download" | "validate" | "load") {
|
|
700
|
+
function installLabel(stage: "install" | "download" | "validate" | "load", language: Language) {
|
|
701
|
+
if (language === "ko") {
|
|
702
|
+
if (stage === "install") return "준비";
|
|
703
|
+
if (stage === "download") return "다운로드";
|
|
704
|
+
if (stage === "validate") return "확인";
|
|
705
|
+
return "로드";
|
|
706
|
+
}
|
|
642
707
|
if (stage === "install") return "Install";
|
|
643
708
|
if (stage === "download") return "Download";
|
|
644
709
|
if (stage === "validate") return "Validate";
|