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.
Files changed (65) hide show
  1. package/README.md +29 -31
  2. package/docs/CHANGELOG.md +48 -0
  3. package/frontend/src/App.tsx +80 -0
  4. package/frontend/src/api/client.ts +11 -0
  5. package/frontend/src/components/AdminAccessGate.tsx +70 -0
  6. package/frontend/src/components/BrainConversation.tsx +232 -8
  7. package/frontend/src/components/FeedbackState.tsx +45 -0
  8. package/frontend/src/components/WorkspaceProfileSwitcher.tsx +176 -0
  9. package/frontend/src/components/onboarding/AnalysisScreen.tsx +20 -12
  10. package/frontend/src/components/onboarding/InstallScreen.tsx +55 -0
  11. package/frontend/src/components/onboarding/RecommendationScreen.tsx +55 -5
  12. package/frontend/src/components/onboarding/recommendationModel.ts +59 -2
  13. package/frontend/src/features/brain/BrainConversation.tsx +382 -7
  14. package/frontend/src/features/brain/BrainGraphLayer.tsx +258 -25
  15. package/frontend/src/features/brain/BrainHome.tsx +300 -1
  16. package/frontend/src/features/brain/brainData.ts +25 -1
  17. package/frontend/src/features/brain/graphLayout.ts +12 -1
  18. package/frontend/src/features/brain/types.ts +54 -0
  19. package/frontend/src/i18n.ts +374 -0
  20. package/frontend/src/store/appStore.ts +15 -0
  21. package/frontend/src/styles.css +1383 -41
  22. package/lattice_brain/__init__.py +1 -1
  23. package/lattice_brain/runtime/multi_agent.py +1 -1
  24. package/latticeai/__init__.py +1 -1
  25. package/latticeai/api/workspace.py +59 -0
  26. package/latticeai/core/marketplace.py +1 -1
  27. package/latticeai/core/workspace_os.py +1 -1
  28. package/package.json +1 -1
  29. package/scripts/brain_quality_eval.py +118 -0
  30. package/src-tauri/Cargo.lock +1 -1
  31. package/src-tauri/Cargo.toml +1 -1
  32. package/src-tauri/tauri.conf.json +1 -1
  33. package/static/app/asset-manifest.json +28 -28
  34. package/static/app/assets/Act-BHEb8bAM.js +2 -0
  35. package/static/app/assets/{Act-DGN37eR4.js.map → Act-BHEb8bAM.js.map} +1 -1
  36. package/static/app/assets/Brain-yvR7xkrV.js +322 -0
  37. package/static/app/assets/Brain-yvR7xkrV.js.map +1 -0
  38. package/static/app/assets/Capture-BrAmsSEH.js +2 -0
  39. package/static/app/assets/Capture-BrAmsSEH.js.map +1 -0
  40. package/static/app/assets/Library-BOVzYfxI.js +2 -0
  41. package/static/app/assets/{Library-BbagRFyd.js.map → Library-BOVzYfxI.js.map} +1 -1
  42. package/static/app/assets/System-D4WaN4kj.js +2 -0
  43. package/static/app/assets/System-D4WaN4kj.js.map +1 -0
  44. package/static/app/assets/index-BOB-W1FZ.js +17 -0
  45. package/static/app/assets/index-BOB-W1FZ.js.map +1 -0
  46. package/static/app/assets/index-DvLsGy-z.css +2 -0
  47. package/static/app/assets/primitives-C3_BfUb8.js +2 -0
  48. package/static/app/assets/primitives-C3_BfUb8.js.map +1 -0
  49. package/static/app/assets/textarea-CbvhHvzg.js +2 -0
  50. package/static/app/assets/{textarea-C2PLu6_O.js.map → textarea-CbvhHvzg.js.map} +1 -1
  51. package/static/app/index.html +2 -2
  52. package/static/app/assets/Act-DGN37eR4.js +0 -2
  53. package/static/app/assets/Brain-HDcIwPHW.js +0 -322
  54. package/static/app/assets/Brain-HDcIwPHW.js.map +0 -1
  55. package/static/app/assets/Capture--eZfb_Ex.js +0 -2
  56. package/static/app/assets/Capture--eZfb_Ex.js.map +0 -1
  57. package/static/app/assets/Library-BbagRFyd.js +0 -2
  58. package/static/app/assets/System-C6pd3Mp_.js +0 -2
  59. package/static/app/assets/System-C6pd3Mp_.js.map +0 -1
  60. package/static/app/assets/index-C950aES_.js +0 -17
  61. package/static/app/assets/index-C950aES_.js.map +0 -1
  62. package/static/app/assets/index-ztppMg0c.css +0 -2
  63. package/static/app/assets/primitives-Bcm5lZRu.js +0 -2
  64. package/static/app/assets/primitives-Bcm5lZRu.js.map +0 -1
  65. 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
- <Button onClick={() => onSelect(items[0])} className="ritual-primary-model-button">
27
- {t(language, "flow.recommend.primary")}
28
- </Button>
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">{model.reason} · {model.size || t(language, "flow.recommend.sizeReady")}</div>
40
- {!model.supported && <div className="ritual-model-warning">{t(language, "flow.recommend.unsupported")}</div>}
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: Boolean(row.download_required),
102
- downloadSize: String(row.download_size || row.size || ""),
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 "";