ltcai 6.7.0 → 7.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 +29 -31
- package/docs/CHANGELOG.md +48 -0
- package/frontend/src/App.tsx +80 -0
- package/frontend/src/api/client.ts +11 -0
- package/frontend/src/components/AdminAccessGate.tsx +70 -0
- package/frontend/src/components/BrainConversation.tsx +232 -8
- package/frontend/src/components/FeedbackState.tsx +45 -0
- package/frontend/src/components/WorkspaceProfileSwitcher.tsx +176 -0
- package/frontend/src/components/onboarding/AnalysisScreen.tsx +20 -12
- package/frontend/src/components/onboarding/InstallScreen.tsx +55 -0
- package/frontend/src/components/onboarding/RecommendationScreen.tsx +55 -5
- package/frontend/src/components/onboarding/recommendationModel.ts +59 -2
- package/frontend/src/features/brain/BrainConversation.tsx +382 -7
- package/frontend/src/features/brain/BrainGraphLayer.tsx +258 -25
- package/frontend/src/features/brain/BrainHome.tsx +300 -1
- package/frontend/src/features/brain/brainData.ts +25 -1
- package/frontend/src/features/brain/graphLayout.ts +12 -1
- package/frontend/src/features/brain/types.ts +54 -0
- package/frontend/src/i18n.ts +374 -0
- package/frontend/src/store/appStore.ts +15 -0
- package/frontend/src/styles.css +1383 -41
- 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/workspace.py +59 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/package.json +1 -1
- package/scripts/brain_quality_eval.py +118 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +28 -28
- package/static/app/assets/Act-BHEb8bAM.js +2 -0
- package/static/app/assets/{Act-DGN37eR4.js.map → Act-BHEb8bAM.js.map} +1 -1
- package/static/app/assets/Brain-yvR7xkrV.js +322 -0
- package/static/app/assets/Brain-yvR7xkrV.js.map +1 -0
- package/static/app/assets/Capture-BrAmsSEH.js +2 -0
- package/static/app/assets/Capture-BrAmsSEH.js.map +1 -0
- package/static/app/assets/Library-BOVzYfxI.js +2 -0
- package/static/app/assets/{Library-BbagRFyd.js.map → Library-BOVzYfxI.js.map} +1 -1
- package/static/app/assets/System-D4WaN4kj.js +2 -0
- package/static/app/assets/System-D4WaN4kj.js.map +1 -0
- package/static/app/assets/index-BOB-W1FZ.js +17 -0
- package/static/app/assets/index-BOB-W1FZ.js.map +1 -0
- package/static/app/assets/index-DvLsGy-z.css +2 -0
- package/static/app/assets/primitives-C3_BfUb8.js +2 -0
- package/static/app/assets/primitives-C3_BfUb8.js.map +1 -0
- package/static/app/assets/textarea-CbvhHvzg.js +2 -0
- package/static/app/assets/{textarea-C2PLu6_O.js.map → textarea-CbvhHvzg.js.map} +1 -1
- package/static/app/index.html +2 -2
- package/static/app/assets/Act-DGN37eR4.js +0 -2
- package/static/app/assets/Brain-HDcIwPHW.js +0 -322
- package/static/app/assets/Brain-HDcIwPHW.js.map +0 -1
- package/static/app/assets/Capture--eZfb_Ex.js +0 -2
- package/static/app/assets/Capture--eZfb_Ex.js.map +0 -1
- package/static/app/assets/Library-BbagRFyd.js +0 -2
- package/static/app/assets/System-C6pd3Mp_.js +0 -2
- package/static/app/assets/System-C6pd3Mp_.js.map +0 -1
- package/static/app/assets/index-C950aES_.js +0 -17
- package/static/app/assets/index-C950aES_.js.map +0 -1
- package/static/app/assets/index-ztppMg0c.css +0 -2
- package/static/app/assets/primitives-Bcm5lZRu.js +0 -2
- package/static/app/assets/primitives-Bcm5lZRu.js.map +0 -1
- package/static/app/assets/textarea-C2PLu6_O.js +0 -2
|
@@ -81,6 +81,20 @@ export function InstallScreen({
|
|
|
81
81
|
{t(language, "flow.install.body")}
|
|
82
82
|
</div>
|
|
83
83
|
|
|
84
|
+
<section className="ritual-card ritual-expected-card" aria-label={t(language, "flow.install.expectedTitle")}>
|
|
85
|
+
<div className="ritual-fact-label">{t(language, "flow.install.expectedTitle")}</div>
|
|
86
|
+
<div className="ritual-time-estimate ritual-expected-line">{expectedLine(model, language)}</div>
|
|
87
|
+
<ol className="ritual-timeline">
|
|
88
|
+
{timelineSteps(model, language).map((step) => (
|
|
89
|
+
<li key={step.key} className="ritual-timeline-step">
|
|
90
|
+
<span className="ritual-timeline-name">{step.name}</span>
|
|
91
|
+
<span className="ritual-timeline-time">{step.time}</span>
|
|
92
|
+
</li>
|
|
93
|
+
))}
|
|
94
|
+
</ol>
|
|
95
|
+
<div className="ritual-muted-hint">{t(language, "flow.install.expectedCompletion")}</div>
|
|
96
|
+
</section>
|
|
97
|
+
|
|
84
98
|
<div className="ritual-install-brain">
|
|
85
99
|
<LivingBrain
|
|
86
100
|
state={brainStateForStage}
|
|
@@ -141,6 +155,47 @@ export function InstallScreen({
|
|
|
141
155
|
);
|
|
142
156
|
}
|
|
143
157
|
|
|
158
|
+
function expectedLine(model: RecommendedModel, language: Language) {
|
|
159
|
+
const response = model.estimatedFirstResponseSeconds;
|
|
160
|
+
if (!model.downloadRequired || model.estimatedDownloadMinutes === 0) {
|
|
161
|
+
return t(language, "flow.install.expected.ready", { response });
|
|
162
|
+
}
|
|
163
|
+
if (model.estimatedDownloadMinutes === null) {
|
|
164
|
+
return t(language, "flow.install.expected.unknown", { response });
|
|
165
|
+
}
|
|
166
|
+
return t(language, "flow.install.expected", {
|
|
167
|
+
download: t(language, "flow.recommend.minutes", { count: model.estimatedDownloadMinutes }),
|
|
168
|
+
response,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function timelineSteps(model: RecommendedModel, language: Language) {
|
|
173
|
+
const downloadTime = model.downloadRequired && model.estimatedDownloadMinutes
|
|
174
|
+
? t(language, "flow.install.timelineApprox", {
|
|
175
|
+
time: t(language, "flow.recommend.minutes", { count: model.estimatedDownloadMinutes }),
|
|
176
|
+
})
|
|
177
|
+
: t(language, "flow.install.timelineQuick");
|
|
178
|
+
return [
|
|
179
|
+
{
|
|
180
|
+
key: "download",
|
|
181
|
+
name: t(language, "flow.install.timelineStep.download"),
|
|
182
|
+
time: downloadTime,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
key: "validate",
|
|
186
|
+
name: t(language, "flow.install.timelineStep.validate"),
|
|
187
|
+
time: t(language, "flow.install.timelineQuick"),
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
key: "load",
|
|
191
|
+
name: t(language, "flow.install.timelineStep.load"),
|
|
192
|
+
time: t(language, "flow.install.timelineApprox", {
|
|
193
|
+
time: `${model.estimatedFirstResponseSeconds}s`,
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
|
|
144
199
|
function friendlyInstallStage(stage: string): InstallStage {
|
|
145
200
|
if (/download|pull|weights/i.test(stage)) return "download";
|
|
146
201
|
if (/smoke|validate|verify|test/i.test(stage)) return "validate";
|
|
@@ -23,9 +23,13 @@ export function RecommendationScreen({
|
|
|
23
23
|
|
|
24
24
|
<div className="ritual-model-list">
|
|
25
25
|
{items[0]?.supported ? (
|
|
26
|
-
<
|
|
27
|
-
{
|
|
28
|
-
|
|
26
|
+
<div className="ritual-primary-cta">
|
|
27
|
+
<Button onClick={() => onSelect(items[0])} className="ritual-primary-model-button">
|
|
28
|
+
{t(language, "flow.recommend.primary")}
|
|
29
|
+
</Button>
|
|
30
|
+
<div className="ritual-time-estimate ritual-primary-note">{primaryNote(items[0], language)}</div>
|
|
31
|
+
<div className="ritual-muted-hint ritual-next-hint">{t(language, "flow.recommend.nextHint")}</div>
|
|
32
|
+
</div>
|
|
29
33
|
) : null}
|
|
30
34
|
{items.slice(0, 3).map((model, index) => (
|
|
31
35
|
<button
|
|
@@ -36,10 +40,23 @@ export function RecommendationScreen({
|
|
|
36
40
|
>
|
|
37
41
|
<div className="role">{rankLabel(model.role, index, language)}</div>
|
|
38
42
|
<div className="name">{model.shortName}</div>
|
|
39
|
-
<div className="reason">
|
|
40
|
-
|
|
43
|
+
<div className="reason">
|
|
44
|
+
{model.reason} · {model.size || t(language, "flow.recommend.sizeReady")}
|
|
45
|
+
{comparisonLabel(model.role, language) ? (
|
|
46
|
+
<span className="ritual-model-comparison"> {comparisonLabel(model.role, language)}</span>
|
|
47
|
+
) : null}
|
|
48
|
+
</div>
|
|
49
|
+
<div className="ritual-model-stats">
|
|
50
|
+
<span className="ritual-time-estimate">{timeEstimate(model, language)}</span>
|
|
51
|
+
</div>
|
|
52
|
+
{model.supported ? (
|
|
53
|
+
<span className="ritual-model-choose">{t(language, "flow.recommend.choose")}</span>
|
|
54
|
+
) : (
|
|
55
|
+
<div className="ritual-model-warning">{t(language, "flow.recommend.unsupported")}</div>
|
|
56
|
+
)}
|
|
41
57
|
</button>
|
|
42
58
|
))}
|
|
59
|
+
<div className="ritual-time-note">{t(language, "flow.recommend.timeNote")}</div>
|
|
43
60
|
</div>
|
|
44
61
|
|
|
45
62
|
<div className="ritual-action-row">
|
|
@@ -51,6 +68,39 @@ export function RecommendationScreen({
|
|
|
51
68
|
);
|
|
52
69
|
}
|
|
53
70
|
|
|
71
|
+
function formatMinutes(minutes: number, language: Language) {
|
|
72
|
+
return t(language, "flow.recommend.minutes", { count: minutes });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function timeEstimate(model: RecommendedModel, language: Language) {
|
|
76
|
+
const response = model.estimatedFirstResponseSeconds;
|
|
77
|
+
if (!model.downloadRequired || model.estimatedDownloadMinutes === 0) {
|
|
78
|
+
return t(language, "flow.recommend.timeEstimate.ready", { response });
|
|
79
|
+
}
|
|
80
|
+
if (model.estimatedDownloadMinutes === null) {
|
|
81
|
+
return t(language, "flow.recommend.timeEstimate.unknown", { response });
|
|
82
|
+
}
|
|
83
|
+
return t(language, "flow.recommend.timeEstimate", {
|
|
84
|
+
download: formatMinutes(model.estimatedDownloadMinutes, language),
|
|
85
|
+
response,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function primaryNote(model: RecommendedModel, language: Language) {
|
|
90
|
+
if (!model.downloadRequired || model.estimatedDownloadMinutes === 0 || model.estimatedDownloadMinutes === null) {
|
|
91
|
+
return t(language, "flow.recommend.primaryNote.unknown");
|
|
92
|
+
}
|
|
93
|
+
return t(language, "flow.recommend.primaryNote", {
|
|
94
|
+
time: formatMinutes(model.estimatedDownloadMinutes, language),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function comparisonLabel(role: RecommendedModel["role"], language: Language) {
|
|
99
|
+
if (role === "faster") return t(language, "flow.recommend.comparison.faster");
|
|
100
|
+
if (role === "advanced") return t(language, "flow.recommend.comparison.advanced");
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
|
|
54
104
|
function rankLabel(role: RecommendedModel["role"], index: number, language: Language) {
|
|
55
105
|
if (role === "best") return t(language, "flow.recommend.rank.best");
|
|
56
106
|
if (role === "faster") return t(language, "flow.recommend.rank.faster");
|
|
@@ -25,6 +25,12 @@ export type RecommendedModel = {
|
|
|
25
25
|
downloadSize: string;
|
|
26
26
|
storageLocation: string;
|
|
27
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;
|
|
28
34
|
};
|
|
29
35
|
|
|
30
36
|
export function buildRecommendations(analysis: FlowAnalysis | null): RecommendedModel[] {
|
|
@@ -75,6 +81,9 @@ export function fallbackModel(): RecommendedModel {
|
|
|
75
81
|
downloadSize: "",
|
|
76
82
|
storageLocation: "~/.latticeai/models",
|
|
77
83
|
externalHost: "",
|
|
84
|
+
estimatedDownloadMinutes: 0,
|
|
85
|
+
estimatedFirstResponseSeconds: 5,
|
|
86
|
+
parameterBillions: 8,
|
|
78
87
|
};
|
|
79
88
|
}
|
|
80
89
|
|
|
@@ -87,6 +96,9 @@ function toRecommendedModel(row: ApiData): RecommendedModel {
|
|
|
87
96
|
&& row.load_status !== "runtime_update_needed"
|
|
88
97
|
&& row.status !== "not_recommended"
|
|
89
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}`);
|
|
90
102
|
return {
|
|
91
103
|
id,
|
|
92
104
|
loadId,
|
|
@@ -98,13 +110,58 @@ function toRecommendedModel(row: ApiData): RecommendedModel {
|
|
|
98
110
|
role: "best",
|
|
99
111
|
reason: String(row.reason || ""),
|
|
100
112
|
supported,
|
|
101
|
-
downloadRequired
|
|
102
|
-
downloadSize
|
|
113
|
+
downloadRequired,
|
|
114
|
+
downloadSize,
|
|
103
115
|
storageLocation: String(row.storage_location || row.local_path || "~/.latticeai/models"),
|
|
104
116
|
externalHost: externalHostLabel(row),
|
|
117
|
+
estimatedDownloadMinutes: downloadRequired ? estimateDownloadMinutes(downloadSize) : 0,
|
|
118
|
+
estimatedFirstResponseSeconds: estimateFirstResponseSeconds(parameterBillions),
|
|
119
|
+
parameterBillions,
|
|
105
120
|
};
|
|
106
121
|
}
|
|
107
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
|
+
|
|
108
165
|
function externalHostLabel(row: ApiData) {
|
|
109
166
|
const raw = String(row.source_url || row.download_url || row.repository || row.provider || row.id || "");
|
|
110
167
|
if (!raw) return "";
|