ltcai 4.7.2 → 5.1.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 +59 -45
- package/docs/CHANGELOG.md +100 -0
- package/docs/TRUST_MODEL.md +66 -0
- package/docs/WHY_LATTICE.md +54 -0
- package/frontend/src/App.tsx +105 -70
- package/frontend/src/components/ProductFlow.tsx +102 -69
- package/frontend/src/components/primitives.tsx +1 -1
- package/frontend/src/i18n.ts +247 -0
- package/frontend/src/pages/System.tsx +1 -1
- package/frontend/src/store/appStore.ts +18 -0
- package/frontend/src/styles.css +36 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/portability.py +11 -7
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chat.py +19 -11
- package/latticeai/api/models.py +6 -0
- package/latticeai/api/security_dashboard.py +3 -15
- package/latticeai/api/static_routes.py +16 -0
- package/latticeai/app_factory.py +114 -40
- package/latticeai/core/audit.py +3 -1
- package/latticeai/core/builtin_hooks.py +7 -9
- package/latticeai/core/logging_safety.py +5 -21
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/security.py +67 -9
- package/latticeai/core/workspace_os.py +1 -1
- package/package.json +2 -2
- package/scripts/clean_release_artifacts.mjs +16 -1
- package/scripts/com.pts.claudecode.discord.plist +31 -0
- package/scripts/pts-claudecode-discord-bridge.mjs +189 -0
- package/scripts/run_integration_tests.mjs +91 -0
- package/scripts/start-pts-claudecode-discord.sh +51 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +3 -2
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/index-DONOJfMn.js +16 -0
- package/static/app/assets/index-DONOJfMn.js.map +1 -0
- package/static/app/assets/{index-KlQ04wVv.css → index-DuYYT2oh.css} +1 -1
- package/static/app/index.html +2 -2
- package/static/app/assets/index-DdAB4yfa.js +0 -16
- package/static/app/assets/index-DdAB4yfa.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";
|
|
@@ -52,6 +54,7 @@ function readSavedFlowUser(): { email?: string; name?: string } | null {
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export function ProductFlow({ onComplete }: { onComplete: () => void }) {
|
|
57
|
+
const language = useAppStore((state) => state.language);
|
|
55
58
|
const [step, setStep] = React.useState<FlowStep>("login");
|
|
56
59
|
const [analysis, setAnalysis] = React.useState<FlowAnalysis | null>(null);
|
|
57
60
|
const [analysisError, setAnalysisError] = React.useState<string | null>(null);
|
|
@@ -78,16 +81,17 @@ export function ProductFlow({ onComplete }: { onComplete: () => void }) {
|
|
|
78
81
|
sysinfo: sysinfo.ok ? sysinfo.data as ApiData : null,
|
|
79
82
|
});
|
|
80
83
|
if (!setup.ok && !recommendationsResult.ok && !models.ok) {
|
|
81
|
-
setAnalysisError("
|
|
84
|
+
setAnalysisError(t(language, "flow.analysis.error"));
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
void runAnalysis();
|
|
85
88
|
return () => { cancelled = true; };
|
|
86
|
-
}, [analysis, step]);
|
|
89
|
+
}, [analysis, language, step]);
|
|
87
90
|
|
|
88
91
|
return (
|
|
89
|
-
<div className="ritual-shell" aria-label=
|
|
92
|
+
<div className="ritual-shell" aria-label={t(language, "flow.shell")}>
|
|
90
93
|
<div className="ritual-container">
|
|
94
|
+
<LanguageChooser />
|
|
91
95
|
{/* The living presence participates in the ritual at every step */}
|
|
92
96
|
<div className="ritual-brain">
|
|
93
97
|
<LivingBrain
|
|
@@ -141,7 +145,29 @@ export function ProductFlow({ onComplete }: { onComplete: () => void }) {
|
|
|
141
145
|
);
|
|
142
146
|
}
|
|
143
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
|
+
|
|
144
169
|
function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
|
|
170
|
+
const language = useAppStore((state) => state.language);
|
|
145
171
|
const [email, setEmail] = React.useState(() => {
|
|
146
172
|
return readSavedFlowUser()?.email || "you@local";
|
|
147
173
|
});
|
|
@@ -156,7 +182,7 @@ function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
|
|
|
156
182
|
const cleanPassword = password.trim();
|
|
157
183
|
const cleanName = name.trim() || cleanEmail.split("@")[0] || "You";
|
|
158
184
|
if (!cleanEmail || !cleanPassword) {
|
|
159
|
-
setError(
|
|
185
|
+
setError(t(language, "flow.login.missing"));
|
|
160
186
|
return;
|
|
161
187
|
}
|
|
162
188
|
setBusy(true);
|
|
@@ -173,12 +199,12 @@ function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
|
|
|
173
199
|
}
|
|
174
200
|
if (savedUser?.email && savedUser.email !== cleanEmail) {
|
|
175
201
|
setBusy(false);
|
|
176
|
-
setError(
|
|
202
|
+
setError(t(language, "flow.login.otherEmail"));
|
|
177
203
|
return;
|
|
178
204
|
}
|
|
179
205
|
if (savedUser?.email === cleanEmail) {
|
|
180
206
|
setBusy(false);
|
|
181
|
-
setError(
|
|
207
|
+
setError(t(language, "flow.login.wrongPassword"));
|
|
182
208
|
return;
|
|
183
209
|
}
|
|
184
210
|
const registered = await latticeApi.register({
|
|
@@ -191,7 +217,7 @@ function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
|
|
|
191
217
|
}
|
|
192
218
|
if (!result.ok) {
|
|
193
219
|
setBusy(false);
|
|
194
|
-
setError(
|
|
220
|
+
setError(t(language, "flow.login.unavailable"));
|
|
195
221
|
return;
|
|
196
222
|
}
|
|
197
223
|
try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
|
|
@@ -201,36 +227,34 @@ function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
|
|
|
201
227
|
|
|
202
228
|
return (
|
|
203
229
|
<div>
|
|
204
|
-
<div className="ritual-title"
|
|
205
|
-
<div className="ritual-subtitle">
|
|
206
|
-
모델은 바뀔 수 있지만, 내 문서와 대화, 결정, 기억은 사라지면 안 됩니다. Lattice는 이 지식을 내가 소유하는 개인 Brain으로 모읍니다.
|
|
207
|
-
</div>
|
|
230
|
+
<div className="ritual-title">{t(language, "flow.login.title")}</div>
|
|
231
|
+
<div className="ritual-subtitle">{t(language, "flow.login.body")}</div>
|
|
208
232
|
|
|
209
233
|
<ProductPromise />
|
|
210
234
|
|
|
211
235
|
<form onSubmit={submit} className="ritual-card" style={{ maxWidth: 420, margin: "0 auto" }}>
|
|
212
236
|
<div style={{ display: "grid", gap: "0.85rem" }}>
|
|
213
237
|
<div>
|
|
214
|
-
<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>
|
|
215
239
|
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="You" />
|
|
216
240
|
</div>
|
|
217
241
|
<div>
|
|
218
|
-
<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>
|
|
219
243
|
<Input value={email} onChange={(e) => setEmail(e.target.value)} type="email" placeholder="you@local" />
|
|
220
244
|
</div>
|
|
221
245
|
<div>
|
|
222
|
-
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}
|
|
223
|
-
<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")} />
|
|
224
248
|
</div>
|
|
225
249
|
</div>
|
|
226
250
|
|
|
227
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>}
|
|
228
252
|
|
|
229
253
|
<Button type="submit" disabled={busy || !email.trim() || !password.trim()} style={{ width: "100%", marginTop: "1rem" }}>
|
|
230
|
-
{busy ?
|
|
254
|
+
{busy ? t(language, "flow.login.busy") : t(language, "flow.login.submit")}
|
|
231
255
|
</Button>
|
|
232
256
|
<div style={{ fontSize: "0.75rem", color: "hsl(var(--fg-muted))", marginTop: "0.6rem" }}>
|
|
233
|
-
|
|
257
|
+
{t(language, "flow.login.note")}
|
|
234
258
|
</div>
|
|
235
259
|
</form>
|
|
236
260
|
</div>
|
|
@@ -238,19 +262,20 @@ function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
|
|
|
238
262
|
}
|
|
239
263
|
|
|
240
264
|
function ProductPromise() {
|
|
265
|
+
const language = useAppStore((state) => state.language);
|
|
241
266
|
return (
|
|
242
267
|
<div className="ritual-promise" aria-label="Lattice AI product promise">
|
|
243
268
|
<div>
|
|
244
|
-
<span
|
|
245
|
-
<strong
|
|
269
|
+
<span>{t(language, "flow.promise.memory.k")}</span>
|
|
270
|
+
<strong>{t(language, "flow.promise.memory.v")}</strong>
|
|
246
271
|
</div>
|
|
247
272
|
<div>
|
|
248
|
-
<span
|
|
249
|
-
<strong
|
|
273
|
+
<span>{t(language, "flow.promise.model.k")}</span>
|
|
274
|
+
<strong>{t(language, "flow.promise.model.v")}</strong>
|
|
250
275
|
</div>
|
|
251
276
|
<div>
|
|
252
|
-
<span
|
|
253
|
-
<strong
|
|
277
|
+
<span>{t(language, "flow.promise.ownership.k")}</span>
|
|
278
|
+
<strong>{t(language, "flow.promise.ownership.v")}</strong>
|
|
254
279
|
</div>
|
|
255
280
|
</div>
|
|
256
281
|
);
|
|
@@ -265,13 +290,12 @@ function AnalysisScreen({
|
|
|
265
290
|
error: string | null;
|
|
266
291
|
onContinue: () => void;
|
|
267
292
|
}) {
|
|
293
|
+
const language = useAppStore((state) => state.language);
|
|
268
294
|
const detected = buildDetectedFacts(analysis);
|
|
269
295
|
return (
|
|
270
296
|
<div>
|
|
271
|
-
<div className="ritual-title"
|
|
272
|
-
<div className="ritual-subtitle">
|
|
273
|
-
Brain이 클라우드에 의존하지 않고 이 컴퓨터에서 편하게 돌아갈 수 있는지 확인합니다.
|
|
274
|
-
</div>
|
|
297
|
+
<div className="ritual-title">{t(language, "flow.analysis.title")}</div>
|
|
298
|
+
<div className="ritual-subtitle">{t(language, "flow.analysis.body")}</div>
|
|
275
299
|
|
|
276
300
|
<div className="ritual-fact-grid">
|
|
277
301
|
{detected.map((item, idx) => (
|
|
@@ -287,9 +311,9 @@ function AnalysisScreen({
|
|
|
287
311
|
<div style={{ display: "flex", alignItems: "center", gap: "0.6rem" }}>
|
|
288
312
|
<Sparkles style={{ color: "hsl(var(--brain-core))" }} />
|
|
289
313
|
<div>
|
|
290
|
-
<div style={{ fontWeight: 620 }}>{analysis ? recommendedSummary(analysis) :
|
|
314
|
+
<div style={{ fontWeight: 620 }}>{analysis ? recommendedSummary(analysis, language) : t(language, "flow.analysis.finding")}</div>
|
|
291
315
|
<div style={{ fontSize: "0.9rem", color: "hsl(var(--fg-muted))" }}>
|
|
292
|
-
{analysis ?
|
|
316
|
+
{analysis ? t(language, "flow.analysis.ready") : t(language, "flow.analysis.wait")}
|
|
293
317
|
</div>
|
|
294
318
|
</div>
|
|
295
319
|
</div>
|
|
@@ -299,7 +323,7 @@ function AnalysisScreen({
|
|
|
299
323
|
|
|
300
324
|
<div style={{ marginTop: "1.25rem" }}>
|
|
301
325
|
<Button onClick={onContinue} disabled={!analysis && !error} style={{ minWidth: 260 }}>
|
|
302
|
-
|
|
326
|
+
{t(language, "flow.analysis.continue")}
|
|
303
327
|
</Button>
|
|
304
328
|
</div>
|
|
305
329
|
</div>
|
|
@@ -315,18 +339,17 @@ function RecommendationScreen({
|
|
|
315
339
|
onBack: () => void;
|
|
316
340
|
onSelect: (model: RecommendedModel) => void;
|
|
317
341
|
}) {
|
|
342
|
+
const language = useAppStore((state) => state.language);
|
|
318
343
|
const items = recommendations.length ? recommendations : [fallbackModel()];
|
|
319
344
|
return (
|
|
320
345
|
<div>
|
|
321
|
-
<div className="ritual-title"
|
|
322
|
-
<div className="ritual-subtitle">
|
|
323
|
-
모델은 Brain의 현재 목소리입니다. 나중에 바꿔도 기억과 지식은 그대로 남습니다.
|
|
324
|
-
</div>
|
|
346
|
+
<div className="ritual-title">{t(language, "flow.recommend.title")}</div>
|
|
347
|
+
<div className="ritual-subtitle">{t(language, "flow.recommend.body")}</div>
|
|
325
348
|
|
|
326
349
|
<div style={{ maxWidth: 560, margin: "0 auto" }}>
|
|
327
350
|
{items[0]?.supported ? (
|
|
328
351
|
<Button onClick={() => onSelect(items[0])} style={{ width: "100%", marginBottom: "0.85rem" }}>
|
|
329
|
-
|
|
352
|
+
{t(language, "flow.recommend.primary")}
|
|
330
353
|
</Button>
|
|
331
354
|
) : null}
|
|
332
355
|
{items.slice(0, 3).map((model, index) => (
|
|
@@ -337,17 +360,17 @@ function RecommendationScreen({
|
|
|
337
360
|
disabled={!model.supported}
|
|
338
361
|
style={{ width: "100%" }}
|
|
339
362
|
>
|
|
340
|
-
<div className="role">{rankLabel(model.role, index)}</div>
|
|
363
|
+
<div className="role">{rankLabel(model.role, index, language)}</div>
|
|
341
364
|
<div className="name">{model.shortName}</div>
|
|
342
365
|
<div className="reason">{model.reason} · {model.size || "ready"}</div>
|
|
343
|
-
{!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>}
|
|
344
367
|
</button>
|
|
345
368
|
))}
|
|
346
369
|
</div>
|
|
347
370
|
|
|
348
371
|
<div style={{ marginTop: "1.1rem", display: "flex", justifyContent: "center", gap: "1rem", alignItems: "center" }}>
|
|
349
|
-
<Button variant="ghost" onClick={onBack}
|
|
350
|
-
<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>
|
|
351
374
|
</div>
|
|
352
375
|
</div>
|
|
353
376
|
);
|
|
@@ -362,10 +385,11 @@ function InstallScreen({
|
|
|
362
385
|
onBack: () => void;
|
|
363
386
|
onComplete: () => void;
|
|
364
387
|
}) {
|
|
388
|
+
const language = useAppStore((state) => state.language);
|
|
365
389
|
const [busy, setBusy] = React.useState(false);
|
|
366
390
|
const [stage, setStage] = React.useState<InstallStage>("idle");
|
|
367
391
|
const [percent, setPercent] = React.useState(0);
|
|
368
|
-
const [message, setMessage] = React.useState(
|
|
392
|
+
const [message, setMessage] = React.useState(t(language, "flow.install.wait"));
|
|
369
393
|
const [error, setError] = React.useState<string | null>(null);
|
|
370
394
|
|
|
371
395
|
async function start() {
|
|
@@ -373,7 +397,7 @@ function InstallScreen({
|
|
|
373
397
|
setError(null);
|
|
374
398
|
setStage("install");
|
|
375
399
|
setPercent(8);
|
|
376
|
-
setMessage(
|
|
400
|
+
setMessage(t(language, "flow.install.prepare"));
|
|
377
401
|
const result = await latticeApi.streamModelPrepare(
|
|
378
402
|
{ model: model.loadId, engine: model.engine || "local_mlx", allow_download: true },
|
|
379
403
|
{
|
|
@@ -381,12 +405,12 @@ function InstallScreen({
|
|
|
381
405
|
const nextStage = friendlyInstallStage(String(event.stage || ""));
|
|
382
406
|
setStage(nextStage);
|
|
383
407
|
setPercent(Number(event.percent || percentForStage(nextStage)));
|
|
384
|
-
setMessage(friendlyInstallMessage(event, nextStage));
|
|
408
|
+
setMessage(friendlyInstallMessage(event, nextStage, language));
|
|
385
409
|
},
|
|
386
410
|
onDone: () => {
|
|
387
411
|
setStage("done");
|
|
388
412
|
setPercent(100);
|
|
389
|
-
setMessage(
|
|
413
|
+
setMessage(t(language, "flow.install.done"));
|
|
390
414
|
},
|
|
391
415
|
onError: (event) => {
|
|
392
416
|
setStage("error");
|
|
@@ -398,7 +422,7 @@ function InstallScreen({
|
|
|
398
422
|
if (result.ok) {
|
|
399
423
|
setStage("done");
|
|
400
424
|
setPercent(100);
|
|
401
|
-
setMessage(
|
|
425
|
+
setMessage(t(language, "flow.install.done"));
|
|
402
426
|
window.setTimeout(onComplete, 700);
|
|
403
427
|
} else {
|
|
404
428
|
setStage("error");
|
|
@@ -414,10 +438,10 @@ function InstallScreen({
|
|
|
414
438
|
|
|
415
439
|
return (
|
|
416
440
|
<div>
|
|
417
|
-
<div className="ritual-title"
|
|
441
|
+
<div className="ritual-title">{t(language, "flow.install.title")}</div>
|
|
418
442
|
<div className="ritual-subtitle">
|
|
419
443
|
<strong>{model.shortName}</strong> — {model.reason}.<br />
|
|
420
|
-
|
|
444
|
+
{t(language, "flow.install.body")}
|
|
421
445
|
</div>
|
|
422
446
|
|
|
423
447
|
{/* Living Brain reacts to the ceremony of installation */}
|
|
@@ -434,7 +458,7 @@ function InstallScreen({
|
|
|
434
458
|
{(["install", "download", "validate", "load"] as const).map((item) => (
|
|
435
459
|
<div key={item} className={`ritual-stage ${installStepState(stage, item)}`}>
|
|
436
460
|
<CheckCircle2 style={{ width: 15, height: 15 }} />
|
|
437
|
-
<span>{installLabel(item)}</span>
|
|
461
|
+
<span>{installLabel(item, language)}</span>
|
|
438
462
|
</div>
|
|
439
463
|
))}
|
|
440
464
|
</div>
|
|
@@ -446,33 +470,33 @@ function InstallScreen({
|
|
|
446
470
|
|
|
447
471
|
<div className="ritual-status">{message}</div>
|
|
448
472
|
<div className="ritual-card" style={{ margin: "0.8rem auto 0", maxWidth: 540, fontSize: "0.86rem", color: "hsl(var(--fg-muted))" }}>
|
|
449
|
-
|
|
473
|
+
{t(language, "flow.install.note")}
|
|
450
474
|
</div>
|
|
451
475
|
|
|
452
476
|
{error && (
|
|
453
477
|
<div className="ritual-card" style={{ borderColor: "hsl(var(--destructive)/0.45)", background: "hsl(var(--destructive)/0.07)", marginBottom: "1rem" }}>
|
|
454
478
|
{error}
|
|
455
|
-
<div style={{ marginTop: "0.5rem", fontSize: "0.85rem" }}
|
|
479
|
+
<div style={{ marginTop: "0.5rem", fontSize: "0.85rem" }}>{t(language, "flow.install.retry")}</div>
|
|
456
480
|
</div>
|
|
457
481
|
)}
|
|
458
482
|
|
|
459
483
|
<div style={{ display: "flex", gap: "0.75rem", justifyContent: "center", marginTop: "1rem" }}>
|
|
460
|
-
<Button variant="ghost" onClick={onBack} disabled={busy}
|
|
484
|
+
<Button variant="ghost" onClick={onBack} disabled={busy}>{t(language, "flow.install.back")}</Button>
|
|
461
485
|
|
|
462
486
|
{stage !== "done" ? (
|
|
463
487
|
<Button
|
|
464
488
|
onClick={start}
|
|
465
489
|
disabled={busy || !model.supported}
|
|
466
490
|
>
|
|
467
|
-
{busy ?
|
|
491
|
+
{busy ? t(language, "flow.install.busy") : t(language, "flow.install.start")}
|
|
468
492
|
</Button>
|
|
469
493
|
) : (
|
|
470
|
-
<Button onClick={onComplete}>
|
|
494
|
+
<Button onClick={onComplete}>{t(language, "flow.install.enter")}</Button>
|
|
471
495
|
)}
|
|
472
496
|
</div>
|
|
473
497
|
|
|
474
498
|
<div style={{ fontSize: "0.72rem", color: "hsl(var(--fg-muted))", marginTop: "0.9rem" }}>
|
|
475
|
-
|
|
499
|
+
{t(language, "flow.install.local")}
|
|
476
500
|
</div>
|
|
477
501
|
</div>
|
|
478
502
|
);
|
|
@@ -604,18 +628,21 @@ function fallbackModel(): RecommendedModel {
|
|
|
604
628
|
};
|
|
605
629
|
}
|
|
606
630
|
|
|
607
|
-
function rankLabel(role: RecommendedModel["role"], index: number) {
|
|
608
|
-
if (role === "best") return "Best Experience";
|
|
609
|
-
if (role === "faster") return "Faster";
|
|
610
|
-
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";
|
|
611
635
|
return `Choice ${index + 1}`;
|
|
612
636
|
}
|
|
613
637
|
|
|
614
|
-
function recommendedSummary(analysis: FlowAnalysis) {
|
|
638
|
+
function recommendedSummary(analysis: FlowAnalysis, language: Language) {
|
|
615
639
|
const recs = asRecord(analysis.recommendations?.recommendations);
|
|
616
640
|
const topPick = asRecord(recs.top_pick);
|
|
617
|
-
if (topPick.name || topPick.id)
|
|
618
|
-
|
|
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.";
|
|
619
646
|
}
|
|
620
647
|
|
|
621
648
|
function friendlyModelName(value: string) {
|
|
@@ -657,20 +684,26 @@ function percentForStage(stage: InstallStage) {
|
|
|
657
684
|
return 8;
|
|
658
685
|
}
|
|
659
686
|
|
|
660
|
-
function friendlyInstallMessage(event: ApiData, stage: InstallStage) {
|
|
687
|
+
function friendlyInstallMessage(event: ApiData, stage: InstallStage, language: Language) {
|
|
661
688
|
const fallback = {
|
|
662
|
-
install: "
|
|
663
|
-
download: "Getting the model files.",
|
|
664
|
-
validate: "Checking that the Brain can answer.",
|
|
665
|
-
load: "Loading the Brain.",
|
|
666
|
-
done: "
|
|
667
|
-
idle: "
|
|
668
|
-
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.",
|
|
669
696
|
}[stage];
|
|
670
697
|
return cleanConsumerText(String(event.user_message || event.message || fallback));
|
|
671
698
|
}
|
|
672
699
|
|
|
673
|
-
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
|
+
}
|
|
674
707
|
if (stage === "install") return "Install";
|
|
675
708
|
if (stage === "download") return "Download";
|
|
676
709
|
if (stage === "validate") return "Validate";
|
|
@@ -316,7 +316,7 @@ export function ModeGate({
|
|
|
316
316
|
<div className="text-lg font-semibold">{title}</div>
|
|
317
317
|
<p className="mt-1 max-w-2xl text-sm leading-6 text-muted-foreground">{detail}</p>
|
|
318
318
|
</div>
|
|
319
|
-
<Button onClick={() => setMode(target)}>{target === "admin" ? "Switch to Admin" : "Switch to Advanced"}</Button>
|
|
319
|
+
<Button onClick={() => setMode(target)}>{target === "admin" ? "Switch to Admin Console" : "Switch to Advanced"}</Button>
|
|
320
320
|
</CardContent>
|
|
321
321
|
</Card>
|
|
322
322
|
);
|