ltcai 7.9.0 → 8.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.
Files changed (107) hide show
  1. package/README.md +35 -49
  2. package/docs/CHANGELOG.md +35 -0
  3. package/docs/V3_FRONTEND.md +11 -5
  4. package/lattice_brain/__init__.py +1 -1
  5. package/lattice_brain/embeddings.py +9 -1
  6. package/lattice_brain/graph/projection.py +19 -5
  7. package/lattice_brain/graph/provenance.py +26 -4
  8. package/lattice_brain/graph/write_master.py +30 -3
  9. package/lattice_brain/runtime/multi_agent.py +1 -1
  10. package/latticeai/__init__.py +1 -1
  11. package/latticeai/app_factory.py +13 -134
  12. package/latticeai/core/local_embeddings.py +8 -0
  13. package/latticeai/core/marketplace.py +1 -1
  14. package/latticeai/core/mcp_registry.py +140 -1
  15. package/latticeai/core/tool_registry.py +7 -0
  16. package/latticeai/core/workspace_os.py +1 -1
  17. package/latticeai/services/architecture_readiness.py +50 -6
  18. package/latticeai/services/model_runtime.py +36 -2
  19. package/latticeai/services/product_readiness.py +19 -17
  20. package/package.json +2 -2
  21. package/scripts/build_frontend_assets.mjs +11 -2
  22. package/src-tauri/Cargo.lock +1 -1
  23. package/src-tauri/Cargo.toml +1 -1
  24. package/src-tauri/tauri.conf.json +1 -1
  25. package/static/app/asset-manifest.json +11 -102
  26. package/static/app/assets/Act-BOO66G-c.js +1 -0
  27. package/static/app/assets/Brain-C6lEYiD7.js +321 -0
  28. package/static/app/assets/{Capture-B9Tlhzqr.js → Capture-TATXBRDw.js} +1 -2
  29. package/static/app/assets/Library-DK4FIp8a.js +1 -0
  30. package/static/app/assets/System-Bgs6Ql7x.js +1 -0
  31. package/static/app/assets/core-CwxXejkd.js +1 -2
  32. package/static/app/assets/index-Bvv79nre.js +16 -0
  33. package/static/app/assets/index-Dslqglia.css +2 -0
  34. package/static/app/assets/primitives-B70WOra0.js +1 -0
  35. package/static/app/assets/{textarea-BZk6ybp5.js → textarea-Czrd9gwM.js} +1 -2
  36. package/static/app/index.html +3 -10
  37. package/static/app/theme-boot.js +8 -0
  38. package/static/sw.js +1 -1
  39. package/frontend/index.html +0 -24
  40. package/frontend/openapi.json +0 -15425
  41. package/frontend/src/App.tsx +0 -243
  42. package/frontend/src/api/client.ts +0 -580
  43. package/frontend/src/api/openapi.ts +0 -17892
  44. package/frontend/src/components/AdminAccessGate.tsx +0 -70
  45. package/frontend/src/components/FeedbackState.tsx +0 -45
  46. package/frontend/src/components/LanguageSwitcher.tsx +0 -23
  47. package/frontend/src/components/LivingBrain.tsx +0 -220
  48. package/frontend/src/components/ProductFlow.tsx +0 -171
  49. package/frontend/src/components/WorkspaceProfileSwitcher.tsx +0 -176
  50. package/frontend/src/components/onboarding/AnalysisScreen.tsx +0 -135
  51. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +0 -29
  52. package/frontend/src/components/onboarding/InstallScreen.tsx +0 -263
  53. package/frontend/src/components/onboarding/LanguageChooser.tsx +0 -23
  54. package/frontend/src/components/onboarding/LoginScreen.tsx +0 -131
  55. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +0 -13
  56. package/frontend/src/components/onboarding/RecommendationScreen.tsx +0 -116
  57. package/frontend/src/components/onboarding/recommendationModel.ts +0 -189
  58. package/frontend/src/components/primitives.tsx +0 -392
  59. package/frontend/src/components/ui/badge.tsx +0 -27
  60. package/frontend/src/components/ui/button.tsx +0 -37
  61. package/frontend/src/components/ui/card.tsx +0 -22
  62. package/frontend/src/components/ui/input.tsx +0 -16
  63. package/frontend/src/components/ui/textarea.tsx +0 -16
  64. package/frontend/src/features/admin/AdminConsole.tsx +0 -334
  65. package/frontend/src/features/brain/BrainCarePanel.tsx +0 -254
  66. package/frontend/src/features/brain/BrainComposer.tsx +0 -85
  67. package/frontend/src/features/brain/BrainConversation.tsx +0 -688
  68. package/frontend/src/features/brain/BrainGraphLayer.tsx +0 -365
  69. package/frontend/src/features/brain/BrainHome.tsx +0 -624
  70. package/frontend/src/features/brain/BrainMemoryLayer.tsx +0 -45
  71. package/frontend/src/features/brain/BrainOverviewPanel.tsx +0 -149
  72. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +0 -43
  73. package/frontend/src/features/brain/DepthEmergence.tsx +0 -53
  74. package/frontend/src/features/brain/brainData.ts +0 -246
  75. package/frontend/src/features/brain/graphLayout.ts +0 -37
  76. package/frontend/src/features/brain/types.ts +0 -150
  77. package/frontend/src/features/review/ReviewCard.tsx +0 -100
  78. package/frontend/src/features/review/ReviewInbox.tsx +0 -127
  79. package/frontend/src/features/review/reviewHelpers.ts +0 -69
  80. package/frontend/src/i18n.ts +0 -1303
  81. package/frontend/src/lib/utils.ts +0 -33
  82. package/frontend/src/main.tsx +0 -23
  83. package/frontend/src/pages/Act.tsx +0 -458
  84. package/frontend/src/pages/Ask.tsx +0 -14
  85. package/frontend/src/pages/Brain.tsx +0 -914
  86. package/frontend/src/pages/Capture.tsx +0 -258
  87. package/frontend/src/pages/Library.tsx +0 -486
  88. package/frontend/src/pages/System.tsx +0 -621
  89. package/frontend/src/routes.ts +0 -92
  90. package/frontend/src/store/appStore.ts +0 -94
  91. package/frontend/src/styles.css +0 -6579
  92. package/static/app/assets/Act-DOvf59ru.js +0 -2
  93. package/static/app/assets/Act-DOvf59ru.js.map +0 -1
  94. package/static/app/assets/Brain-C7_0mEiI.js +0 -322
  95. package/static/app/assets/Brain-C7_0mEiI.js.map +0 -1
  96. package/static/app/assets/Capture-B9Tlhzqr.js.map +0 -1
  97. package/static/app/assets/Library-BJPEEm5O.js +0 -2
  98. package/static/app/assets/Library-BJPEEm5O.js.map +0 -1
  99. package/static/app/assets/System-D6t9jo9V.js +0 -2
  100. package/static/app/assets/System-D6t9jo9V.js.map +0 -1
  101. package/static/app/assets/core-CwxXejkd.js.map +0 -1
  102. package/static/app/assets/index-C7g26IF6.css +0 -2
  103. package/static/app/assets/index-DbcEYJQ2.js +0 -17
  104. package/static/app/assets/index-DbcEYJQ2.js.map +0 -1
  105. package/static/app/assets/primitives-CD38lt4n.js +0 -2
  106. package/static/app/assets/primitives-CD38lt4n.js.map +0 -1
  107. package/static/app/assets/textarea-BZk6ybp5.js.map +0 -1
