ltcai 6.2.0 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +18 -15
  2. package/docs/CHANGELOG.md +42 -0
  3. package/frontend/src/components/onboarding/AnalysisScreen.tsx +127 -0
  4. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +29 -0
  5. package/frontend/src/components/onboarding/InstallScreen.tsx +208 -0
  6. package/frontend/src/components/onboarding/LanguageChooser.tsx +23 -0
  7. package/frontend/src/components/onboarding/LoginScreen.tsx +131 -0
  8. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +13 -688
  9. package/frontend/src/components/onboarding/RecommendationScreen.tsx +59 -0
  10. package/frontend/src/components/onboarding/recommendationModel.ts +132 -0
  11. package/frontend/src/features/admin/AdminConsole.tsx +1 -1
  12. package/frontend/src/features/brain/BrainCarePanel.tsx +254 -0
  13. package/frontend/src/features/brain/BrainComposer.tsx +66 -0
  14. package/frontend/src/features/brain/BrainConversation.tsx +131 -0
  15. package/frontend/src/features/brain/BrainGraphLayer.tsx +132 -0
  16. package/frontend/src/features/brain/BrainHome.tsx +28 -795
  17. package/frontend/src/features/brain/BrainMemoryLayer.tsx +38 -0
  18. package/frontend/src/features/brain/BrainOverviewPanel.tsx +74 -0
  19. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +43 -0
  20. package/frontend/src/features/brain/DepthEmergence.tsx +53 -0
  21. package/frontend/src/features/brain/types.ts +6 -6
  22. package/frontend/src/features/review/ReviewCard.tsx +3 -1
  23. package/frontend/src/features/review/ReviewInbox.tsx +11 -1
  24. package/frontend/src/i18n.ts +92 -0
  25. package/frontend/src/pages/Brain.tsx +63 -5
  26. package/frontend/src/pages/Capture.tsx +102 -13
  27. package/frontend/src/pages/Library.tsx +72 -6
  28. package/lattice_brain/__init__.py +1 -1
  29. package/lattice_brain/runtime/multi_agent.py +1 -1
  30. package/latticeai/__init__.py +1 -1
  31. package/latticeai/app_factory.py +31 -39
  32. package/latticeai/core/marketplace.py +1 -1
  33. package/latticeai/core/workspace_os.py +1 -1
  34. package/latticeai/runtime/chat_wiring.py +119 -0
  35. package/latticeai/runtime/model_wiring.py +40 -0
  36. package/latticeai/runtime/platform_runtime_wiring.py +2 -3
  37. package/latticeai/runtime/review_wiring.py +37 -0
  38. package/latticeai/runtime/tail_wiring.py +21 -0
  39. package/package.json +2 -2
  40. package/scripts/check_i18n_literals.mjs +52 -0
  41. package/src-tauri/Cargo.lock +1 -1
  42. package/src-tauri/Cargo.toml +1 -1
  43. package/src-tauri/tauri.conf.json +1 -1
  44. package/static/app/asset-manifest.json +5 -5
  45. package/static/app/assets/{index-D91Rz5--.js → index-D76dWuQk.js} +2 -2
  46. package/static/app/assets/index-D76dWuQk.js.map +1 -0
  47. package/static/app/assets/index-Div5vMlq.css +2 -0
  48. package/static/app/index.html +2 -2
  49. package/static/app/assets/index-B2-1Gm0q.css +0 -2
  50. package/static/app/assets/index-D91Rz5--.js.map +0 -1