@@ -1,131 +0,0 @@
1
- import * as React from "react";
2
- import { latticeApi } from "@/api/client";
3
- import { Button } from "@/components/ui/button";
4
- import { Input } from "@/components/ui/input";
5
- import { t } from "@/i18n";
6
- import { useAppStore } from "@/store/appStore";
7
-
8
- const FLOW_USER_KEY = "lattice.productFlow.user";
9
-
10
- function readSavedFlowUser(): { email?: string; name?: string } | null {
11
- try {
12
- const raw = localStorage.getItem(FLOW_USER_KEY);
13
- return raw ? JSON.parse(raw) : null;
14
- } catch {}
15
- return null;
16
- }
17
-
18
- export function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
19
- const language = useAppStore((state) => state.language);
20
- const [email, setEmail] = React.useState(() => {
21
- return readSavedFlowUser()?.email || "you@local";
22
- });
23
- const [password, setPassword] = React.useState("");
24
- const [name, setName] = React.useState(() => readSavedFlowUser()?.name || "You");
25
- const [busy, setBusy] = React.useState(false);
26
- const [error, setError] = React.useState<string | null>(null);
27
-
28
- async function submit(event: React.FormEvent) {
29
- event.preventDefault();
30
- const cleanEmail = email.trim();
31
- const cleanPassword = password.trim();
32
- const cleanName = name.trim() || cleanEmail.split("@")[0] || "You";
33
- if (!cleanEmail || !cleanPassword) {
34
- setError(t(language, "flow.login.missing"));
35
- return;
36
- }
37
- setBusy(true);
38
- setError(null);
39
- const savedUser = readSavedFlowUser();
40
- let result = await latticeApi.login(cleanEmail, cleanPassword);
41
- if (!result.ok) {
42
- const profile = await latticeApi.profile();
43
- if (profile.ok && (!savedUser?.email || savedUser.email === cleanEmail)) {
44
- try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
45
- setBusy(false);
46
- onSuccess();
47
- return;
48
- }
49
- if (savedUser?.email && savedUser.email !== cleanEmail) {
50
- setBusy(false);
51
- setError(t(language, "flow.login.otherEmail"));
52
- return;
53
- }
54
- if (savedUser?.email === cleanEmail) {
55
- setBusy(false);
56
- setError(t(language, "flow.login.wrongPassword"));
57
- return;
58
- }
59
- const registered = await latticeApi.register({
60
- email: cleanEmail,
61
- password: cleanPassword,
62
- name: cleanName,
63
- nickname: cleanName,
64
- });
65
- if (registered.ok) result = await latticeApi.login(cleanEmail, cleanPassword);
66
- }
67
- if (!result.ok) {
68
- setBusy(false);
69
- setError(t(language, "flow.login.unavailable"));
70
- return;
71
- }
72
- try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
73
- setBusy(false);
74
- onSuccess();
75
- }
76
-
77
- return (
78
- <div>
79
- <div className="ritual-title">{t(language, "flow.login.title")}</div>
80
- <div className="ritual-subtitle">{t(language, "flow.login.body")}</div>
81
-
82
- <ProductPromise />
83
-
84
- <form onSubmit={submit} className="ritual-card ritual-form">
85
- <div className="ritual-field-stack">
86
- <div>
87
- <div className="ritual-field-label">{t(language, "flow.name")}</div>
88
- <Input value={name} onChange={(e) => setName(e.target.value)} placeholder={t(language, "flow.name.placeholder")} />
89
- </div>
90
- <div>
91
- <div className="ritual-field-label">{t(language, "flow.email")}</div>
92
- <Input value={email} onChange={(e) => setEmail(e.target.value)} type="email" placeholder={t(language, "flow.email.placeholder")} />
93
- </div>
94
- <div>
95
- <div className="ritual-field-label">{t(language, "flow.password")}</div>
96
- <Input value={password} onChange={(e) => setPassword(e.target.value)} type="password" placeholder={t(language, "flow.password.placeholder")} />
97
- </div>
98
- </div>
99
-
100
- {error && <div className="ritual-error" role="alert">{error}</div>}
101
-
102
- <Button type="submit" disabled={busy || !email.trim() || !password.trim()} className="ritual-full-button">
103
- {busy ? t(language, "flow.login.busy") : t(language, "flow.login.submit")}
104
- </Button>
105
- <div className="ritual-note">
106
- {t(language, "flow.login.note")}
107
- </div>
108
- </form>
109
- </div>
110
- );
111
- }
112
-
113
- function ProductPromise() {
114
- const language = useAppStore((state) => state.language);
115
- return (
116
- <div className="ritual-promise" aria-label={t(language, "flow.promise.aria")}>
117
- <div>
118
- <span>{t(language, "flow.promise.memory.k")}</span>
119
- <strong>{t(language, "flow.promise.memory.v")}</strong>
120
- </div>
121
- <div>
122
- <span>{t(language, "flow.promise.model.k")}</span>
123
- <strong>{t(language, "flow.promise.model.v")}</strong>
124
- </div>
125
- <div>
126
- <span>{t(language, "flow.promise.ownership.k")}</span>
127
- <strong>{t(language, "flow.promise.ownership.v")}</strong>
128
- </div>
129
- </div>
130
- );
131
- }
@@ -1,13 +0,0 @@
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";
@@ -1,116 +0,0 @@
1
- import { Button } from "@/components/ui/button";
2
- import { t, type Language } from "@/i18n";
3
- import { useAppStore } from "@/store/appStore";
4
- import { fallbackModel, type RecommendedModel } from "./recommendationModel";
5
- import { ArrowRight, Gauge, Star, Zap } from "lucide-react";
6
-
7
- export function RecommendationScreen({
8
- recommendations,
9
- onBack,
10
- onSkipModel,
11
- onSelect,
12
- }: {
13
- recommendations: RecommendedModel[];
14
- onBack: () => void;
15
- onSkipModel: () => void;
16
- onSelect: (model: RecommendedModel) => void;
17
- }) {
18
- const language = useAppStore((state) => state.language);
19
- const items = recommendations.length ? recommendations : [fallbackModel()];
20
- return (
21
- <div>
22
- <div className="ritual-title">{t(language, "flow.recommend.title")}</div>
23
- <div className="ritual-subtitle">{t(language, "flow.recommend.body")}</div>
24
-
25
- <div className="ritual-model-list">
26
- {items[0]?.supported ? (
27
- <div className="ritual-primary-cta">
28
- <Button onClick={() => onSelect(items[0])} className="ritual-primary-model-button">
29
- {t(language, "flow.recommend.primary")} <ArrowRight size={16} />
30
- </Button>
31
- <div className="ritual-time-estimate ritual-primary-note">{primaryNote(items[0], language)}</div>
32
- <div className="ritual-muted-hint ritual-next-hint">{t(language, "flow.recommend.nextHint")}</div>
33
- </div>
34
- ) : null}
35
- {items.slice(0, 3).map((model, index) => {
36
- const Icon = model.role === "best" ? Star : model.role === "faster" ? Zap : Gauge;
37
- return (
38
- <button
39
- key={`${model.role}-${model.id}`}
40
- className="ritual-model-card"
41
- onClick={() => model.supported && onSelect(model)}
42
- disabled={!model.supported}
43
- >
44
- <div className="ritual-model-heading">
45
- <Icon size={16} />
46
- <div className="role">{rankLabel(model.role, index, language)}</div>
47
- </div>
48
- <div className="name">{model.shortName}</div>
49
- <div className="reason">
50
- {model.reason} · {model.size || t(language, "flow.recommend.sizeReady")}
51
- {comparisonLabel(model.role, language) ? (
52
- <span className="ritual-model-comparison"> {comparisonLabel(model.role, language)}</span>
53
- ) : null}
54
- </div>
55
- <div className="ritual-model-stats">
56
- <span className="ritual-time-estimate">{timeEstimate(model, language)}</span>
57
- </div>
58
- {model.supported ? (
59
- <span className="ritual-model-choose">{t(language, "flow.recommend.choose")} <ArrowRight size={14} /></span>
60
- ) : (
61
- <div className="ritual-model-warning">{t(language, "flow.recommend.unsupported")}</div>
62
- )}
63
- </button>
64
- );
65
- })}
66
- <div className="ritual-time-note">{t(language, "flow.recommend.timeNote")}</div>
67
- </div>
68
-
69
- <div className="ritual-action-row">
70
- <Button variant="ghost" onClick={onBack}>{t(language, "flow.recommend.back")}</Button>
71
- <Button variant="outline" onClick={onSkipModel}>{t(language, "flow.recommend.skip")}</Button>
72
- <div className="ritual-muted-hint">{t(language, "flow.recommend.hint")}</div>
73
- </div>
74
- </div>
75
- );
76
- }
77
-
78
- function formatMinutes(minutes: number, language: Language) {
79
- return t(language, "flow.recommend.minutes", { count: minutes });
80
- }
81
-
82
- function timeEstimate(model: RecommendedModel, language: Language) {
83
- const response = model.estimatedFirstResponseSeconds;
84
- if (!model.downloadRequired || model.estimatedDownloadMinutes === 0) {
85
- return t(language, "flow.recommend.timeEstimate.ready", { response });
86
- }
87
- if (model.estimatedDownloadMinutes === null) {
88
- return t(language, "flow.recommend.timeEstimate.unknown", { response });
89
- }
90
- return t(language, "flow.recommend.timeEstimate", {
91
- download: formatMinutes(model.estimatedDownloadMinutes, language),
92
- response,
93
- });
94
- }
95
-
96
- function primaryNote(model: RecommendedModel, language: Language) {
97
- if (!model.downloadRequired || model.estimatedDownloadMinutes === 0 || model.estimatedDownloadMinutes === null) {
98
- return t(language, "flow.recommend.primaryNote.unknown");
99
- }
100
- return t(language, "flow.recommend.primaryNote", {
101
- time: formatMinutes(model.estimatedDownloadMinutes, language),
102
- });
103
- }
104
-
105
- function comparisonLabel(role: RecommendedModel["role"], language: Language) {
106
- if (role === "faster") return t(language, "flow.recommend.comparison.faster");
107
- if (role === "advanced") return t(language, "flow.recommend.comparison.advanced");
108
- return "";
109
- }
110
-
111
- function rankLabel(role: RecommendedModel["role"], index: number, language: Language) {
112
- if (role === "best") return t(language, "flow.recommend.rank.best");
113
- if (role === "faster") return t(language, "flow.recommend.rank.faster");
114
- if (role === "advanced") return t(language, "flow.recommend.rank.advanced");
115
- return t(language, "flow.recommend.rank.choice", { index: index + 1 });
116
- }
@@ -1,189 +0,0 @@
1
- import { asArray } from "@/lib/utils";
2
-
3
- export type FlowStep = "wake" | "login" | "analysis" | "recommend" | "install";
4
- export type ApiData = Record<string, unknown>;
5
-
6
- export type FlowAnalysis = {
7
- setup?: ApiData | null;
8
- models?: ApiData | null;
9
- recommendations?: ApiData | null;
10
- sysinfo?: ApiData | null;
11
- };
12
-
13
- export type RecommendedModel = {
14
- id: string;
15
- loadId: string;
16
- engine: string;
17
- name: string;
18
- shortName: string;
19
- family: string;
20
- size: string;
21
- role: "best" | "faster" | "advanced";
22
- reason: string;
23
- supported: boolean;
24
- downloadRequired: boolean;
25
- downloadSize: string;
26
- storageLocation: string;
27
- externalHost: string;
28
- /** Estimated download time in minutes. 0 when nothing needs downloading; null when size is unknown. */
29
- estimatedDownloadMinutes: number | null;
30
- /** Estimated seconds to first response after the model is loaded. */
31
- estimatedFirstResponseSeconds: number;
32
- /** Rough parameter scale (e.g. 8, 12, 70) parsed from the model name; null when unknown. */
33
- parameterBillions: number | null;
34
- };
35
-
36
- export function buildRecommendations(analysis: FlowAnalysis | null): RecommendedModel[] {
37
- const models = asRecord(analysis?.models);
38
- const modelRows = [
39
- ...asArray<ApiData>(models.recommended),
40
- ...asArray<ApiData>(models.catalog),
41
- ];
42
- const recommendationRoot = asRecord(analysis?.recommendations?.recommendations);
43
- const recRows = asArray<ApiData>(recommendationRoot.models);
44
- const topPick = asRecord(recommendationRoot.top_pick);
45
- const merged = new Map<string, ApiData>();
46
- for (const row of [...recRows, ...modelRows]) {
47
- const id = String(row.id || row.model_id || row.recommended_load_id || "");
48
- if (!id) continue;
49
- merged.set(id, { ...(merged.get(id) || {}), ...row });
50
- }
51
- if (topPick.id && !merged.has(String(topPick.id))) merged.set(String(topPick.id), topPick);
52
- const all = Array.from(merged.values()).map(toRecommendedModel).filter((item) => item.id);
53
- const supported = all.filter((item) => item.supported);
54
- const pool = supported.length ? supported : all;
55
- const byName = (pattern: RegExp) => pool.find((item) => pattern.test(`${item.name} ${item.id}`));
56
- const byId = (id?: unknown) => pool.find((item) => item.id === String(id));
57
- const best = byId(topPick.id) || byName(/gemma.*12|12b/i) || pool[0];
58
- 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);
59
- const advanced = pool.find((item) => item.id !== best?.id && item.id !== faster?.id && /26b|32b|70b|advanced/i.test(`${item.name} ${item.id}`))
60
- || pool.find((item) => item.id !== best?.id && item.id !== faster?.id);
61
- return [
62
- best ? { ...best, role: "best" as const, reason: best.reason || "best" } : null,
63
- faster ? { ...faster, role: "faster" as const, reason: faster.reason || "faster" } : null,
64
- advanced ? { ...advanced, role: "advanced" as const, reason: advanced.reason || "advanced" } : null,
65
- ].filter(Boolean) as RecommendedModel[];
66
- }
67
-
68
- export function fallbackModel(): RecommendedModel {
69
- return {
70
- id: "mlx-community/Qwen3-VL-8B-Instruct-4bit",
71
- loadId: "mlx-community/Qwen3-VL-8B-Instruct-4bit",
72
- engine: "local_mlx",
73
- name: "Qwen3-VL 8B",
74
- shortName: "Qwen 3",
75
- family: "Qwen 3",
76
- size: "",
77
- role: "best",
78
- reason: "best",
79
- supported: true,
80
- downloadRequired: false,
81
- downloadSize: "",
82
- storageLocation: "~/.latticeai/models",
83
- externalHost: "",
84
- estimatedDownloadMinutes: 0,
85
- estimatedFirstResponseSeconds: 5,
86
- parameterBillions: 8,
87
- };
88
- }
89
-
90
- function toRecommendedModel(row: ApiData): RecommendedModel {
91
- const compatibility = asRecord(row.runtime_compatibility);
92
- const id = String(row.id || row.model_id || row.recommended_load_id || "");
93
- const loadId = String(row.recommended_load_id || row.load_id || id);
94
- const name = String(row.display_name || row.name || id || "recommended_brain");
95
- const supported = row.load_status !== "unsupported"
96
- && row.load_status !== "runtime_update_needed"
97
- && row.status !== "not_recommended"
98
- && compatibility.supported !== false;
99
- const downloadRequired = Boolean(row.download_required);
100
- const downloadSize = String(row.download_size || row.size || "");
101
- const parameterBillions = parseParameterBillions(`${name} ${id}`);
102
- return {
103
- id,
104
- loadId,
105
- engine: String(row.recommended_engine || row.engine || "local_mlx"),
106
- name,
107
- shortName: friendlyModelName(name || id),
108
- family: friendlyModelName(String(row.family || name || "local_brain")),
109
- size: String(row.size || ""),
110
- role: "best",
111
- reason: String(row.reason || ""),
112
- supported,
113
- downloadRequired,
114
- downloadSize,
115
- storageLocation: String(row.storage_location || row.local_path || "~/.latticeai/models"),
116
- externalHost: externalHostLabel(row),
117
- estimatedDownloadMinutes: downloadRequired ? estimateDownloadMinutes(downloadSize) : 0,
118
- estimatedFirstResponseSeconds: estimateFirstResponseSeconds(parameterBillions),
119
- parameterBillions,
120
- };
121
- }
122
-
123
- /** Parse a download size string like "8GB", "8.5 GB", "512MB" into megabytes; null when unparseable. */
124
- export function parseDownloadMegabytes(value: string): number | null {
125
- const match = String(value || "").match(/([\d.]+)\s*(t|g|m)?b?/i);
126
- if (!match) return null;
127
- const amount = Number(match[1]);
128
- if (!Number.isFinite(amount) || amount <= 0) return null;
129
- const unit = (match[2] || "g").toLowerCase();
130
- if (unit === "t") return amount * 1024 * 1024;
131
- if (unit === "m") return amount;
132
- return amount * 1024; // gigabytes (default assumption for model weights)
133
- }
134
-
135
- /**
136
- * Estimate download minutes from a size string, assuming a conservative ~15 Mbps
137
- * (≈1.875 MB/s) average home connection. Returns null when the size is unknown so
138
- * the UI can fall back to a "time unknown" message instead of showing 0.
139
- */
140
- export function estimateDownloadMinutes(downloadSize: string): number | null {
141
- const megabytes = parseDownloadMegabytes(downloadSize);
142
- if (megabytes === null) return null;
143
- const megabytesPerSecond = 15 / 8; // 15 Mbps
144
- const minutes = megabytes / megabytesPerSecond / 60;
145
- return Math.max(1, Math.round(minutes));
146
- }
147
-
148
- /** Pull a rough parameter scale (billions) out of a model name like "Gemma 12B" or "qwen-8b". */
149
- export function parseParameterBillions(text: string): number | null {
150
- const match = String(text || "").match(/(\d{1,3}(?:\.\d)?)\s*b\b/i);
151
- if (!match) return null;
152
- const value = Number(match[1]);
153
- return Number.isFinite(value) && value > 0 ? value : null;
154
- }
155
-
156
- /** Estimate seconds to first response from model scale: 8B→5s, 12B→10s, 70B→30s. */
157
- export function estimateFirstResponseSeconds(parameterBillions: number | null): number {
158
- if (parameterBillions === null) return 8;
159
- if (parameterBillions <= 9) return 5;
160
- if (parameterBillions <= 16) return 10;
161
- if (parameterBillions <= 40) return 18;
162
- return 30;
163
- }
164
-
165
- function externalHostLabel(row: ApiData) {
166
- const raw = String(row.source_url || row.download_url || row.repository || row.provider || row.id || "");
167
- if (!raw) return "";
168
- if (/huggingface|hf\\.co|mlx-community/i.test(raw)) return "huggingface";
169
- if (/ollama/i.test(raw)) return "ollama";
170
- return raw.replace(/^https?:\/\//, "").split("/")[0] || raw;
171
- }
172
-
173
- export function friendlyModelName(value: string) {
174
- return String(value || "recommended_brain")
175
- .replace(/^mlx-community\//i, "")
176
- .replace(/[-_]?Instruct/gi, "")
177
- .replace(/[-_]?4bit/gi, "")
178
- .replace(/Qwen3[-_ ]?VL/gi, "Qwen 3")
179
- .replace(/Qwen3/gi, "Qwen 3")
180
- .replace(/Gemma[-_ ]?4/gi, "Gemma 4")
181
- .replace(/A4B/gi, "")
182
- .replace(/[-_]+/g, " ")
183
- .replace(/\s+/g, " ")
184
- .trim();
185
- }
186
-
187
- export function asRecord(value: unknown): ApiData {
188
- return value && typeof value === "object" && !Array.isArray(value) ? value as ApiData : {};
189
- }