@@ -1,688 +1,13 @@
1
- import * as React from "react";
2
- import { CheckCircle2, Cpu, Download, LockKeyhole, MonitorCog, Sparkles } from "lucide-react";
3
- import { latticeApi } from "@/api/client";
4
- import { LivingBrain } from "@/components/LivingBrain";
5
- import { Badge } from "@/components/ui/badge";
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";
10
- import { useAppStore } from "@/store/appStore";
11
-
12
- const FLOW_USER_KEY = "lattice.productFlow.user";
13
-
14
- export type FlowStep = "login" | "analysis" | "recommend" | "install";
15
- type ApiData = Record<string, unknown>;
16
-
17
- export type FlowAnalysis = {
18
- setup?: ApiData | null;
19
- models?: ApiData | null;
20
- recommendations?: ApiData | null;
21
- sysinfo?: ApiData | null;
22
- };
23
-
24
- export type RecommendedModel = {
25
- id: string;
26
- loadId: string;
27
- engine: string;
28
- name: string;
29
- shortName: string;
30
- family: string;
31
- size: string;
32
- role: "best" | "faster" | "advanced";
33
- reason: string;
34
- supported: boolean;
35
- downloadRequired: boolean;
36
- downloadSize: string;
37
- storageLocation: string;
38
- externalHost: string;
39
- };
40
-
41
- type InstallStage = "idle" | "install" | "download" | "validate" | "load" | "done" | "error";
42
-
43
- function readSavedFlowUser(): { email?: string; name?: string } | null {
44
- try {
45
- const raw = localStorage.getItem(FLOW_USER_KEY);
46
- return raw ? JSON.parse(raw) : null;
47
- } catch {}
48
- return null;
49
- }
50
-
51
- export function LanguageChooser() {
52
- const language = useAppStore((state) => state.language);
53
- const setLanguage = useAppStore((state) => state.setLanguage);
54
-
55
- return (
56
- <div className="language-switcher ritual-language" aria-label={t(language, "language.label")}>
57
- {(["ko", "en"] as Language[]).map((item) => (
58
- <button
59
- key={item}
60
- type="button"
61
- className={language === item ? "is-active" : ""}
62
- onClick={() => setLanguage(item)}
63
- aria-pressed={language === item}
64
- >
65
- {LANGUAGE_LABELS[item]}
66
- </button>
67
- ))}
68
- </div>
69
- );
70
- }
71
-
72
- export function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
73
- const language = useAppStore((state) => state.language);
74
- const [email, setEmail] = React.useState(() => {
75
- return readSavedFlowUser()?.email || "you@local";
76
- });
77
- const [password, setPassword] = React.useState("");
78
- const [name, setName] = React.useState(() => readSavedFlowUser()?.name || "You");
79
- const [busy, setBusy] = React.useState(false);
80
- const [error, setError] = React.useState<string | null>(null);
81
-
82
- async function submit(event: React.FormEvent) {
83
- event.preventDefault();
84
- const cleanEmail = email.trim();
85
- const cleanPassword = password.trim();
86
- const cleanName = name.trim() || cleanEmail.split("@")[0] || "You";
87
- if (!cleanEmail || !cleanPassword) {
88
- setError(t(language, "flow.login.missing"));
89
- return;
90
- }
91
- setBusy(true);
92
- setError(null);
93
- const savedUser = readSavedFlowUser();
94
- let result = await latticeApi.login(cleanEmail, cleanPassword);
95
- if (!result.ok) {
96
- const profile = await latticeApi.profile();
97
- if (profile.ok && (!savedUser?.email || savedUser.email === cleanEmail)) {
98
- try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
99
- setBusy(false);
100
- onSuccess();
101
- return;
102
- }
103
- if (savedUser?.email && savedUser.email !== cleanEmail) {
104
- setBusy(false);
105
- setError(t(language, "flow.login.otherEmail"));
106
- return;
107
- }
108
- if (savedUser?.email === cleanEmail) {
109
- setBusy(false);
110
- setError(t(language, "flow.login.wrongPassword"));
111
- return;
112
- }
113
- const registered = await latticeApi.register({
114
- email: cleanEmail,
115
- password: cleanPassword,
116
- name: cleanName,
117
- nickname: cleanName,
118
- });
119
- if (registered.ok) result = await latticeApi.login(cleanEmail, cleanPassword);
120
- }
121
- if (!result.ok) {
122
- setBusy(false);
123
- setError(t(language, "flow.login.unavailable"));
124
- return;
125
- }
126
- try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
127
- setBusy(false);
128
- onSuccess();
129
- }
130
-
131
- return (
132
- <div>
133
- <div className="ritual-title">{t(language, "flow.login.title")}</div>
134
- <div className="ritual-subtitle">{t(language, "flow.login.body")}</div>
135
-
136
- <ProductPromise />
137
-
138
- <form onSubmit={submit} className="ritual-card ritual-form">
139
- <div className="ritual-field-stack">
140
- <div>
141
- <div className="ritual-field-label">{t(language, "flow.name")}</div>
142
- <Input value={name} onChange={(e) => setName(e.target.value)} placeholder="You" />
143
- </div>
144
- <div>
145
- <div className="ritual-field-label">{t(language, "flow.email")}</div>
146
- <Input value={email} onChange={(e) => setEmail(e.target.value)} type="email" placeholder="you@local" />
147
- </div>
148
- <div>
149
- <div className="ritual-field-label">{t(language, "flow.password")}</div>
150
- <Input value={password} onChange={(e) => setPassword(e.target.value)} type="password" placeholder={t(language, "flow.password.placeholder")} />
151
- </div>
152
- </div>
153
-
154
- {error && <div className="ritual-error" role="alert">{error}</div>}
155
-
156
- <Button type="submit" disabled={busy || !email.trim() || !password.trim()} className="ritual-full-button">
157
- {busy ? t(language, "flow.login.busy") : t(language, "flow.login.submit")}
158
- </Button>
159
- <div className="ritual-note">
160
- {t(language, "flow.login.note")}
161
- </div>
162
- </form>
163
- </div>
164
- );
165
- }
166
-
167
- function ProductPromise() {
168
- const language = useAppStore((state) => state.language);
169
- return (
170
- <div className="ritual-promise" aria-label="Lattice AI product promise">
171
- <div>
172
- <span>{t(language, "flow.promise.memory.k")}</span>
173
- <strong>{t(language, "flow.promise.memory.v")}</strong>
174
- </div>
175
- <div>
176
- <span>{t(language, "flow.promise.model.k")}</span>
177
- <strong>{t(language, "flow.promise.model.v")}</strong>
178
- </div>
179
- <div>
180
- <span>{t(language, "flow.promise.ownership.k")}</span>
181
- <strong>{t(language, "flow.promise.ownership.v")}</strong>
182
- </div>
183
- </div>
184
- );
185
- }
186
-
187
- export function AnalysisScreen({
188
- analysis,
189
- error,
190
- onContinue,
191
- }: {
192
- analysis: FlowAnalysis | null;
193
- error: string | null;
194
- onContinue: () => void;
195
- }) {
196
- const language = useAppStore((state) => state.language);
197
- const detected = buildDetectedFacts(analysis, language);
198
- return (
199
- <div>
200
- <div className="ritual-title">{t(language, "flow.analysis.title")}</div>
201
- <div className="ritual-subtitle">{t(language, "flow.analysis.body")}</div>
202
-
203
- <div className="ritual-fact-grid">
204
- {detected.map((item, idx) => (
205
- <div key={idx} className="ritual-fact">
206
- <div className="ritual-fact-label">{item.label}</div>
207
- <div className="ritual-fact-value">{item.value}</div>
208
- <div className="ritual-fact-detail">{item.detail}</div>
209
- </div>
210
- ))}
211
- </div>
212
-
213
- <div className="ritual-card ritual-analysis-card">
214
- <div className="ritual-inline-row">
215
- <Sparkles className="ritual-core-icon" />
216
- <div>
217
- <div className="ritual-strong-text">{analysis ? recommendedSummary(analysis, language) : t(language, "flow.analysis.finding")}</div>
218
- <div className="ritual-muted-text">
219
- {analysis ? t(language, "flow.analysis.ready") : t(language, "flow.analysis.wait")}
220
- </div>
221
- </div>
222
- </div>
223
- </div>
224
-
225
- {error && <div className="ritual-card ritual-error-card" role="alert">{error}</div>}
226
-
227
- <div className="ritual-centered-actions">
228
- <Button onClick={onContinue} disabled={!analysis && !error} className="ritual-wide-button">
229
- {t(language, "flow.analysis.continue")}
230
- </Button>
231
- </div>
232
- </div>
233
- );
234
- }
235
-
236
- export function RecommendationScreen({
237
- recommendations,
238
- onBack,
239
- onSkipModel,
240
- onSelect,
241
- }: {
242
- recommendations: RecommendedModel[];
243
- onBack: () => void;
244
- onSkipModel: () => void;
245
- onSelect: (model: RecommendedModel) => void;
246
- }) {
247
- const language = useAppStore((state) => state.language);
248
- const items = recommendations.length ? recommendations : [fallbackModel()];
249
- return (
250
- <div>
251
- <div className="ritual-title">{t(language, "flow.recommend.title")}</div>
252
- <div className="ritual-subtitle">{t(language, "flow.recommend.body")}</div>
253
-
254
- <div className="ritual-model-list">
255
- {items[0]?.supported ? (
256
- <Button onClick={() => onSelect(items[0])} className="ritual-primary-model-button">
257
- {t(language, "flow.recommend.primary")}
258
- </Button>
259
- ) : null}
260
- {items.slice(0, 3).map((model, index) => (
261
- <button
262
- key={`${model.role}-${model.id}`}
263
- className="ritual-model-card"
264
- onClick={() => model.supported && onSelect(model)}
265
- disabled={!model.supported}
266
- >
267
- <div className="role">{rankLabel(model.role, index, language)}</div>
268
- <div className="name">{model.shortName}</div>
269
- <div className="reason">{model.reason} · {model.size || t(language, "flow.recommend.sizeReady")}</div>
270
- {!model.supported && <div className="ritual-model-warning">{t(language, "flow.recommend.unsupported")}</div>}
271
- </button>
272
- ))}
273
- </div>
274
-
275
- <div className="ritual-action-row">
276
- <Button variant="ghost" onClick={onBack}>{t(language, "flow.recommend.back")}</Button>
277
- <Button variant="outline" onClick={onSkipModel}>{t(language, "flow.recommend.skip")}</Button>
278
- <div className="ritual-muted-hint">{t(language, "flow.recommend.hint")}</div>
279
- </div>
280
- </div>
281
- );
282
- }
283
-
284
- export function InstallScreen({
285
- model,
286
- onBack,
287
- onComplete,
288
- onLater,
289
- }: {
290
- model: RecommendedModel;
291
- onBack: () => void;
292
- onComplete: () => void;
293
- onLater: () => void;
294
- }) {
295
- const language = useAppStore((state) => state.language);
296
- const [busy, setBusy] = React.useState(false);
297
- const [stage, setStage] = React.useState<InstallStage>("idle");
298
- const [percent, setPercent] = React.useState(0);
299
- const [message, setMessage] = React.useState(t(language, "flow.install.wait"));
300
- const [error, setError] = React.useState<string | null>(null);
301
-
302
- async function start() {
303
- setBusy(true);
304
- setError(null);
305
- setStage("install");
306
- setPercent(8);
307
- setMessage(t(language, "flow.install.prepare"));
308
- const result = await latticeApi.streamModelPrepare(
309
- { model: model.loadId, engine: model.engine || "local_mlx", allow_download: true },
310
- {
311
- onProgress: (event) => {
312
- const nextStage = friendlyInstallStage(String(event.stage || ""));
313
- setStage(nextStage);
314
- setPercent(Number(event.percent || percentForStage(nextStage)));
315
- setMessage(friendlyInstallMessage(event, nextStage, language));
316
- },
317
- onDone: () => {
318
- setStage("done");
319
- setPercent(100);
320
- setMessage(t(language, "flow.install.done"));
321
- },
322
- onError: (event) => {
323
- setStage("error");
324
- setError(consumerError(event));
325
- },
326
- },
327
- );
328
- setBusy(false);
329
- if (result.ok) {
330
- setStage("done");
331
- setPercent(100);
332
- setMessage(t(language, "flow.install.done"));
333
- window.setTimeout(onComplete, 700);
334
- } else {
335
- setStage("error");
336
- setError(consumerError(result.data as ApiData));
337
- }
338
- }
339
-
340
- const brainStateForStage: any =
341
- stage === "download" ? "thinking" :
342
- stage === "validate" ? "recalling" :
343
- stage === "load" ? "synthesizing" :
344
- stage === "done" ? "idle" : "listening";
345
-
346
- return (
347
- <div>
348
- <div className="ritual-title">{t(language, "flow.install.title")}</div>
349
- <div className="ritual-subtitle">
350
- <strong>{model.shortName}</strong> — {model.reason}.<br />
351
- {t(language, "flow.install.body")}
352
- </div>
353
-
354
- {/* Living Brain reacts to the ceremony of installation */}
355
- <div className="ritual-install-brain">
356
- <LivingBrain
357
- state={brainStateForStage}
358
- intensity={stage === "download" || stage === "load" ? 0.96 : 0.82}
359
- size="normal"
360
- />
361
- </div>
362
-
363
- <DownloadConsentPanel model={model} />
364
-
365
- <div className="ritual-progress">
366
- <div className="ritual-stage-list">
367
- {(["install", "download", "validate", "load"] as const).map((item) => (
368
- <div key={item} className={`ritual-stage ${installStepState(stage, item)}`}>
369
- <CheckCircle2 className="ritual-stage-icon" />
370
- <span>{installLabel(item, language)}</span>
371
- </div>
372
- ))}
373
- </div>
374
-
375
- <div className="ritual-bar">
376
- <span className={`ritual-bar-fill ${progressClass(percent)}`} />
377
- </div>
378
- </div>
379
-
380
- <div className="ritual-status">{message}</div>
381
- <div className="ritual-card ritual-status-card">
382
- {t(language, "flow.install.note")}
383
- </div>
384
-
385
- {error && (
386
- <div className="ritual-card ritual-error-card ritual-install-error" role="alert">
387
- {error}
388
- <div className="ritual-error-detail">{t(language, "flow.install.retry")}</div>
389
- </div>
390
- )}
391
-
392
- <div className="ritual-button-row">
393
- <Button variant="ghost" onClick={onBack} disabled={busy}>{t(language, "flow.install.back")}</Button>
394
- <Button variant="outline" onClick={onLater} disabled={busy}>{t(language, "flow.install.later")}</Button>
395
-
396
- {stage !== "done" ? (
397
- <Button
398
- onClick={start}
399
- disabled={busy || !model.supported}
400
- >
401
- {busy ? t(language, "flow.install.busy") : t(language, "flow.install.start")}
402
- </Button>
403
- ) : (
404
- <Button onClick={onComplete}>{t(language, "flow.install.enter")}</Button>
405
- )}
406
- </div>
407
-
408
- <div className="ritual-local-note">
409
- {t(language, "flow.install.local")}
410
- </div>
411
- </div>
412
- );
413
- }
414
-
415
- function DownloadConsentPanel({ model }: { model: RecommendedModel }) {
416
- const language = useAppStore((state) => state.language);
417
- const items = [
418
- { label: t(language, "flow.consent.size"), value: model.downloadSize || t(language, "flow.consent.sizeUnknown") },
419
- { label: t(language, "flow.consent.location"), value: model.storageLocation },
420
- { label: t(language, "flow.consent.external"), value: model.externalHost || t(language, "flow.consent.externalNone") },
421
- ];
422
-
423
- return (
424
- <section className="ritual-consent-panel" aria-label={t(language, "flow.consent.title")}>
425
- <div>
426
- <strong>{t(language, "flow.consent.title")}</strong>
427
- <p>{model.downloadRequired ? t(language, "flow.consent.body") : t(language, "flow.consent.ready")}</p>
428
- </div>
429
- <dl>
430
- {items.map((item) => (
431
- <div key={item.label}>
432
- <dt>{item.label}</dt>
433
- <dd>{item.value}</dd>
434
- </div>
435
- ))}
436
- </dl>
437
- </section>
438
- );
439
- }
440
-
441
- function buildDetectedFacts(analysis: FlowAnalysis | null, language: Language) {
442
- if (!analysis) {
443
- return [
444
- { label: t(language, "flow.analysis.fact.computer"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.computerDetail") },
445
- { label: t(language, "flow.analysis.fact.memory"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.memoryDetail") },
446
- { label: t(language, "flow.analysis.fact.graphics"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.graphicsDetail") },
447
- { label: t(language, "flow.analysis.fact.support"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.supportDetail") },
448
- { label: t(language, "flow.analysis.fact.models"), value: t(language, "flow.analysis.checking"), detail: t(language, "flow.analysis.fact.modelsDetail") },
449
- ];
450
- }
451
- const setupEnv = asRecord(analysis.setup?.environment);
452
- const recProfile = asRecord(analysis.recommendations?.profile);
453
- const recs = asRecord(analysis.recommendations?.recommendations);
454
- const models = asRecord(analysis.models);
455
- const sysinfo = asRecord(analysis.sysinfo);
456
- const profile = { ...setupEnv, ...recProfile };
457
- const ramGb = Number(recs.ram_gb || Number(profile.ram_mb || 0) / 1024 || 0);
458
- const appleSilicon = Boolean(recs.apple_silicon || String(profile.arch || "").includes("arm"));
459
- const loadedModels = asArray(models.loaded);
460
- const gpu = asRecord(profile.gpu);
461
- const installedRuntimes = [
462
- ...asArray(setupEnv.installed_runtimes),
463
- ...asArray(profile.installed_runtimes),
464
- ...asArray(recs.installed_runtimes),
465
- ];
466
- return [
467
- {
468
- label: t(language, "flow.analysis.fact.computer"),
469
- value: appleSilicon ? t(language, "flow.analysis.apple") : friendlyOs(profile.os, language),
470
- detail: t(language, "flow.analysis.readyDetail"),
471
- },
472
- {
473
- label: t(language, "flow.analysis.fact.memory"),
474
- value: ramGb ? `${Math.round(ramGb)} GB` : t(language, "flow.analysis.detected"),
475
- detail: t(language, "flow.analysis.memoryReadyDetail"),
476
- },
477
- {
478
- label: t(language, "flow.analysis.fact.graphics"),
479
- value: gpu.vendor || sysinfo.gpu_mem_gb ? t(language, "flow.analysis.localReady") : t(language, "flow.analysis.standardLocal"),
480
- detail: t(language, "flow.analysis.graphicsReadyDetail"),
481
- },
482
- {
483
- label: t(language, "flow.analysis.fact.support"),
484
- value: installedRuntimes.length ? t(language, "flow.analysis.supportReady") : t(language, "flow.analysis.supportInstall"),
485
- detail: installedRuntimes.length ? t(language, "flow.analysis.supportReadyDetail") : t(language, "flow.analysis.supportInstallDetail"),
486
- },
487
- {
488
- label: t(language, "flow.analysis.fact.models"),
489
- value: loadedModels.length ? t(language, "flow.analysis.modelsInstalled", { count: loadedModels.length }) : t(language, "flow.analysis.noModels"),
490
- detail: loadedModels.length ? t(language, "flow.analysis.modelsReadyDetail") : t(language, "flow.analysis.modelsInstallDetail"),
491
- },
492
- ];
493
- }
494
-
495
- export function buildRecommendations(analysis: FlowAnalysis | null): RecommendedModel[] {
496
- const models = asRecord(analysis?.models);
497
- const modelRows = [
498
- ...asArray<ApiData>(models.recommended),
499
- ...asArray<ApiData>(models.catalog),
500
- ];
501
- const recommendationRoot = asRecord(analysis?.recommendations?.recommendations);
502
- const recRows = asArray<ApiData>(recommendationRoot.models);
503
- const topPick = asRecord(recommendationRoot.top_pick);
504
- const merged = new Map<string, ApiData>();
505
- for (const row of [...recRows, ...modelRows]) {
506
- const id = String(row.id || row.model_id || row.recommended_load_id || "");
507
- if (!id) continue;
508
- merged.set(id, { ...(merged.get(id) || {}), ...row });
509
- }
510
- if (topPick.id && !merged.has(String(topPick.id))) merged.set(String(topPick.id), topPick);
511
- const all = Array.from(merged.values()).map(toRecommendedModel).filter((item) => item.id);
512
- const supported = all.filter((item) => item.supported);
513
- const pool = supported.length ? supported : all;
514
- const byName = (pattern: RegExp) => pool.find((item) => pattern.test(`${item.name} ${item.id}`));
515
- const byId = (id?: unknown) => pool.find((item) => item.id === String(id));
516
- const best = byId(topPick.id) || byName(/gemma.*12|12b/i) || pool[0];
517
- 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);
518
- const advanced = pool.find((item) => item.id !== best?.id && item.id !== faster?.id && /26b|32b|70b|advanced/i.test(`${item.name} ${item.id}`))
519
- || pool.find((item) => item.id !== best?.id && item.id !== faster?.id);
520
- return [
521
- best ? { ...best, role: "best" as const, reason: best.reason || "Best Experience" } : null,
522
- faster ? { ...faster, role: "faster" as const, reason: faster.reason || "Faster" } : null,
523
- advanced ? { ...advanced, role: "advanced" as const, reason: advanced.reason || "Advanced" } : null,
524
- ].filter(Boolean) as RecommendedModel[];
525
- }
526
-
527
- function toRecommendedModel(row: ApiData): RecommendedModel {
528
- const compatibility = asRecord(row.runtime_compatibility);
529
- const id = String(row.id || row.model_id || row.recommended_load_id || "");
530
- const loadId = String(row.recommended_load_id || row.load_id || id);
531
- const name = String(row.display_name || row.name || id || "Recommended Brain");
532
- const supported = row.load_status !== "unsupported"
533
- && row.load_status !== "runtime_update_needed"
534
- && row.status !== "not_recommended"
535
- && compatibility.supported !== false;
536
- return {
537
- id,
538
- loadId,
539
- engine: String(row.recommended_engine || row.engine || "local_mlx"),
540
- name,
541
- shortName: friendlyModelName(name || id),
542
- family: friendlyModelName(String(row.family || name || "Local Brain")),
543
- size: String(row.size || ""),
544
- role: "best",
545
- reason: String(row.reason || ""),
546
- supported,
547
- downloadRequired: Boolean(row.download_required),
548
- downloadSize: String(row.download_size || row.size || ""),
549
- storageLocation: String(row.storage_location || row.local_path || "~/.latticeai/models"),
550
- externalHost: externalHostLabel(row),
551
- };
552
- }
553
-
554
- export function fallbackModel(): RecommendedModel {
555
- return {
556
- id: "mlx-community/Qwen3-VL-8B-Instruct-4bit",
557
- loadId: "mlx-community/Qwen3-VL-8B-Instruct-4bit",
558
- engine: "local_mlx",
559
- name: "Qwen3-VL 8B",
560
- shortName: "Qwen 3",
561
- family: "Qwen 3",
562
- size: "",
563
- role: "best",
564
- reason: "Best Experience",
565
- supported: true,
566
- downloadRequired: false,
567
- downloadSize: "",
568
- storageLocation: "~/.latticeai/models",
569
- externalHost: "",
570
- };
571
- }
572
-
573
- function externalHostLabel(row: ApiData) {
574
- const raw = String(row.source_url || row.download_url || row.repository || row.provider || row.id || "");
575
- if (!raw) return "";
576
- if (/huggingface|hf\\.co|mlx-community/i.test(raw)) return "Hugging Face / model repository";
577
- if (/ollama/i.test(raw)) return "Ollama registry";
578
- return raw.replace(/^https?:\/\//, "").split("/")[0] || raw;
579
- }
580
-
581
- function rankLabel(role: RecommendedModel["role"], index: number, language: Language) {
582
- if (role === "best") return language === "ko" ? "추천" : "Best Experience";
583
- if (role === "faster") return language === "ko" ? "빠른 선택" : "Faster";
584
- if (role === "advanced") return language === "ko" ? "고급 선택" : "Advanced";
585
- return `Choice ${index + 1}`;
586
- }
587
-
588
- function recommendedSummary(analysis: FlowAnalysis, language: Language) {
589
- const recs = asRecord(analysis.recommendations?.recommendations);
590
- const topPick = asRecord(recs.top_pick);
591
- if (topPick.name || topPick.id) {
592
- const model = friendlyModelName(String(topPick.name || topPick.id));
593
- return language === "ko" ? `${model}이 이 컴퓨터에 가장 잘 맞습니다.` : `${model} looks like the best fit.`;
594
- }
595
- return language === "ko" ? "이 컴퓨터에는 개인 로컬 Brain을 추천합니다." : "A private local Brain is recommended for this computer.";
596
- }
597
-
598
- function friendlyModelName(value: string) {
599
- return String(value || "Recommended Brain")
600
- .replace(/^mlx-community\//i, "")
601
- .replace(/[-_]?Instruct/gi, "")
602
- .replace(/[-_]?4bit/gi, "")
603
- .replace(/Qwen3[-_ ]?VL/gi, "Qwen 3")
604
- .replace(/Qwen3/gi, "Qwen 3")
605
- .replace(/Gemma[-_ ]?4/gi, "Gemma 4")
606
- .replace(/A4B/gi, "")
607
- .replace(/[-_]+/g, " ")
608
- .replace(/\s+/g, " ")
609
- .trim();
610
- }
611
-
612
- function friendlyOs(value: unknown, language: Language) {
613
- const text = String(value || "Computer");
614
- if (/darwin|mac/i.test(text)) return "Mac";
615
- if (/win/i.test(text)) return "Windows PC";
616
- if (/linux/i.test(text)) return "Linux computer";
617
- return t(language, "flow.analysis.fact.computer");
618
- }
619
-
620
- function friendlyInstallStage(stage: string): InstallStage {
621
- if (/download|pull|weights/i.test(stage)) return "download";
622
- if (/smoke|validate|verify|test/i.test(stage)) return "validate";
623
- if (/load|ready/i.test(stage)) return "load";
624
- if (/done|complete/i.test(stage)) return "done";
625
- return "install";
626
- }
627
-
628
- function percentForStage(stage: InstallStage) {
629
- if (stage === "install") return 20;
630
- if (stage === "download") return 55;
631
- if (stage === "validate") return 82;
632
- if (stage === "load") return 94;
633
- if (stage === "done") return 100;
634
- return 8;
635
- }
636
-
637
- function friendlyInstallMessage(event: ApiData, stage: InstallStage, language: Language) {
638
- const fallback = {
639
- install: t(language, "flow.install.prepare"),
640
- download: t(language, "flow.install.stage.download"),
641
- validate: t(language, "flow.install.stage.validate"),
642
- load: t(language, "flow.install.stage.load"),
643
- done: t(language, "flow.install.done"),
644
- idle: t(language, "flow.install.wait"),
645
- error: t(language, "flow.install.stage.error"),
646
- }[stage];
647
- return cleanConsumerText(String(event.user_message || event.message || fallback));
648
- }
649
-
650
- function installLabel(stage: "install" | "download" | "validate" | "load", language: Language) {
651
- return t(language, `flow.install.step.${stage}`);
652
- }
653
-
654
- function progressClass(percent: number) {
655
- const step = Math.max(0, Math.min(100, Math.round(percent / 10) * 10));
656
- return `progress-${step}`;
657
- }
658
-
659
- function installStepState(current: InstallStage, item: "install" | "download" | "validate" | "load") {
660
- const order: InstallStage[] = ["idle", "install", "download", "validate", "load", "done"];
661
- const currentIndex = order.indexOf(current);
662
- const itemIndex = order.indexOf(item);
663
- if (current === "error") return "is-error";
664
- if (current === "done" || currentIndex > itemIndex) return "is-done";
665
- if (current === item) return "is-active";
666
- return "";
667
- }
668
-
669
- function consumerError(data: ApiData | unknown) {
670
- const record = asRecord(data);
671
- const guidance = asArray<string>(record.recovery_guidance).map(cleanConsumerText).filter(Boolean);
672
- const message = cleanConsumerText(String(record.user_message || record.reason || record.error || "The selected model could not be loaded."));
673
- return [message, ...guidance.slice(0, 2)].join(" ");
674
- }
675
-
676
- function cleanConsumerText(value: string) {
677
- return String(value || "")
678
- .replace(/gemma4_unified/gi, "this model format")
679
- .replace(/mlx[-_ ]?vlm|mlx[-_ ]?lm|local_mlx|\bmlx\b|\bgguf\b|\bollama\b|huggingface|hugging face/gi, "local model support")
680
- .replace(/runtime/gi, "model support")
681
- .replace(/No module named ['\"][^'\"]+['\"]/gi, "A local support component is missing")
682
- .replace(/\s+/g, " ")
683
- .trim();
684
- }
685
-
686
- function asRecord(value: unknown): ApiData {
687
- return value && typeof value === "object" && !Array.isArray(value) ? value as ApiData : {};
688
- }
1
+ export { AnalysisScreen } from "./AnalysisScreen";
2
+ export { DownloadConsentPanel } from "./DownloadConsentPanel";
3
+ export { InstallScreen } from "./InstallScreen";
4
+ export { LanguageChooser } from "./LanguageChooser";
5
+ export { LoginScreen } from "./LoginScreen";
6
+ export { RecommendationScreen } from "./RecommendationScreen";
7
+ export {
8
+ buildRecommendations,
9
+ fallbackModel,
10
+ type FlowAnalysis,
11
+ type FlowStep,
12
+ type RecommendedModel,
13
+ } from "./recommendationModel";